From e3e2d64b2adc59caafeab3ccaec5eb512a93ac14 Mon Sep 17 00:00:00 2001 From: filipriec_vm Date: Sun, 11 Jan 2026 12:50:31 +0100 Subject: [PATCH] input pipeline is done in the library --- examples/simple_input.rs | 55 ++++++++++++++++ src/input/action.rs | 36 ++++++++++- src/input/bindings.rs | 133 +++++++++++++++++++++++++++++++++++---- src/input/handler.rs | 33 +++++++++- src/input/key.rs | 131 +++++++++++++++++++++++++++++++++++++- src/input/mod.rs | 6 +- src/input/result.rs | 7 ++- src/input/source.rs | 38 +++++++++++ 8 files changed, 416 insertions(+), 23 deletions(-) create mode 100644 examples/simple_input.rs create mode 100644 src/input/source.rs diff --git a/examples/simple_input.rs b/examples/simple_input.rs new file mode 100644 index 0000000..ea989bb --- /dev/null +++ b/examples/simple_input.rs @@ -0,0 +1,55 @@ +extern crate alloc; + +use tui_orchestrator::input::{default_bindings, ComponentAction, InputError, InputSource, Key}; + +struct MockInput { + keys: alloc::vec::Vec, + index: usize, +} + +impl MockInput { + fn new(keys: alloc::vec::Vec) -> Self { + Self { keys, index: 0 } + } +} + +impl InputSource for MockInput { + fn read_key(&mut self) -> Result { + if self.index < self.keys.len() { + let key = self.keys[self.index]; + self.index += 1; + Ok(key) + } else { + Err(InputError::BackendError) + } + } +} + +fn main() { + let bindings = default_bindings(); + let mut input = MockInput::new(alloc::vec![Key::tab(), Key::enter(), Key::esc(),]); + + loop { + match input.read_key() { + Ok(key) => { + if let Some(action) = bindings.get(&key) { + match action { + ComponentAction::Next => println!("Next"), + ComponentAction::Select => println!("Select"), + ComponentAction::Cancel => { + println!("Cancel - exiting"); + break; + } + _ => println!("Other action: {:?}", action), + } + } else { + println!("No binding for key: {:?}", key); + } + } + Err(e) => { + println!("Error: {:?}", e); + break; + } + } + } +} diff --git a/src/input/action.rs b/src/input/action.rs index d49935e..b06d839 100644 --- a/src/input/action.rs +++ b/src/input/action.rs @@ -1,3 +1,35 @@ -// path_from_the_root: src/input/action.rs - +/// Marker trait for actions that can be bound to keys. +/// +/// Actions must be cloneable, comparable, and debuggable. pub trait Action: Clone + PartialEq + Eq + core::fmt::Debug {} + +/// Default component actions for common TUI patterns. +/// +/// These actions cover the most common TUI interactions: +/// - Navigation (next, prev, first, last, directional) +/// - Interaction (select, cancel) +/// - Text input (type character, backspace, delete) +/// - Custom actions via `Custom(usize)` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ComponentAction { + Next, + Prev, + First, + Last, + Select, + Cancel, + TypeChar(char), + Backspace, + Delete, + Home, + End, + PageUp, + PageDown, + Up, + Down, + Left, + Right, + Custom(usize), +} + +impl Action for ComponentAction {} diff --git a/src/input/bindings.rs b/src/input/bindings.rs index ddcd351..acd4c38 100644 --- a/src/input/bindings.rs +++ b/src/input/bindings.rs @@ -1,43 +1,98 @@ -// path_from_the_root: src/input/bindings.rs - -use super::action::Action; +use super::action::{Action, ComponentAction}; use super::key::Key; #[cfg(feature = "alloc")] use hashbrown::HashSet; +/// Maps keys to actions. +/// +/// When `alloc` feature is enabled, uses HashMap for O(1) lookup. +/// Without `alloc`, falls back to Vec with O(n) lookup. #[derive(Debug, Clone)] pub struct Bindings { + #[cfg(feature = "alloc")] + bindings: hashbrown::HashMap, + #[cfg(not(feature = "alloc"))] bindings: alloc::vec::Vec<(Key, A)>, } impl Bindings { pub fn new() -> Self { Self { + #[cfg(feature = "alloc")] + bindings: hashbrown::HashMap::new(), + #[cfg(not(feature = "alloc"))] bindings: alloc::vec::Vec::new(), } } pub fn bind(&mut self, key: Key, action: A) { - self.bindings.push((key, action)); + #[cfg(feature = "alloc")] + { + self.bindings.insert(key, action); + } + #[cfg(not(feature = "alloc"))] + { + self.bindings.push((key, action)); + } } pub fn get(&self, key: &Key) -> Option<&A> { - self.bindings.iter().find(|(k, _)| k == key).map(|(_, a)| a) + #[cfg(feature = "alloc")] + { + self.bindings.get(key) + } + #[cfg(not(feature = "alloc"))] + { + self.bindings.iter().find(|(k, _)| k == key).map(|(_, a)| a) + } } pub fn remove(&mut self, key: &Key) { - self.bindings.retain(|(k, _)| k != key); + #[cfg(feature = "alloc")] + { + self.bindings.remove(key); + } + #[cfg(not(feature = "alloc"))] + { + self.bindings.retain(|(k, _)| k != key); + } } pub fn is_empty(&self) -> bool { - self.bindings.is_empty() + #[cfg(feature = "alloc")] + { + self.bindings.is_empty() + } + #[cfg(not(feature = "alloc"))] + { + self.bindings.is_empty() + } } pub fn len(&self) -> usize { - self.bindings.len() + #[cfg(feature = "alloc")] + { + self.bindings.len() + } + #[cfg(not(feature = "alloc"))] + { + self.bindings.len() + } } + pub fn keys(&self) -> alloc::vec::Vec<&Key> { + #[cfg(feature = "alloc")] + { + self.bindings.keys().collect() + } + #[cfg(not(feature = "alloc"))] + { + self.bindings.iter().map(|(k, _)| k).collect() + } + } + + #[cfg(not(feature = "alloc"))] pub fn iter(&self) -> impl Iterator { self.bindings.iter() } @@ -52,19 +107,71 @@ impl Default for Bindings { #[cfg(feature = "sequences")] impl Bindings { pub fn bind_sequence(&mut self, keys: alloc::vec::Vec, action: A) { - for key in keys { - self.bindings.push((key, action.clone())); + #[cfg(feature = "alloc")] + { + for key in keys { + self.bindings.insert(key, action.clone()); + } + } + #[cfg(not(feature = "alloc"))] + { + for key in keys { + self.bindings.push((key, action.clone())); + } } } pub fn get_sequences(&self) -> alloc::vec::Vec<&A> { let mut actions = alloc::vec::Vec::new(); let mut seen = HashSet::new(); - for (_, action) in &self.bindings { - if seen.insert(action) { - actions.push(action); + #[cfg(feature = "alloc")] + { + for (_, action) in &self.bindings { + if seen.insert(action) { + actions.push(action); + } + } + } + #[cfg(not(feature = "alloc"))] + { + for (_, action) in &self.bindings { + if seen.insert(action) { + actions.push(action); + } } } actions } } + +/// Returns default key bindings for common TUI patterns. +/// +/// Includes: +/// - Tab/Shift+Tab for next/prev navigation +/// - Enter for select +/// - Esc for cancel +/// - Arrow keys for directional movement +/// - Home/End/PageUp/PageDown for scrolling +/// - Backspace/Delete for editing +/// - Ctrl+C as custom quit action +/// +/// Use as-is or extend with your own bindings. +pub fn default_bindings() -> Bindings { + let mut bindings = Bindings::new(); + bindings.bind(Key::tab(), ComponentAction::Next); + bindings.bind(Key::shift_tab(), ComponentAction::Prev); + bindings.bind(Key::enter(), ComponentAction::Select); + bindings.bind(Key::esc(), ComponentAction::Cancel); + bindings.bind(Key::home(), ComponentAction::Home); + bindings.bind(Key::end(), ComponentAction::End); + bindings.bind(Key::page_up(), ComponentAction::PageUp); + bindings.bind(Key::page_down(), ComponentAction::PageDown); + bindings.bind(Key::up(), ComponentAction::Up); + bindings.bind(Key::down(), ComponentAction::Down); + bindings.bind(Key::left(), ComponentAction::Left); + bindings.bind(Key::right(), ComponentAction::Right); + bindings.bind(Key::backspace(), ComponentAction::Backspace); + bindings.bind(Key::delete(), ComponentAction::Delete); + bindings.bind(Key::ctrl('c'), ComponentAction::Custom(0)); + bindings +} diff --git a/src/input/handler.rs b/src/input/handler.rs index 26fd8dc..35326fd 100644 --- a/src/input/handler.rs +++ b/src/input/handler.rs @@ -1,5 +1,3 @@ -// path_from_the_root: src/input/handler.rs - #[cfg(feature = "sequences")] use super::action::Action; @@ -9,10 +7,20 @@ use super::key::Key; #[cfg(feature = "sequences")] use super::result::MatchResult; +#[cfg(all(feature = "sequences", feature = "std"))] +extern crate std; + +#[cfg(all(feature = "sequences", feature = "std"))] +use std::time::{Duration, Instant}; + #[cfg(feature = "sequences")] pub struct SequenceHandler { sequences: alloc::vec::Vec<(alloc::vec::Vec, A)>, current: alloc::vec::Vec, + #[cfg(feature = "std")] + last_timestamp: Option, + #[cfg(feature = "std")] + timeout: Duration, } #[cfg(feature = "sequences")] @@ -21,6 +29,17 @@ impl SequenceHandler { Self { sequences: alloc::vec::Vec::new(), current: alloc::vec::Vec::new(), + #[cfg(feature = "std")] + last_timestamp: None, + #[cfg(feature = "std")] + timeout: Duration::from_millis(500), + } + } + + pub fn with_timeout_ms(&mut self, ms: u64) { + #[cfg(feature = "std")] + { + self.timeout = Duration::from_millis(ms); } } @@ -30,6 +49,16 @@ impl SequenceHandler { } pub fn handle(&mut self, key: Key) -> MatchResult { + #[cfg(feature = "std")] + { + if let Some(last) = self.last_timestamp { + if last.elapsed() > self.timeout { + self.current.clear(); + } + } + self.last_timestamp = Some(Instant::now()); + } + self.current.push(key); for (seq, action) in &self.sequences { diff --git a/src/input/key.rs b/src/input/key.rs index 11ab017..f5d1e7e 100644 --- a/src/input/key.rs +++ b/src/input/key.rs @@ -1,5 +1,7 @@ -// path_from_the_root: src/input/key.rs - +/// Represents a key code without modifiers. +/// +/// This includes character keys, special keys (Enter, Tab, Esc, etc.), +/// and function keys F1-F255. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum KeyCode { Char(char), @@ -20,6 +22,9 @@ pub enum KeyCode { Null, } +/// Represents key modifier flags (Control, Alt, Shift). +/// +/// These modifiers can be combined with any key code. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct KeyModifiers { pub control: bool, @@ -56,6 +61,9 @@ impl KeyModifiers { } } +/// Represents a complete key press with modifiers. +/// +/// This is the main type used throughout the input system. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Key { pub code: KeyCode, @@ -81,6 +89,125 @@ impl Key { } } + pub const fn shift_tab() -> Self { + Self { + code: KeyCode::Tab, + modifiers: KeyModifiers::new().with_shift(), + } + } + + pub const fn alt_char(c: char) -> Self { + Self { + code: KeyCode::Char(c), + modifiers: KeyModifiers::new().with_alt(), + } + } + + pub const fn ctrl_shift_char(c: char) -> Self { + Self { + code: KeyCode::Char(c), + modifiers: KeyModifiers::new().with_control().with_shift(), + } + } + + pub const fn alt_ctrl_char(c: char) -> Self { + Self { + code: KeyCode::Char(c), + modifiers: KeyModifiers::new().with_alt().with_control(), + } + } + + pub const fn enter() -> Self { + Self { + code: KeyCode::Enter, + modifiers: KeyModifiers::new(), + } + } + + pub const fn esc() -> Self { + Self { + code: KeyCode::Esc, + modifiers: KeyModifiers::new(), + } + } + + pub const fn backspace() -> Self { + Self { + code: KeyCode::Backspace, + modifiers: KeyModifiers::new(), + } + } + + pub const fn delete() -> Self { + Self { + code: KeyCode::Delete, + modifiers: KeyModifiers::new(), + } + } + + pub const fn tab() -> Self { + Self { + code: KeyCode::Tab, + modifiers: KeyModifiers::new(), + } + } + + pub const fn page_up() -> Self { + Self { + code: KeyCode::PageUp, + modifiers: KeyModifiers::new(), + } + } + + pub const fn page_down() -> Self { + Self { + code: KeyCode::PageDown, + modifiers: KeyModifiers::new(), + } + } + + pub const fn up() -> Self { + Self { + code: KeyCode::Up, + modifiers: KeyModifiers::new(), + } + } + + pub const fn down() -> Self { + Self { + code: KeyCode::Down, + modifiers: KeyModifiers::new(), + } + } + + pub const fn left() -> Self { + Self { + code: KeyCode::Left, + modifiers: KeyModifiers::new(), + } + } + + pub const fn right() -> Self { + Self { + code: KeyCode::Right, + modifiers: KeyModifiers::new(), + } + } + + pub const fn home() -> Self { + Self { + code: KeyCode::Home, + modifiers: KeyModifiers::new(), + } + } + + pub const fn end() -> Self { + Self { + code: KeyCode::End, + modifiers: KeyModifiers::new(), + } + } + pub fn display_string(&self) -> alloc::string::String { let mut out = alloc::string::String::new(); if self.modifiers.control { diff --git a/src/input/mod.rs b/src/input/mod.rs index c0770ed..85a0dea 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -5,11 +5,13 @@ pub mod bindings; pub mod handler; pub mod key; pub mod result; +pub mod source; -pub use action::Action; -pub use bindings::Bindings; +pub use action::{Action, ComponentAction}; +pub use bindings::{default_bindings, Bindings}; pub use key::{Key, KeyCode, KeyModifiers}; pub use result::MatchResult; +pub use source::{InputError, InputSource}; #[cfg(feature = "sequences")] pub use handler::SequenceHandler; diff --git a/src/input/result.rs b/src/input/result.rs index 3d4b285..d8c70eb 100644 --- a/src/input/result.rs +++ b/src/input/result.rs @@ -1,5 +1,8 @@ -// path_from_the_root: src/input/result.rs - +/// Result of matching a key or sequence. +/// +/// - `Match(action)` - Key or sequence matched, returns the action +/// - `Pending` - Sequence in progress, waiting for more keys +/// - `NoMatch` - No binding found #[derive(Debug, Clone, PartialEq, Eq)] pub enum MatchResult { Match(A), diff --git a/src/input/source.rs b/src/input/source.rs new file mode 100644 index 0000000..3c37e57 --- /dev/null +++ b/src/input/source.rs @@ -0,0 +1,38 @@ +use super::key::Key; + +/// Errors that can occur when reading input. +#[derive(Debug)] +pub enum InputError { + NotAKeyEvent, + BackendError, +} + +/// Trait for reading keys from a backend. +/// +/// Users implement this trait to bridge to crossterm, termion, +/// or any other terminal backend. +/// +/// Example with crossterm: +/// +/// ```ignore +/// use crossterm::event; +/// +/// struct CrosstermInput; +/// +/// impl InputSource for CrosstermInput { +/// fn read_key(&mut self) -> Result { +/// match event::read()? { +/// event::Event::Key(key_event) => { +/// Ok(Key::new( +/// KeyCode::from(key_event.code), +/// KeyModifiers::from(key_event.modifiers), +/// )) +/// } +/// _ => Err(InputError::NotAKeyEvent), +/// } +/// } +/// } +/// ``` +pub trait InputSource { + fn read_key(&mut self) -> Result; +}