diff --git a/src/tui/app.rs b/src/tui/app.rs index 47bd6b5..ec70957 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -17,10 +17,14 @@ pub struct App<'a> { impl<'a> App<'a> { pub(crate) fn new(root_path: std::path::PathBuf) -> Self { - Self { - explorer: Explorer::new(root_path), + let mut app = Self { + explorer: Explorer::new(&root_path), editor: Editor::new(), - } + }; + app.editor + .set_contents(&root_path.join("src/tui/app.rs")) + .expect("Failed to set editor contents."); + app } fn get_event(&mut self) -> Option { @@ -33,19 +37,18 @@ impl<'a> App<'a> { pub fn run(mut self, mut terminal: DefaultTerminal) -> anyhow::Result<()> { loop { + // TODO: Handle events based on which component is active. terminal.draw(|f| { f.render_widget(&mut self, f.area()); })?; - // TODO: Handle events based on which component is active. if let Some(event) = self.get_event() { - self.editor - .event_handler - .on_event(event.clone(), &mut self.editor.state); match self.handle_event(event) { - Action::Noop => {} Action::Quit => break, - Action::Pass => {} + Action::Handled => {} + _ => { + // panic!("Unhandled event: {:?}", event); + } } } } @@ -131,6 +134,43 @@ impl<'a> Widget for &mut App<'a> { } 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..) + 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; + } + _ => {} + } + } + self.editor.handle_event(event); + + // 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, + // } + // } + Action::Noop + } + fn handle_key_events(&mut self, key: KeyEvent) -> Action { match key { KeyEvent { diff --git a/src/tui/component.rs b/src/tui/component.rs index 1ba4d60..4af5b70 100644 --- a/src/tui/component.rs +++ b/src/tui/component.rs @@ -3,12 +3,28 @@ use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent}; pub enum Action { - Noop, + /// Exit the application. Quit, - Pass, // Pass input to another component. + + /// The input was checked by the Component and had no effect. + Noop, + + /// Pass input to another component or external handler. + /// Similar to Noop with the added context that externally handled input may have had an impact. + Pass, + + /// Save the current file. + Save, + + /// The input was handled by a Component and should not be passed to the next component. + Handled, } 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) -> Action { 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 dfa88f2..580043a 100644 --- a/src/tui/editor.rs +++ b/src/tui/editor.rs @@ -1,9 +1,11 @@ use crate::tui::component::{Action, Component}; + +use anyhow::Result; use edtui::{ - EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, SyntaxHighlighter, + EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter, }; use ratatui::buffer::Buffer; -use ratatui::crossterm::event::KeyEvent; +use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use ratatui::layout::{Alignment, Rect}; use ratatui::prelude::{Color, Style}; use ratatui::widgets::{Block, Borders, Padding, Widget}; @@ -15,6 +17,7 @@ use ratatui::widgets::{Block, Borders, Padding, Widget}; pub struct Editor { pub state: EditorState, pub event_handler: EditorEventHandler, + file_path: Option, } impl Editor { @@ -22,15 +25,32 @@ impl Editor { Editor { state: EditorState::default(), event_handler: EditorEventHandler::default(), + file_path: None, } } + + pub fn set_contents(&mut self, path: &std::path::PathBuf) -> Result<()> { + let contents = std::fs::read_to_string(path) + .expect(&format!("Failed to read file contents: {}", path.display())); + let lines: Vec<_> = contents + .lines() + .map(|line| line.chars().collect::>()) + .collect(); + self.file_path = Some(path.clone()); + self.state.lines = Lines::new(lines); + Ok(()) + } + + pub fn save(&self) -> Result<()> { + if let Some(path) = &self.file_path { + return std::fs::write(path, self.state.lines.to_string()).map_err(|e| e.into()); + }; + Err(anyhow::anyhow!("File not saved. No file path set.")) + } } impl Widget for &mut Editor { - fn render(self, area: Rect, buf: &mut Buffer) - where - Self: Sized, - { + fn render(self, area: Rect, buf: &mut Buffer) { // TODO: Use current file extension for syntax highlighting here. EditorView::new(&mut self.state) .wrap(true) @@ -52,8 +72,36 @@ impl Widget for &mut Editor { } impl Component for Editor { - fn handle_key_events(&mut self, key: KeyEvent) -> Action { - self.event_handler.on_key_event(key, &mut self.state); + fn id(&self) -> &str { + "editor" + } + + fn handle_event(&mut self, event: Event) -> Action { + if let Some(key_event) = event.as_key_event() { + // Handle events here that should not be passed on to the vim emulation handler. + match self.handle_key_events(key_event) { + Action::Handled => return Action::Handled, + _ => {} + } + } + self.event_handler.on_event(event, &mut self.state); Action::Pass } + + /// The events for the vim emulation should be handled by EditorEventHandler::on_event. + /// These events are custom to the clide application. + fn handle_key_events(&mut self, key: KeyEvent) -> Action { + match key { + KeyEvent { + code: KeyCode::Char('s'), + modifiers: KeyModifiers::CONTROL, + .. + } => { + self.save().expect("Failed to save file."); + Action::Handled + } + // For other events not handled here, pass to the vim emulation handler. + _ => Action::Noop, + } + } } diff --git a/src/tui/explorer.rs b/src/tui/explorer.rs index e934cee..a8aec73 100644 --- a/src/tui/explorer.rs +++ b/src/tui/explorer.rs @@ -16,10 +16,10 @@ pub struct Explorer<'a> { } impl<'a> Explorer<'a> { - pub fn new(path: std::path::PathBuf) -> Self { + pub fn new(path: &std::path::PathBuf) -> Self { let explorer = Explorer { root_path: path.to_owned(), - tree_items: Self::build_tree_from_path(path), + tree_items: Self::build_tree_from_path(path.to_owned()), }; explorer } @@ -80,4 +80,8 @@ impl<'a> Widget for &Explorer<'a> { } } -impl<'a> Component for Explorer<'a> {} +impl<'a> Component for Explorer<'a> { + fn id(&self) -> &str { + "explorer" + } +}