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}; 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::widgets::{Block, Borders, Padding, Paragraph, Tabs, Wrap}; use ratatui::{DefaultTerminal, symbols}; use std::path::PathBuf; 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 good way to dynamically run Component methods on all widgets. #[derive(PartialEq)] pub enum AppComponent { AppEditor, AppExplorer, AppLogger, } pub struct App<'a> { 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 app = Self { editor: Editor::new(), explorer: Explorer::new(&root_path)?, logger: Logger::new(), last_active: AppEditor, }; 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:?}" ))?; 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.")?; terminal.draw(|f| { f.render_widget(&mut self, f.area()); })?; if event::poll(Duration::from_millis(250)).context("event poll failed")? { match self.handle_event(event::read()?)? { Action::Quit => break, Action::Handled => {} _ => { // bail!("Unhandled event: {:?}", event); } } } } Ok(()) } fn draw_top_status(&self, area: Rect, buf: &mut Buffer) { // TODO: Status bar should have drop down menus Tabs::new(["File", "Edit", "View", "Help"]) .style(Style::default()) .block(Block::default().borders(Borders::ALL)) .render(area, buf); } fn draw_bottom_status(&self, area: Rect, buf: &mut Buffer) { // 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). 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() .borders(Borders::NONE) .padding(Padding::new(0, 0, 0, 0)), ) .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.explorer.selected() { Ok(path) => PathBuf::from(path), Err(_) => PathBuf::from(std::path::absolute(file!())?.to_string_lossy().to_string()), }; 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(()); } self.editor.set_contents(&selected_pathbuf) } } impl<'a> Widget for &mut App<'a> { fn render(self, area: Rect, buf: &mut Buffer) where Self: Sized, { let vertical = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), // top status bar Constraint::Percentage(70), // horizontal layout Constraint::Percentage(30), // terminal Constraint::Length(3), // bottom status bar ]) .split(area); let horizontal = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Max(30), // File explorer with a max width of 30 characters. Constraint::Fill(1), // Editor fills the remaining space. ]) .split(vertical[1]); let editor_layout = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(1), // Editor tabs. Constraint::Fill(1), // Editor contents. ]) .split(horizontal[1]); self.draw_top_status(vertical[0], buf); self.draw_bottom_status(vertical[3], buf); self.draw_tabs(editor_layout[0], buf); 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> { /// Handles events for the App and delegates to attached Components. fn handle_event(&mut self, event: Event) -> Result { // Handle events in the primary application. if let Some(key_event) = event.as_key_event() { let res = self .handle_key_events(key_event) .context("Failed to handle key events for primary App Component."); match res { Ok(Action::Quit) | Ok(Action::Handled) => return res, _ => {} } } // Handle events for all components. 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) } /// Handles key events for the App Component only. fn handle_key_events(&mut self, key: KeyEvent) -> Result { match key { KeyEvent { code: KeyCode::Char('q'), modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, state: _state, } => { self.change_focus(AppExplorer); Ok(Action::Handled) } KeyEvent { code: KeyCode::Char('w'), modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, state: _state, } => { self.change_focus(AppEditor); Ok(Action::Handled) } KeyEvent { code: KeyCode::Char('e'), modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, state: _state, } => { self.change_focus(AppLogger); Ok(Action::Handled) } KeyEvent { code: KeyCode::Char('l'), modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, state: _state, } => { 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 { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: _state, } => Ok(Action::Quit), _ => Ok(Action::Noop), } } }