diff --git a/src/tui/app.rs b/src/tui/app.rs index cedb9f8..7fa5d26 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -1,6 +1,7 @@ use crate::tui::component::{Action, Component}; use crate::tui::editor::Editor; use crate::tui::explorer::Explorer; +use anyhow::{Result, anyhow}; use ratatui::buffer::Buffer; use ratatui::crossterm::event; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; @@ -11,23 +12,71 @@ use ratatui::{DefaultTerminal, symbols}; use std::path::PathBuf; use std::time::Duration; +pub enum AppComponents<'a> { + AppEditor(Editor), + AppExplorer(Explorer<'a>), + AppComponent(Box), +} + pub struct App<'a> { - explorer: Explorer<'a>, - editor: Editor, + components: Vec>, } impl<'a> App<'a> { pub(crate) fn new(root_path: PathBuf) -> Self { let mut app = Self { - explorer: Explorer::new(&root_path), - editor: Editor::new(), + components: vec![ + AppComponents::AppExplorer(Explorer::new(&root_path)), + AppComponents::AppEditor(Editor::new()), + ], }; - app.editor + app.get_editor_mut() + .unwrap() .set_contents(&root_path.join("src/tui/app.rs")) .expect("Failed to set editor contents."); app } + fn get_explorer(&self) -> Result<&Explorer<'a>> { + for component in &self.components { + if let AppComponents::AppExplorer(explorer) = component { + return Ok(explorer); + } + } + Err(anyhow::anyhow!("Failed to find project explorer widget.")) + } + + fn get_explorer_mut(&mut self) -> Result<&mut Explorer<'a>> { + for component in &mut self.components { + if let AppComponents::AppExplorer(explorer) = component { + return Ok(explorer); + } + } + Err(anyhow::anyhow!("Failed to find project explorer widget.")) + } + + fn get_editor(&self) -> Option<&Editor> { + for component in &self.components { + if let AppComponents::AppEditor(editor) = component { + return Some(editor); + } + } + + // There is no editor currently opened. + None + } + + fn get_editor_mut(&mut self) -> Option<&mut Editor> { + for component in &mut self.components { + if let AppComponents::AppEditor(editor) = component { + return Some(editor); + } + } + + // There is no editor currently opened. + None + } + fn get_event(&mut self) -> Option { if !event::poll(Duration::from_millis(250)).expect("event poll failed") { return None; @@ -36,7 +85,7 @@ impl<'a> App<'a> { event::read().ok() } - pub fn run(mut self, mut terminal: DefaultTerminal) -> anyhow::Result<()> { + pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { loop { // TODO: Handle events based on which component is active. terminal.draw(|f| { @@ -66,15 +115,16 @@ impl<'a> App<'a> { fn draw_tabs(&self, area: Rect, buf: &mut Buffer) { // Determine the tab title from the current file (or use a fallback). - let title = self - .editor - .file_path - .as_ref() - .and_then(|p| p.file_name()) - .and_then(|s| s.to_str()) - .unwrap_or("Untitled"); + let mut title: Option<&str> = None; + if let Some(editor) = self.get_editor() { + title = editor + .file_path + .as_ref() + .and_then(|p| p.file_name()) + .and_then(|s| s.to_str()) + } - Tabs::new(vec![title]) + Tabs::new(vec![title.unwrap_or("Unknown")]) .divider(symbols::DOT) .block( Block::default() @@ -102,17 +152,22 @@ impl<'a> App<'a> { /// Refresh the contents of the editor to match the selected TreeItem in the file Explorer. /// If the selected item is not a file, this does nothing. - fn refresh_editor_contents(&mut self) { - if let Some(current_file_path) = self.editor.file_path.clone() { - if let Some(selected_path_string) = self.explorer.selected() { - let selected_pathbuf = PathBuf::from(selected_path_string); - if std::path::absolute(&selected_pathbuf).unwrap().is_file() - && selected_pathbuf != current_file_path - { - self.editor.set_contents(&selected_pathbuf.into()).ok(); - } + fn refresh_editor_contents(&mut self) -> Result<()> { + // Use the currently selected TreeItem or get an absolute path to this source file. + let selected_pathbuf = match self.get_explorer()?.selected() { + Ok(path) => PathBuf::from(path), + Err(_) => PathBuf::from(std::path::absolute(file!())?.to_string_lossy().to_string()), + }; + let editor = self + .get_editor_mut() + .expect("Failed to get active editor while refreshing contents."); + if let Some(current_file_path) = editor.file_path.clone() { + if selected_pathbuf == current_file_path || !selected_pathbuf.is_file() { + return Ok(()); } + return editor.set_contents(&selected_pathbuf); } + Err(anyhow!("Failed to refresh editor contents")) } } @@ -149,12 +204,13 @@ impl<'a> Widget for &mut App<'a> { self.draw_status(vertical[0], buf); self.draw_terminal(vertical[2], buf); - - self.explorer.render(horizontal[0], buf); - + if let Ok(explorer) = self.get_explorer_mut() { + explorer.render(horizontal[0], buf); + } self.draw_tabs(editor_layout[0], buf); - self.refresh_editor_contents(); - self.editor.render(editor_layout[1], buf); + self.refresh_editor_contents() + .expect("Failed to refresh editor contents."); + self.get_editor_mut().unwrap().render(editor_layout[1], buf); } } @@ -169,34 +225,35 @@ impl<'a> Component for App<'a> { /// /// App could then provide helpers for altering Component state based on TUI grouping.. /// (such as editor tabs, file explorer, status bars, etc..) + /// + /// Handles events for the App and delegates to attached Components. fn handle_event(&mut self, event: Event) -> Action { // Handle events in the primary application. if let Some(key_event) = event.as_key_event() { match self.handle_key_events(key_event) { Action::Quit => return Action::Quit, - Action::Handled => { - // dbg!(format!("Handled event: {:?}", self.id())); - return Action::Handled; - } + Action::Handled => return Action::Handled, _ => {} } } - self.explorer.handle_event(event.clone()); - self.editor.handle_event(event.clone()); // Handle events for all components. - // for component in &mut self.components { - // dbg!(format!("Handling event: {:?}", component.id())); - // // Actions returned here abort the input handling iteration. - // match component.handle_event(event.clone()) { - // Action::Quit => return Action::Quit, - // Action::Handled => return Action::Handled, - // _ => continue, - // } - // } + for component in &mut self.components { + let action = match component { + AppComponents::AppEditor(editor) => editor.handle_event(event.clone()), + AppComponents::AppExplorer(explorer) => explorer.handle_event(event.clone()), + AppComponents::AppComponent(comp) => comp.handle_event(event.clone()), + }; + // Actions returned here abort the input handling iteration. + match action { + Action::Quit | Action::Handled => return action, + _ => {} + } + } Action::Noop } + /// Handles key events for the App Component only. fn handle_key_events(&mut self, key: KeyEvent) -> Action { match key { KeyEvent { @@ -205,10 +262,7 @@ impl<'a> Component for App<'a> { kind: KeyEventKind::Press, state: _state, } => Action::Quit, - key_event => { - // Pass the key event to each component that can handle it. - self.explorer.handle_key_events(key_event) - } + _ => Action::Noop, } } } diff --git a/src/tui/editor.rs b/src/tui/editor.rs index e78f66d..71eae11 100644 --- a/src/tui/editor.rs +++ b/src/tui/editor.rs @@ -1,6 +1,6 @@ use crate::tui::component::{Action, Component}; -use anyhow::Result; +use anyhow::{Context, Result}; use edtui::{ EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter, }; @@ -40,9 +40,8 @@ impl Editor { .collect(); self.file_path = Some(path.clone()); self.state.lines = Lines::new(lines); - return Ok(()); } - Err(anyhow::Error::msg("Failed to set editor file contents")) + Ok(()) } pub fn save(&self) -> Result<()> { diff --git a/src/tui/explorer.rs b/src/tui/explorer.rs index 0c843b1..4b72648 100644 --- a/src/tui/explorer.rs +++ b/src/tui/explorer.rs @@ -102,8 +102,11 @@ impl<'a> Explorer<'a> { ) } - pub fn selected(&self) -> Option<&String> { - self.tree_state.selected().last() + pub fn selected(&self) -> Result { + if let Some(path) = self.tree_state.selected().last() { + return Ok(std::path::absolute(path)?.to_str().unwrap().to_string()); + } + Err(anyhow::anyhow!("Failed to get selected TreeItem")) } }