TUI #1

Merged
shaunrd0 merged 73 commits from ui into master 2026-01-25 20:57:37 +00:00
7 changed files with 143 additions and 30 deletions
Showing only changes of commit 0c87fda795 - Show all commits

29
Cargo.lock generated
View File

@ -299,6 +299,7 @@ dependencies = [
"edtui", "edtui",
"log", "log",
"ratatui", "ratatui",
"strum",
"syntect", "syntect",
"tui-logger", "tui-logger",
"tui-tree-widget", "tui-tree-widget",
@ -451,9 +452,9 @@ dependencies = [
[[package]] [[package]]
name = "cxx" name = "cxx"
version = "1.0.192" version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbda285ba6e5866529faf76352bdf73801d9b44a6308d7cd58ca2379f378e994" checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e"
dependencies = [ dependencies = [
"cc", "cc",
"cxx-build", "cxx-build",
@ -466,9 +467,9 @@ dependencies = [
[[package]] [[package]]
name = "cxx-build" name = "cxx-build"
version = "1.0.192" version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9efde466c5d532d57efd92f861da3bdb7f61e369128ce8b4c3fe0c9de4fa4d" checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e"
dependencies = [ dependencies = [
"cc", "cc",
"codespan-reporting 0.13.1", "codespan-reporting 0.13.1",
@ -481,9 +482,9 @@ dependencies = [
[[package]] [[package]]
name = "cxx-gen" name = "cxx-gen"
version = "0.7.192" version = "0.7.194"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee08d1131e8f050a1d1acbb7c699e5c8d29c325dffc382331c280d99f98c2618" checksum = "035b6c61a944483e8a4b2ad4fb8b13830d63491bd004943716ad16d85dcc64bc"
dependencies = [ dependencies = [
"codespan-reporting 0.13.1", "codespan-reporting 0.13.1",
"indexmap", "indexmap",
@ -564,9 +565,9 @@ dependencies = [
[[package]] [[package]]
name = "cxxbridge-cmd" name = "cxxbridge-cmd"
version = "1.0.192" version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3efb93799095bccd4f763ca07997dc39a69e5e61ab52d2c407d4988d21ce144d" checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328"
dependencies = [ dependencies = [
"clap", "clap",
"codespan-reporting 0.13.1", "codespan-reporting 0.13.1",
@ -578,15 +579,15 @@ dependencies = [
[[package]] [[package]]
name = "cxxbridge-flags" name = "cxxbridge-flags"
version = "1.0.192" version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3092010228026e143b32a4463ed9fa8f86dca266af4bf5f3b2a26e113dbe4e45" checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a"
[[package]] [[package]]
name = "cxxbridge-macro" name = "cxxbridge-macro"
version = "1.0.192" version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31d72ebfcd351ae404fb00ff378dfc9571827a00722c9e735c9181aec320ba0a" checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"proc-macro2", "proc-macro2",
@ -1599,9 +1600,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.105" version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]

View File

@ -16,6 +16,7 @@ anyhow = "1.0.100"
tui-tree-widget = "0.24.0" tui-tree-widget = "0.24.0"
tui-logger = "0.18.1" tui-logger = "0.18.1"
edtui = "0.11.1" edtui = "0.11.1"
strum = "0.27.2"
[build-dependencies] [build-dependencies]
# The link_qt_object_files feature is required for statically linking Qt 6. # The link_qt_object_files feature is required for statically linking Qt 6.

View File

@ -1,4 +1,4 @@
use crate::tui::component::{Action, Component}; use crate::tui::component::{Action, Component, Focus, FocusState};
use crate::tui::editor::Editor; use crate::tui::editor::Editor;
use crate::tui::explorer::Explorer; use crate::tui::explorer::Explorer;
use crate::tui::logger::Logger; use crate::tui::logger::Logger;
@ -21,6 +21,7 @@ pub enum AppComponents<'a> {
AppEditor(Editor), AppEditor(Editor),
AppExplorer(Explorer<'a>), AppExplorer(Explorer<'a>),
AppLogger(Logger), AppLogger(Logger),
#[allow(dead_code)]
AppComponent(Box<dyn Component>), AppComponent(Box<dyn Component>),
} }
@ -45,12 +46,13 @@ impl<'a> App<'a> {
AppComponents::AppLogger(Logger::new()), AppComponents::AppLogger(Logger::new()),
], ],
}; };
app.get_component_mut::<Editor>() let editor = app.get_component_mut::<Editor>().unwrap();
.unwrap() editor
.set_contents(&root_path.join("src/tui/app.rs")) .set_contents(&root_path.join("src/tui/app.rs"))
.context(format!( .context(format!(
"Failed to initialize editor contents to path: {root_path:?}" "Failed to initialize editor contents to path: {root_path:?}"
))?; ))?;
editor.component_state.set_focus(Focus::Active);
Ok(app) Ok(app)
} }
@ -219,13 +221,20 @@ impl<'a> Component for App<'a> {
// Handle events for all components. // Handle events for all components.
for component in &mut self.components { for component in &mut self.components {
let action = match component { let c = match component {
AppComponents::AppEditor(editor) => editor.handle_event(event.clone())?, AppComponents::AppEditor(e) => e as &mut dyn Component,
AppComponents::AppExplorer(explorer) => explorer.handle_event(event.clone())?, AppComponents::AppExplorer(e) => e as &mut dyn Component,
AppComponents::AppComponent(comp) => comp.handle_event(event.clone())?, AppComponents::AppLogger(e) => e as &mut dyn Component,
AppComponents::AppLogger(logger) => logger.handle_event(event.clone())?, AppComponents::AppComponent(e) => e.as_mut() as &mut dyn Component,
}; };
// Actions returned here abort the input handling iteration. 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 { match action {
Action::Quit | Action::Handled => return Ok(action), Action::Quit | Action::Handled => return Ok(action),
_ => {} _ => {}
@ -238,18 +247,53 @@ impl<'a> Component for App<'a> {
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> { fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> {
match key { match key {
KeyEvent { KeyEvent {
code: KeyCode::Char('l'), code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::ALT,
kind: KeyEventKind::Press,
state: _state,
} => {
self.get_component_mut::<Explorer>()
.unwrap()
.component_state
.toggle_focus();
Ok(Action::Handled)
}
KeyEvent {
code: KeyCode::Char('w'),
modifiers: KeyModifiers::ALT,
kind: KeyEventKind::Press,
state: _state,
} => {
self.get_component_mut::<Editor>()
.unwrap()
.component_state
.toggle_focus();
Ok(Action::Handled)
}
KeyEvent {
code: KeyCode::Char('e'),
modifiers: KeyModifiers::ALT,
kind: KeyEventKind::Press,
state: _state,
} => {
self.get_component_mut::<Logger>()
.unwrap()
.component_state
.toggle_focus();
Ok(Action::Handled)
}
KeyEvent {
code: KeyCode::Char('l'),
modifiers: KeyModifiers::ALT,
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => { } => {
// Some example logs for testing.
error!(target:self.id(), "an error"); error!(target:self.id(), "an error");
warn!(target:self.id(), "a warning"); warn!(target:self.id(), "a warning");
info!(target:self.id(), "a two line info\nsecond line"); info!(target:self.id(), "a two line info\nsecond line");
debug!(target:self.id(), "a debug"); debug!(target:self.id(), "a debug");
trace!(target:self.id(), "a trace"); trace!(target:self.id(), "a trace");
Ok(Action::Noop) Ok(Action::Handled)
} }
KeyEvent { KeyEvent {
code: KeyCode::Char('c'), code: KeyCode::Char('c'),

View File

@ -44,4 +44,52 @@ pub trait Component {
fn update(&mut self, action: Action) -> Result<Action> { fn update(&mut self, action: Action) -> Result<Action> {
Ok(Action::Noop) Ok(Action::Noop)
} }
/// Override this method for creating components that conditionally handle input.
fn is_active(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ComponentState {
pub(crate) focus: Focus,
}
impl ComponentState {
fn new() -> Self {
Self {
focus: Focus::Active,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub enum Focus {
Active,
#[default]
Inactive,
}
pub trait FocusState {
fn with_focus(self, focus: Focus) -> Self;
fn set_focus(&mut self, focus: Focus);
fn toggle_focus(&mut self);
}
impl FocusState for ComponentState {
fn with_focus(self, focus: Focus) -> Self {
Self { focus }
}
fn set_focus(&mut self, focus: Focus) {
self.focus = focus;
}
fn toggle_focus(&mut self) {
match self.focus {
Focus::Active => self.set_focus(Focus::Inactive),
Focus::Inactive => self.set_focus(Focus::Active),
}
}
} }

View File

@ -1,4 +1,4 @@
use crate::tui::component::{Action, Component}; use crate::tui::component::{Action, Component, ComponentState, Focus};
use crate::tui::app::{AppComponents, ComponentOf}; use crate::tui::app::{AppComponents, ComponentOf};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
@ -21,6 +21,7 @@ pub struct Editor {
pub event_handler: EditorEventHandler, pub event_handler: EditorEventHandler,
pub file_path: Option<std::path::PathBuf>, pub file_path: Option<std::path::PathBuf>,
syntax_set: SyntaxSet, syntax_set: SyntaxSet,
pub(crate) component_state: ComponentState,
} }
impl<'a> ComponentOf<Editor> for AppComponents<'a> { impl<'a> ComponentOf<Editor> for AppComponents<'a> {
@ -45,6 +46,7 @@ impl Editor {
event_handler: EditorEventHandler::default(), event_handler: EditorEventHandler::default(),
file_path: None, file_path: None,
syntax_set: SyntaxSet::load_defaults_nonewlines(), syntax_set: SyntaxSet::load_defaults_nonewlines(),
component_state: Default::default(),
} }
} }
@ -106,6 +108,10 @@ impl Component for Editor {
"Editor" "Editor"
} }
fn is_active(&self) -> bool {
self.component_state.focus == Focus::Active
}
fn handle_event(&mut self, event: Event) -> Result<Action> { fn handle_event(&mut self, event: Event) -> Result<Action> {
if let Some(key_event) = event.as_key_event() { if let Some(key_event) = event.as_key_event() {
// Handle events here that should not be passed on to the vim emulation handler. // Handle events here that should not be passed on to the vim emulation handler.

View File

@ -1,5 +1,5 @@
use crate::tui::app::{AppComponents, ComponentOf}; use crate::tui::app::{AppComponents, ComponentOf};
use crate::tui::component::{Action, Component}; use crate::tui::component::{Action, Component, ComponentState, Focus};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind}; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
@ -15,6 +15,7 @@ pub struct Explorer<'a> {
root_path: std::path::PathBuf, root_path: std::path::PathBuf,
tree_items: TreeItem<'a, String>, tree_items: TreeItem<'a, String>,
tree_state: TreeState<String>, tree_state: TreeState<String>,
pub(crate) component_state: ComponentState,
} }
impl<'a> ComponentOf<Explorer<'a>> for AppComponents<'a> { impl<'a> ComponentOf<Explorer<'a>> for AppComponents<'a> {
@ -38,6 +39,7 @@ impl<'a> Explorer<'a> {
root_path: path.to_owned(), root_path: path.to_owned(),
tree_items: Self::build_tree_from_path(path.to_owned())?, tree_items: Self::build_tree_from_path(path.to_owned())?,
tree_state: TreeState::default(), tree_state: TreeState::default(),
component_state: Default::default(),
}; };
Ok(explorer) Ok(explorer)
} }
@ -131,6 +133,11 @@ impl<'a> Component for Explorer<'a> {
fn id(&self) -> &str { fn id(&self) -> &str {
"Explorer" "Explorer"
} }
fn is_active(&self) -> bool {
self.component_state.focus == Focus::Active
}
fn handle_event(&mut self, event: Event) -> Result<Action> { fn handle_event(&mut self, event: Event) -> Result<Action> {
if let Some(key_event) = event.as_key_event() { if let Some(key_event) = event.as_key_event() {
// Handle events here that should not be passed on to the vim emulation handler. // Handle events here that should not be passed on to the vim emulation handler.

View File

@ -1,5 +1,5 @@
use crate::tui::app::{AppComponents, ComponentOf}; use crate::tui::app::{AppComponents, ComponentOf};
use crate::tui::component::{Action, Component}; use crate::tui::component::{Action, Component, ComponentState, Focus};
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent}; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent};
use ratatui::layout::Rect; use ratatui::layout::Rect;
@ -11,6 +11,7 @@ use tui_logger::{TuiLoggerLevelOutput, TuiLoggerSmartWidget, TuiWidgetEvent, Tui
/// The logger is bound to info!, debug!, error!, trace! macros within Tui::new(). /// The logger is bound to info!, debug!, error!, trace! macros within Tui::new().
pub struct Logger { pub struct Logger {
state: TuiWidgetState, state: TuiWidgetState,
pub(crate) component_state: ComponentState,
} }
impl<'a> ComponentOf<Logger> for AppComponents<'a> { impl<'a> ComponentOf<Logger> for AppComponents<'a> {
@ -32,6 +33,7 @@ impl Logger {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
state: TuiWidgetState::new(), state: TuiWidgetState::new(),
component_state: Default::default(),
} }
} }
} }
@ -64,6 +66,10 @@ impl Component for Logger {
"Logger" "Logger"
} }
fn is_active(&self) -> bool {
self.component_state.focus == Focus::Active
}
fn handle_event(&mut self, event: Event) -> anyhow::Result<Action> { fn handle_event(&mut self, event: Event) -> anyhow::Result<Action> {
if let Some(key_event) = event.as_key_event() { if let Some(key_event) = event.as_key_event() {
return self.handle_key_events(key_event); return self.handle_key_events(key_event);