TUI #1
29
Cargo.lock
generated
29
Cargo.lock
generated
@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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'),
|
||||||
|
|||||||
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user