From 95149b79c4b0d013dcba1eda80e1d14a01087fea Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 27 Apr 2025 16:28:05 +0300 Subject: [PATCH] tui: add pgup/pgdn and extract tui_keys --- tui/src/lib.rs | 15 +++++ tui/src/tui_engine.rs | 57 ++++++++--------- tui/src/tui_engine/tui_input.rs | 104 +------------------------------- tui/src/tui_engine/tui_keys.rs | 90 +++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 130 deletions(-) create mode 100644 tui/src/tui_engine/tui_keys.rs diff --git a/tui/src/lib.rs b/tui/src/lib.rs index 4a5f751..8e98c58 100644 --- a/tui/src/lib.rs +++ b/tui/src/lib.rs @@ -63,3 +63,18 @@ pub(crate) use std::ffi::OsString; //engine.run(&state)?; Ok(()) } + +#[cfg(test)] #[test] fn test_parse_key () { + use KeyModifiers as Mods; + let test = |x: &str, y|assert_eq!(KeyMatcher::new(x).build(), Some(Event::Key(y))); + //test(":x", + //KeyEvent::new(KeyCode::Char('x'), Mods::NONE)); + //test(":ctrl-x", + //KeyEvent::new(KeyCode::Char('x'), Mods::CONTROL)); + //test(":alt-x", + //KeyEvent::new(KeyCode::Char('x'), Mods::ALT)); + //test(":shift-x", + //KeyEvent::new(KeyCode::Char('x'), Mods::SHIFT)); + //test(":ctrl-alt-shift-x", + //KeyEvent::new(KeyCode::Char('x'), Mods::CONTROL | Mods::ALT | Mods::SHIFT )); +} diff --git a/tui/src/tui_engine.rs b/tui/src/tui_engine.rs index 8caa842..0969cc3 100644 --- a/tui/src/tui_engine.rs +++ b/tui/src/tui_engine.rs @@ -1,10 +1,11 @@ use crate::*; use std::time::Duration; -mod tui_buffer; pub use self::tui_buffer::*; -mod tui_input; pub use self::tui_input::*; -mod tui_output; pub use self::tui_output::*; -mod tui_perf; pub use self::tui_perf::*; +mod tui_buffer; pub use self::tui_buffer::*; +mod tui_input; pub use self::tui_input::*; +mod tui_keys; pub use self::tui_keys::*; +mod tui_output; pub use self::tui_output::*; +mod tui_perf; pub use self::tui_perf::*; pub struct Tui { pub exited: Arc, @@ -14,30 +15,6 @@ pub struct Tui { pub perf: PerfModel, } -pub trait TuiRun + Handle + 'static> { - /// Run an app in the main loop. - fn run (&self, state: &Arc>) -> Usually<()>; -} - -impl + Handle + 'static> TuiRun for Arc> { - fn run (&self, state: &Arc>) -> Usually<()> { - let _input_thread = TuiIn::run_input(self, state, Duration::from_millis(100)); - self.write().unwrap().setup()?; - let render_thread = TuiOut::run_output(self, state, Duration::from_millis(10)); - match render_thread.join() { - Ok(result) => { - self.write().unwrap().teardown()?; - println!("\n\rRan successfully: {result:?}\n\r"); - }, - Err(error) => { - self.write().unwrap().teardown()?; - panic!("\n\rRender thread failed: {error:?}.\n\r") - }, - } - Ok(()) - } -} - impl Tui { /// Construct a new TUI engine and wrap it for shared ownership. pub fn new () -> Usually>> { @@ -89,3 +66,27 @@ impl Tui { disable_raw_mode().map_err(Into::into) } } + +pub trait TuiRun + Handle + 'static> { + /// Run an app in the main loop. + fn run (&self, state: &Arc>) -> Usually<()>; +} + +impl + Handle + 'static> TuiRun for Arc> { + fn run (&self, state: &Arc>) -> Usually<()> { + let _input_thread = TuiIn::run_input(self, state, Duration::from_millis(100)); + self.write().unwrap().setup()?; + let render_thread = TuiOut::run_output(self, state, Duration::from_millis(10)); + match render_thread.join() { + Ok(result) => { + self.write().unwrap().teardown()?; + println!("\n\rRan successfully: {result:?}\n\r"); + }, + Err(error) => { + self.write().unwrap().teardown()?; + panic!("\n\rRender thread failed: {error:?}.\n\r") + }, + } + Ok(()) + } +} diff --git a/tui/src/tui_engine/tui_input.rs b/tui/src/tui_engine/tui_input.rs index 6f1c661..3cd6373 100644 --- a/tui/src/tui_engine/tui_input.rs +++ b/tui/src/tui_engine/tui_input.rs @@ -11,6 +11,7 @@ impl Input for TuiIn { fn is_done (&self) -> bool { self.0.fetch_and(true, Relaxed) } fn done (&self) { self.0.store(true, Relaxed); } } + impl TuiIn { /// Spawn the input thread. pub fn run_input + 'static> ( @@ -47,6 +48,7 @@ impl TuiIn { }) } } + impl AtomInput for TuiIn { fn matches_atom (&self, token: &str) -> bool { if let Some(event) = KeyMatcher::new(token).build() { @@ -55,106 +57,4 @@ impl AtomInput for TuiIn { false } } - //fn get_event (item: &AtomItem>) -> Option { - //match item { AtomItem::Sym(s) => KeyMatcher::new(s).build(), _ => None } - //} -} -struct KeyMatcher { - valid: bool, - key: Option, - mods: KeyModifiers, -} -impl KeyMatcher { - fn new (token: impl AsRef) -> Self { - let token = token.as_ref(); - if token.len() < 2 { - Self { valid: false, key: None, mods: KeyModifiers::NONE } - } else if token.chars().next() != Some('@') { - Self { valid: false, key: None, mods: KeyModifiers::NONE } - } else { - Self { valid: true, key: None, mods: KeyModifiers::NONE }.next(&token[1..]) - } - } - fn next (mut self, token: &str) -> Self { - let mut tokens = token.split('-').peekable(); - while let Some(token) = tokens.next() { - if tokens.peek().is_some() { - match token { - "ctrl" | "Ctrl" | "c" | "C" => self.mods |= KeyModifiers::CONTROL, - "alt" | "Alt" | "m" | "M" => self.mods |= KeyModifiers::ALT, - "shift" | "Shift" | "s" | "S" => { - self.mods |= KeyModifiers::SHIFT; - // + TODO normalize character case, BackTab, etc. - }, - _ => panic!("unknown modifier {token}"), - } - } else { - self.key = if token.len() == 1 { - Some(KeyCode::Char(token.chars().next().unwrap())) - } else { - Some(Self::named_key(token).unwrap_or_else(||panic!("unknown character {token}"))) - } - } - } - self - } - fn named_key (token: &str) -> Option { - use KeyCode::*; - Some(match token { - "up" => Up, - "down" => Down, - "left" => Left, - "right" => Right, - "esc" | "escape" => Esc, - "enter" | "return" => Enter, - "delete" | "del" => Delete, - "tab" => Tab, - "space" => Char(' '), - "comma" => Char(','), - "period" => Char('.'), - "plus" => Char('+'), - "minus" | "dash" => Char('-'), - "equal" | "equals" => Char('='), - "underscore" => Char('_'), - "backtick" => Char('`'), - "lt" => Char('<'), - "gt" => Char('>'), - "openbracket" => Char('['), - "closebracket" => Char(']'), - "f1" => F(1), - "f2" => F(2), - "f3" => F(3), - "f4" => F(4), - "f5" => F(5), - "f6" => F(6), - "f7" => F(7), - "f8" => F(8), - "f9" => F(9), - "f10" => F(10), - "f11" => F(11), - "f12" => F(12), - _ => return None, - }) - } - fn build (self) -> Option { - if self.valid && self.key.is_some() { - Some(Event::Key(KeyEvent::new(self.key.unwrap(), self.mods))) - } else { - None - } - } -} -#[cfg(test)] #[test] fn test_parse_key () { - use KeyModifiers as Mods; - let test = |x: &str, y|assert_eq!(KeyMatcher::new(x).build(), Some(Event::Key(y))); - //test(":x", - //KeyEvent::new(KeyCode::Char('x'), Mods::NONE)); - //test(":ctrl-x", - //KeyEvent::new(KeyCode::Char('x'), Mods::CONTROL)); - //test(":alt-x", - //KeyEvent::new(KeyCode::Char('x'), Mods::ALT)); - //test(":shift-x", - //KeyEvent::new(KeyCode::Char('x'), Mods::SHIFT)); - //test(":ctrl-alt-shift-x", - //KeyEvent::new(KeyCode::Char('x'), Mods::CONTROL | Mods::ALT | Mods::SHIFT )); } diff --git a/tui/src/tui_engine/tui_keys.rs b/tui/src/tui_engine/tui_keys.rs new file mode 100644 index 0000000..6e0e0e6 --- /dev/null +++ b/tui/src/tui_engine/tui_keys.rs @@ -0,0 +1,90 @@ +use crate::*; + +pub struct KeyMatcher { + valid: bool, + key: Option, + mods: KeyModifiers, +} + +impl KeyMatcher { + pub fn new (token: impl AsRef) -> Self { + let token = token.as_ref(); + if token.len() < 2 { + Self { valid: false, key: None, mods: KeyModifiers::NONE } + } else if token.chars().next() != Some('@') { + Self { valid: false, key: None, mods: KeyModifiers::NONE } + } else { + Self { valid: true, key: None, mods: KeyModifiers::NONE }.next(&token[1..]) + } + } + pub fn build (self) -> Option { + if self.valid && self.key.is_some() { + Some(Event::Key(KeyEvent::new(self.key.unwrap(), self.mods))) + } else { + None + } + } + fn next (mut self, token: &str) -> Self { + let mut tokens = token.split('-').peekable(); + while let Some(token) = tokens.next() { + if tokens.peek().is_some() { + match token { + "ctrl" | "Ctrl" | "c" | "C" => self.mods |= KeyModifiers::CONTROL, + "alt" | "Alt" | "m" | "M" => self.mods |= KeyModifiers::ALT, + "shift" | "Shift" | "s" | "S" => { + self.mods |= KeyModifiers::SHIFT; + // + TODO normalize character case, BackTab, etc. + }, + _ => panic!("unknown modifier {token}"), + } + } else { + self.key = if token.len() == 1 { + Some(KeyCode::Char(token.chars().next().unwrap())) + } else { + Some(Self::named_key(token).unwrap_or_else(||panic!("unknown character {token}"))) + } + } + } + self + } + fn named_key (token: &str) -> Option { + use KeyCode::*; + Some(match token { + "up" => Up, + "down" => Down, + "left" => Left, + "right" => Right, + "esc" | "escape" => Esc, + "enter" | "return" => Enter, + "delete" | "del" => Delete, + "tab" => Tab, + "space" => Char(' '), + "comma" => Char(','), + "period" => Char('.'), + "plus" => Char('+'), + "minus" | "dash" => Char('-'), + "equal" | "equals" => Char('='), + "underscore" => Char('_'), + "backtick" => Char('`'), + "lt" => Char('<'), + "gt" => Char('>'), + "openbracket" => Char('['), + "closebracket" => Char(']'), + "pgup" | "pageup" => PageUp, + "pgdn" | "pagedown" => PageDown, + "f1" => F(1), + "f2" => F(2), + "f3" => F(3), + "f4" => F(4), + "f5" => F(5), + "f6" => F(6), + "f7" => F(7), + "f8" => F(8), + "f9" => F(9), + "f10" => F(10), + "f11" => F(11), + "f12" => F(12), + _ => return None, + }) + } +}