From 029e0b2952a300034ff090162c5d5ed69eb285c4 Mon Sep 17 00:00:00 2001 From: Shaun Reed Date: Sat, 24 Jan 2026 10:47:29 -0500 Subject: [PATCH] [tui] Remove AppComponent data. It just seems to be simpler this way. --- src/tui.rs | 11 +- src/tui/app.rs | 242 +++++++++++++++++++------------------------ src/tui/component.rs | 4 - src/tui/editor.rs | 33 ++---- src/tui/explorer.rs | 39 +++---- src/tui/logger.rs | 35 ++----- 6 files changed, 142 insertions(+), 222 deletions(-) diff --git a/src/tui.rs b/src/tui.rs index 356f9e0..fc67c72 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -33,10 +33,13 @@ impl Tui { let mut dir = env::temp_dir(); dir.push("clide.log"); - let file_options = TuiLoggerFile::new(dir.to_str().unwrap()) - .output_level(Some(TuiLoggerLevelOutput::Abbreviated)) - .output_file(false) - .output_separator(':'); + let file_options = TuiLoggerFile::new( + dir.to_str() + .context("Failed to set temp directory for file logging")?, + ) + .output_level(Some(TuiLoggerLevelOutput::Abbreviated)) + .output_file(false) + .output_separator(':'); set_log_file(file_options); debug!(target:"Tui", "Logging to file: {dir:?}"); diff --git a/src/tui/app.rs b/src/tui/app.rs index a356f5b..d2140c7 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -1,15 +1,15 @@ +use crate::tui::app::AppComponent::{AppEditor, AppExplorer, AppLogger}; use crate::tui::component::{Action, Component, Focus, FocusState}; use crate::tui::editor::Editor; use crate::tui::explorer::Explorer; use crate::tui::logger::Logger; -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result}; use log::{debug, error, info, trace, warn}; use ratatui::buffer::Buffer; use ratatui::crossterm::event; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::prelude::{Color, Style, Widget}; -use ratatui::text::Text; use ratatui::widgets::{Block, Borders, Padding, Paragraph, Tabs, Wrap}; use ratatui::{DefaultTerminal, symbols}; use std::path::PathBuf; @@ -17,61 +17,50 @@ use std::time::Duration; // TODO: Need a way to dynamically run Widget::render on all widgets. // TODO: + Need a way to map Rect to Component::id() to position each widget? -// TODO: Need a way to dynamically run Component methods on all widgets. -pub enum AppComponents<'a> { - AppEditor(Editor), - AppExplorer(Explorer<'a>), - AppLogger(Logger), - #[allow(dead_code)] - AppComponent(Box), -} - -/// Usage: get_component_mut::() OR get_component::() -/// -/// Implementing this trait for each AppComponent allows for easy lookup in the vector. -pub(crate) trait ComponentOf { - fn as_ref(&self) -> Option<&T>; - fn as_mut(&mut self) -> Option<&mut T>; +// TODO: Need a good way to dynamically run Component methods on all widgets. +#[derive(PartialEq)] +pub enum AppComponent { + AppEditor, + AppExplorer, + AppLogger, } pub struct App<'a> { - components: Vec>, + editor: Editor, + explorer: Explorer<'a>, + logger: Logger, + last_active: AppComponent, } impl<'a> App<'a> { + pub fn id() -> &'static str { + "App" + } + pub fn new(root_path: PathBuf) -> Result { - let mut app = Self { - components: vec![ - AppComponents::AppExplorer(Explorer::new(&root_path)?), - AppComponents::AppEditor(Editor::new()), - AppComponents::AppLogger(Logger::new()), - ], + let app = Self { + editor: Editor::new(), + explorer: Explorer::new(&root_path)?, + logger: Logger::new(), + last_active: AppEditor, }; - let editor = app.get_component_mut::().unwrap(); - editor + Ok(app) + } + + /// Logic that should be executed once on application startup. + pub fn start(&mut self) -> Result<()> { + let root_path = self.explorer.root_path.clone(); + self.editor .set_contents(&root_path.join("src/tui/app.rs")) .context(format!( "Failed to initialize editor contents to path: {root_path:?}" ))?; - editor.component_state.set_focus(Focus::Active); - Ok(app) - } - - fn get_component(&self) -> Option<&T> - where - AppComponents<'a>: ComponentOf, - { - self.components.iter().find_map(|c| c.as_ref()) - } - - fn get_component_mut(&mut self) -> Option<&mut T> - where - AppComponents<'a>: ComponentOf, - { - self.components.iter_mut().find_map(|c| c.as_mut()) + self.editor.component_state.set_focus(Focus::Active); + Ok(()) } pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + self.start()?; loop { self.refresh_editor_contents() .context("Failed to refresh editor contents.")?; @@ -80,7 +69,6 @@ impl<'a> App<'a> { f.render_widget(&mut self, f.area()); })?; - // TODO: Handle events based on which component is active. if event::poll(Duration::from_millis(250)).context("event poll failed")? { match self.handle_event(event::read()?)? { Action::Quit => break, @@ -103,32 +91,29 @@ impl<'a> App<'a> { } fn draw_bottom_status(&self, area: Rect, buf: &mut Buffer) { - // TODO: Set help text based on most recent component enabled. - Paragraph::new( - self.get_component::() - .unwrap() - .component_state - .help_text - .clone(), - ) - .style(Color::Gray) - .wrap(Wrap { trim: false }) - .centered() - .render(area, buf); + // Determine help text from the most recently focused component. + let help = match self.last_active { + AppEditor => self.editor.component_state.help_text.clone(), + AppExplorer => self.explorer.component_state.help_text.clone(), + AppLogger => self.logger.component_state.help_text.clone(), + }; + Paragraph::new(help) + .style(Color::Gray) + .wrap(Wrap { trim: false }) + .centered() + .render(area, buf); } fn draw_tabs(&self, area: Rect, buf: &mut Buffer) { // Determine the tab title from the current file (or use a fallback). - let mut title: Option<&str> = None; - if let Some(editor) = self.get_component::() { - title = editor - .file_path - .as_ref() - .and_then(|p| p.file_name()) - .and_then(|s| s.to_str()) - } - - Tabs::new(vec![title.unwrap_or("Unknown")]) + if let Some(title) = self.editor.file_path.clone() { + Tabs::new(vec![ + title + .file_name() + .map(|f| f.to_str()) + .unwrap_or(Some("Unknown")) + .unwrap(), + ]) .divider(symbols::DOT) .block( Block::default() @@ -137,30 +122,44 @@ impl<'a> App<'a> { ) .highlight_style(Style::default().fg(Color::LightRed)) .render(area, buf); + } else { + error!(target:Self::id(), "Failed to get Editor file_path while drawing Tabs widget."); + } + } + + fn change_focus(&mut self, focus: AppComponent) { + if self.last_active == AppEditor { + self.editor.state.cursor.row = 0; + self.editor.state.cursor.col = 0; + } + match focus { + AppEditor => self.editor.component_state.set_focus(Focus::Active), + AppExplorer => self.explorer.component_state.set_focus(Focus::Active), + AppLogger => self.logger.component_state.set_focus(Focus::Active), + } + self.last_active = focus; } /// 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) -> Result<()> { // Use the currently selected TreeItem or get an absolute path to this source file. - let selected_pathbuf = match self.get_component::().unwrap().selected() { + let selected_pathbuf = match self.explorer.selected() { Ok(path) => PathBuf::from(path), Err(_) => PathBuf::from(std::path::absolute(file!())?.to_string_lossy().to_string()), }; - let editor = self - .get_component_mut::() - .context("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); + let current_file_path = self + .editor + .file_path + .clone() + .context("Failed to get Editor current file_path")?; + if selected_pathbuf == current_file_path || !selected_pathbuf.is_file() { + return Ok(()); } - bail!("Failed to refresh editor contents") + self.editor.set_contents(&selected_pathbuf) } } -// TODO: Separate complex components into their own widgets. impl<'a> Widget for &mut App<'a> { fn render(self, area: Rect, buf: &mut Buffer) where @@ -195,35 +194,17 @@ impl<'a> Widget for &mut App<'a> { self.draw_top_status(vertical[0], buf); self.draw_bottom_status(vertical[3], buf); self.draw_tabs(editor_layout[0], buf); - let id = self.id().to_string(); - for component in &mut self.components { - match component { - AppComponents::AppEditor(editor) => editor.render(editor_layout[1], buf), - AppComponents::AppExplorer(explorer) => { - explorer - .render(horizontal[0], buf) - .context("Failed to render Explorer") - .unwrap_or_else(|e| error!(target:id.as_str(), "{}", e)); - } - AppComponents::AppLogger(logger) => logger.render(vertical[2], buf), - AppComponents::AppComponent(_) => {} - } - } + let id = App::id().to_string(); + self.editor.render(editor_layout[1], buf); + self.explorer + .render(horizontal[0], buf) + .context("Failed to render Explorer") + .unwrap_or_else(|e| error!(target:id.as_str(), "{}", e)); + self.logger.render(vertical[2], buf); } } impl<'a> Component for App<'a> { - fn id(&self) -> &str { - "App" - } - - /// TODO: Get active widget with some Component trait function helper? - /// trait Component { fn get_state() -> ComponentState; } - /// if component.get_state() = ComponentState::Active { component.handle_event(); } - /// - /// 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) -> Result { // Handle events in the primary application. @@ -238,25 +219,21 @@ impl<'a> Component for App<'a> { } // Handle events for all components. - for component in &mut self.components { - let c = match component { - AppComponents::AppEditor(e) => e as &mut dyn Component, - AppComponents::AppExplorer(e) => e as &mut dyn Component, - AppComponents::AppLogger(e) => e as &mut dyn Component, - AppComponents::AppComponent(e) => e.as_mut(), - }; - if !c.is_active() { - if let Some(mouse) = event.as_mouse_event() { - // Always handle mouse events for click interaction. - c.handle_mouse_events(mouse)?; - } - continue; - } - let action = c.handle_event(event.clone())?; - match action { - Action::Quit | Action::Handled => return Ok(action), - _ => {} - } + let action = match self.last_active { + AppEditor => self.editor.handle_event(event)?, + AppExplorer => self.explorer.handle_event(event)?, + AppLogger => self.logger.handle_event(event)?, + }; + // if !c.is_active() { + // if let Some(mouse) = event.as_mouse_event() { + // // Always handle mouse events for click interaction. + // c.handle_mouse_events(mouse)?; + // } + // continue; + // } + match action { + Action::Quit | Action::Handled => return Ok(action), + _ => {} } Ok(Action::Noop) } @@ -270,10 +247,7 @@ impl<'a> Component for App<'a> { kind: KeyEventKind::Press, state: _state, } => { - self.get_component_mut::() - .unwrap() - .component_state - .toggle_focus(); + self.change_focus(AppExplorer); Ok(Action::Handled) } KeyEvent { @@ -282,10 +256,7 @@ impl<'a> Component for App<'a> { kind: KeyEventKind::Press, state: _state, } => { - self.get_component_mut::() - .unwrap() - .component_state - .toggle_focus(); + self.change_focus(AppEditor); Ok(Action::Handled) } KeyEvent { @@ -294,10 +265,7 @@ impl<'a> Component for App<'a> { kind: KeyEventKind::Press, state: _state, } => { - self.get_component_mut::() - .unwrap() - .component_state - .toggle_focus(); + self.change_focus(AppLogger); Ok(Action::Handled) } KeyEvent { @@ -306,11 +274,11 @@ impl<'a> Component for App<'a> { kind: KeyEventKind::Press, state: _state, } => { - error!(target:self.id(), "an error"); - warn!(target:self.id(), "a warning"); - info!(target:self.id(), "a two line info\nsecond line"); - debug!(target:self.id(), "a debug"); - trace!(target:self.id(), "a trace"); + error!(target:App::id(), "an error"); + warn!(target:App::id(), "a warning"); + info!(target:App::id(), "a two line info\nsecond line"); + debug!(target:App::id(), "a debug"); + trace!(target:App::id(), "a trace"); Ok(Action::Handled) } KeyEvent { diff --git a/src/tui/component.rs b/src/tui/component.rs index e06b86b..0a98b84 100644 --- a/src/tui/component.rs +++ b/src/tui/component.rs @@ -22,10 +22,6 @@ pub enum Action { } pub trait Component { - /// Returns a unique identifier for the component. - /// This is used for lookup in a container of Components. - fn id(&self) -> &str; - fn handle_event(&mut self, event: Event) -> Result { match event { Event::Key(key_event) => self.handle_key_events(key_event), diff --git a/src/tui/editor.rs b/src/tui/editor.rs index 282cd1e..c5c6b11 100644 --- a/src/tui/editor.rs +++ b/src/tui/editor.rs @@ -1,6 +1,4 @@ use crate::tui::component::{Action, Component, ComponentState, Focus}; - -use crate::tui::app::{AppComponents, ComponentOf}; use anyhow::{Context, Result, bail}; use edtui::{ EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter, @@ -24,22 +22,11 @@ pub struct Editor { pub(crate) component_state: ComponentState, } -impl<'a> ComponentOf for AppComponents<'a> { - fn as_ref(&self) -> Option<&Editor> { - if let AppComponents::AppEditor(ref e) = *self { - return Some(e); - } - None - } - fn as_mut(&mut self) -> Option<&mut Editor> { - if let AppComponents::AppEditor(ref mut e) = *self { - return Some(e); - } - None - } -} - impl Editor { + pub fn id() -> &'static str { + "Editor" + } + pub fn new() -> Self { Editor { state: EditorState::default(), @@ -104,14 +91,6 @@ impl Widget for &mut Editor { } impl Component for Editor { - fn id(&self) -> &str { - "Editor" - } - - fn is_active(&self) -> bool { - self.component_state.focus == Focus::Active - } - fn handle_event(&mut self, event: Event) -> Result { if let Some(key_event) = event.as_key_event() { // Handle events here that should not be passed on to the vim emulation handler. @@ -140,4 +119,8 @@ impl Component for Editor { _ => Ok(Action::Noop), } } + + fn is_active(&self) -> bool { + self.component_state.focus == Focus::Active + } } diff --git a/src/tui/explorer.rs b/src/tui/explorer.rs index d914da5..9de4895 100644 --- a/src/tui/explorer.rs +++ b/src/tui/explorer.rs @@ -1,4 +1,3 @@ -use crate::tui::app::{AppComponents, ComponentOf}; use crate::tui::component::{Action, Component, ComponentState, Focus}; use anyhow::{Context, Result, bail}; use ratatui::buffer::Buffer; @@ -12,28 +11,17 @@ use tui_tree_widget::{Tree, TreeItem, TreeState}; #[derive(Debug)] pub struct Explorer<'a> { - root_path: std::path::PathBuf, + pub(crate) root_path: std::path::PathBuf, tree_items: TreeItem<'a, String>, tree_state: TreeState, pub(crate) component_state: ComponentState, } -impl<'a> ComponentOf> for AppComponents<'a> { - fn as_ref(&self) -> Option<&Explorer<'a>> { - if let AppComponents::AppExplorer(ref e) = *self { - return Some(e); - } - None - } - fn as_mut(&mut self) -> Option<&mut Explorer<'a>> { - if let AppComponents::AppExplorer(ref mut e) = *self { - return Some(e); - } - None - } -} - impl<'a> Explorer<'a> { + pub fn id() -> &'static str { + "Explorer" + } + pub fn new(path: &std::path::PathBuf) -> Result { let explorer = Explorer { root_path: path.to_owned(), @@ -123,21 +111,16 @@ impl<'a> Explorer<'a> { 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()); + return Ok(std::path::absolute(path)? + .to_str() + .context("Failed to get absolute path to selected TreeItem")? + .to_string()); } bail!("Failed to get selected TreeItem") } } impl<'a> Component for Explorer<'a> { - fn id(&self) -> &str { - "Explorer" - } - - fn is_active(&self) -> bool { - self.component_state.focus == Focus::Active - } - fn handle_event(&mut self, event: Event) -> Result { if let Some(key_event) = event.as_key_event() { // Handle events here that should not be passed on to the vim emulation handler. @@ -188,4 +171,8 @@ impl<'a> Component for Explorer<'a> { } Ok(Action::Noop) } + + fn is_active(&self) -> bool { + self.component_state.focus == Focus::Active + } } diff --git a/src/tui/logger.rs b/src/tui/logger.rs index 3c859b0..d3f37e9 100644 --- a/src/tui/logger.rs +++ b/src/tui/logger.rs @@ -1,4 +1,3 @@ -use crate::tui::app::{AppComponents, ComponentOf}; use crate::tui::component::{Action, Component, ComponentState, Focus}; use ratatui::buffer::Buffer; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent}; @@ -14,27 +13,16 @@ pub struct Logger { pub(crate) component_state: ComponentState, } -impl<'a> ComponentOf for AppComponents<'a> { - fn as_ref(&self) -> Option<&Logger> { - if let AppComponents::AppLogger(ref e) = *self { - return Some(e); - } - None - } - fn as_mut(&mut self) -> Option<&mut Logger> { - if let AppComponents::AppLogger(ref mut e) = *self { - return Some(e); - } - None - } -} - impl Logger { + pub fn id() -> &'static str { + "Logger" + } + pub fn new() -> Self { Self { state: TuiWidgetState::new(), component_state: ComponentState::default().with_help_text(concat!( - "Q: Quit | Tab: Switch state | ↑/↓: Select target | f: Focus target", + "Q: Quit | ↑/↓: Select target | f: Focus target", " | ←/→: Display level | +/-: Filter level | Space: Toggle hidden targets", " | h: Hide target selector | PageUp/Down: Scroll | Esc: Cancel scroll" )), @@ -47,7 +35,6 @@ impl Widget for &Logger { where Self: Sized, { - // TODO: Use output_file? TuiLoggerSmartWidget::default() .style_error(Style::default().fg(Color::Red)) .style_debug(Style::default().fg(Color::Green)) @@ -66,14 +53,6 @@ impl Widget for &Logger { } impl Component for Logger { - fn id(&self) -> &str { - "Logger" - } - - fn is_active(&self) -> bool { - self.component_state.focus == Focus::Active - } - fn handle_event(&mut self, event: Event) -> anyhow::Result { if let Some(key_event) = event.as_key_event() { return self.handle_key_events(key_event); @@ -99,4 +78,8 @@ impl Component for Logger { } Ok(Action::Pass) } + + fn is_active(&self) -> bool { + self.component_state.focus == Focus::Active + } }