[tui] Add AppComponent enum for storing all components.

This commit is contained in:
Shaun Reed 2026-01-20 16:03:38 -05:00
parent 3ffdcc2865
commit ce2949159c
3 changed files with 108 additions and 52 deletions

View File

@ -1,6 +1,7 @@
use crate::tui::component::{Action, Component}; use crate::tui::component::{Action, Component};
use crate::tui::editor::Editor; use crate::tui::editor::Editor;
use crate::tui::explorer::Explorer; use crate::tui::explorer::Explorer;
use anyhow::{Result, anyhow};
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event; use ratatui::crossterm::event;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
@ -11,23 +12,71 @@ use ratatui::{DefaultTerminal, symbols};
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
pub enum AppComponents<'a> {
AppEditor(Editor),
AppExplorer(Explorer<'a>),
AppComponent(Box<dyn Component>),
}
pub struct App<'a> { pub struct App<'a> {
explorer: Explorer<'a>, components: Vec<AppComponents<'a>>,
editor: Editor,
} }
impl<'a> App<'a> { impl<'a> App<'a> {
pub(crate) fn new(root_path: PathBuf) -> Self { pub(crate) fn new(root_path: PathBuf) -> Self {
let mut app = Self { let mut app = Self {
explorer: Explorer::new(&root_path), components: vec![
editor: Editor::new(), 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")) .set_contents(&root_path.join("src/tui/app.rs"))
.expect("Failed to set editor contents."); .expect("Failed to set editor contents.");
app 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<Event> { fn get_event(&mut self) -> Option<Event> {
if !event::poll(Duration::from_millis(250)).expect("event poll failed") { if !event::poll(Duration::from_millis(250)).expect("event poll failed") {
return None; return None;
@ -36,7 +85,7 @@ impl<'a> App<'a> {
event::read().ok() event::read().ok()
} }
pub fn run(mut self, mut terminal: DefaultTerminal) -> anyhow::Result<()> { pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
loop { loop {
// TODO: Handle events based on which component is active. // TODO: Handle events based on which component is active.
terminal.draw(|f| { terminal.draw(|f| {
@ -66,15 +115,16 @@ impl<'a> App<'a> {
fn draw_tabs(&self, area: Rect, buf: &mut Buffer) { fn draw_tabs(&self, area: Rect, buf: &mut Buffer) {
// Determine the tab title from the current file (or use a fallback). // Determine the tab title from the current file (or use a fallback).
let title = self let mut title: Option<&str> = None;
.editor if let Some(editor) = self.get_editor() {
title = editor
.file_path .file_path
.as_ref() .as_ref()
.and_then(|p| p.file_name()) .and_then(|p| p.file_name())
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
.unwrap_or("Untitled"); }
Tabs::new(vec![title]) Tabs::new(vec![title.unwrap_or("Unknown")])
.divider(symbols::DOT) .divider(symbols::DOT)
.block( .block(
Block::default() 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. /// 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. /// If the selected item is not a file, this does nothing.
fn refresh_editor_contents(&mut self) { fn refresh_editor_contents(&mut self) -> Result<()> {
if let Some(current_file_path) = self.editor.file_path.clone() { // Use the currently selected TreeItem or get an absolute path to this source file.
if let Some(selected_path_string) = self.explorer.selected() { let selected_pathbuf = match self.get_explorer()?.selected() {
let selected_pathbuf = PathBuf::from(selected_path_string); Ok(path) => PathBuf::from(path),
if std::path::absolute(&selected_pathbuf).unwrap().is_file() Err(_) => PathBuf::from(std::path::absolute(file!())?.to_string_lossy().to_string()),
&& selected_pathbuf != current_file_path };
{ let editor = self
self.editor.set_contents(&selected_pathbuf.into()).ok(); .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_status(vertical[0], buf);
self.draw_terminal(vertical[2], buf); self.draw_terminal(vertical[2], buf);
if let Ok(explorer) = self.get_explorer_mut() {
self.explorer.render(horizontal[0], buf); explorer.render(horizontal[0], buf);
}
self.draw_tabs(editor_layout[0], buf); self.draw_tabs(editor_layout[0], buf);
self.refresh_editor_contents(); self.refresh_editor_contents()
self.editor.render(editor_layout[1], buf); .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.. /// App could then provide helpers for altering Component state based on TUI grouping..
/// (such as editor tabs, file explorer, status bars, etc..) /// (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 { fn handle_event(&mut self, event: Event) -> Action {
// Handle events in the primary application. // Handle events in the primary application.
if let Some(key_event) = event.as_key_event() { if let Some(key_event) = event.as_key_event() {
match self.handle_key_events(key_event) { match self.handle_key_events(key_event) {
Action::Quit => return Action::Quit, Action::Quit => return Action::Quit,
Action::Handled => { Action::Handled => return Action::Handled,
// dbg!(format!("Handled event: {:?}", self.id()));
return Action::Handled;
}
_ => {} _ => {}
} }
} }
self.explorer.handle_event(event.clone());
self.editor.handle_event(event.clone());
// Handle events for all components. // Handle events for all components.
// for component in &mut self.components { for component in &mut self.components {
// dbg!(format!("Handling event: {:?}", component.id())); let action = match component {
// // Actions returned here abort the input handling iteration. AppComponents::AppEditor(editor) => editor.handle_event(event.clone()),
// match component.handle_event(event.clone()) { AppComponents::AppExplorer(explorer) => explorer.handle_event(event.clone()),
// Action::Quit => return Action::Quit, AppComponents::AppComponent(comp) => comp.handle_event(event.clone()),
// Action::Handled => return Action::Handled, };
// _ => continue, // Actions returned here abort the input handling iteration.
// } match action {
// } Action::Quit | Action::Handled => return action,
_ => {}
}
}
Action::Noop Action::Noop
} }
/// Handles key events for the App Component only.
fn handle_key_events(&mut self, key: KeyEvent) -> Action { fn handle_key_events(&mut self, key: KeyEvent) -> Action {
match key { match key {
KeyEvent { KeyEvent {
@ -205,10 +262,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => Action::Quit, } => Action::Quit,
key_event => { _ => Action::Noop,
// Pass the key event to each component that can handle it.
self.explorer.handle_key_events(key_event)
}
} }
} }
} }

View File

@ -1,6 +1,6 @@
use crate::tui::component::{Action, Component}; use crate::tui::component::{Action, Component};
use anyhow::Result; use anyhow::{Context, Result};
use edtui::{ use edtui::{
EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter, EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter,
}; };
@ -40,9 +40,8 @@ impl Editor {
.collect(); .collect();
self.file_path = Some(path.clone()); self.file_path = Some(path.clone());
self.state.lines = Lines::new(lines); self.state.lines = Lines::new(lines);
return Ok(());
} }
Err(anyhow::Error::msg("Failed to set editor file contents")) Ok(())
} }
pub fn save(&self) -> Result<()> { pub fn save(&self) -> Result<()> {

View File

@ -102,8 +102,11 @@ impl<'a> Explorer<'a> {
) )
} }
pub fn selected(&self) -> Option<&String> { pub fn selected(&self) -> Result<String> {
self.tree_state.selected().last() 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"))
} }
} }