input pipeline is done in the library

This commit is contained in:
filipriec_vm
2026-01-11 12:50:31 +01:00
parent 0926bbee46
commit e3e2d64b2a
8 changed files with 416 additions and 23 deletions

55
examples/simple_input.rs Normal file
View File

@@ -0,0 +1,55 @@
extern crate alloc;
use tui_orchestrator::input::{default_bindings, ComponentAction, InputError, InputSource, Key};
struct MockInput {
keys: alloc::vec::Vec<Key>,
index: usize,
}
impl MockInput {
fn new(keys: alloc::vec::Vec<Key>) -> Self {
Self { keys, index: 0 }
}
}
impl InputSource for MockInput {
fn read_key(&mut self) -> Result<Key, InputError> {
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;
}
}
}
}

View File

@@ -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 {} 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 {}

View File

@@ -1,43 +1,98 @@
// path_from_the_root: src/input/bindings.rs use super::action::{Action, ComponentAction};
use super::action::Action;
use super::key::Key; use super::key::Key;
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
use hashbrown::HashSet; 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)] #[derive(Debug, Clone)]
pub struct Bindings<A: Action> { pub struct Bindings<A: Action> {
#[cfg(feature = "alloc")]
bindings: hashbrown::HashMap<Key, A>,
#[cfg(not(feature = "alloc"))]
bindings: alloc::vec::Vec<(Key, A)>, bindings: alloc::vec::Vec<(Key, A)>,
} }
impl<A: Action> Bindings<A> { impl<A: Action> Bindings<A> {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
#[cfg(feature = "alloc")]
bindings: hashbrown::HashMap::new(),
#[cfg(not(feature = "alloc"))]
bindings: alloc::vec::Vec::new(), bindings: alloc::vec::Vec::new(),
} }
} }
pub fn bind(&mut self, key: Key, action: A) { pub fn bind(&mut self, key: Key, action: A) {
#[cfg(feature = "alloc")]
{
self.bindings.insert(key, action);
}
#[cfg(not(feature = "alloc"))]
{
self.bindings.push((key, action)); self.bindings.push((key, action));
} }
}
pub fn get(&self, key: &Key) -> Option<&A> { pub fn get(&self, key: &Key) -> Option<&A> {
#[cfg(feature = "alloc")]
{
self.bindings.get(key)
}
#[cfg(not(feature = "alloc"))]
{
self.bindings.iter().find(|(k, _)| k == key).map(|(_, a)| a) self.bindings.iter().find(|(k, _)| k == key).map(|(_, a)| a)
} }
}
pub fn remove(&mut self, key: &Key) { pub fn remove(&mut self, key: &Key) {
#[cfg(feature = "alloc")]
{
self.bindings.remove(key);
}
#[cfg(not(feature = "alloc"))]
{
self.bindings.retain(|(k, _)| k != key); self.bindings.retain(|(k, _)| k != key);
} }
}
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
#[cfg(feature = "alloc")]
{
self.bindings.is_empty() self.bindings.is_empty()
} }
#[cfg(not(feature = "alloc"))]
{
self.bindings.is_empty()
}
}
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
#[cfg(feature = "alloc")]
{
self.bindings.len() 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<Item = &(Key, A)> { pub fn iter(&self) -> impl Iterator<Item = &(Key, A)> {
self.bindings.iter() self.bindings.iter()
} }
@@ -52,19 +107,71 @@ impl<A: Action> Default for Bindings<A> {
#[cfg(feature = "sequences")] #[cfg(feature = "sequences")]
impl<A: Action + core::hash::Hash + Eq> Bindings<A> { impl<A: Action + core::hash::Hash + Eq> Bindings<A> {
pub fn bind_sequence(&mut self, keys: alloc::vec::Vec<Key>, action: A) { pub fn bind_sequence(&mut self, keys: alloc::vec::Vec<Key>, action: A) {
#[cfg(feature = "alloc")]
{
for key in keys {
self.bindings.insert(key, action.clone());
}
}
#[cfg(not(feature = "alloc"))]
{
for key in keys { for key in keys {
self.bindings.push((key, action.clone())); self.bindings.push((key, action.clone()));
} }
} }
}
pub fn get_sequences(&self) -> alloc::vec::Vec<&A> { pub fn get_sequences(&self) -> alloc::vec::Vec<&A> {
let mut actions = alloc::vec::Vec::new(); let mut actions = alloc::vec::Vec::new();
let mut seen = HashSet::new(); let mut seen = HashSet::new();
#[cfg(feature = "alloc")]
{
for (_, action) in &self.bindings { for (_, action) in &self.bindings {
if seen.insert(action) { if seen.insert(action) {
actions.push(action); actions.push(action);
} }
} }
}
#[cfg(not(feature = "alloc"))]
{
for (_, action) in &self.bindings {
if seen.insert(action) {
actions.push(action);
}
}
}
actions 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<ComponentAction> {
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
}

View File

@@ -1,5 +1,3 @@
// path_from_the_root: src/input/handler.rs
#[cfg(feature = "sequences")] #[cfg(feature = "sequences")]
use super::action::Action; use super::action::Action;
@@ -9,10 +7,20 @@ use super::key::Key;
#[cfg(feature = "sequences")] #[cfg(feature = "sequences")]
use super::result::MatchResult; 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")] #[cfg(feature = "sequences")]
pub struct SequenceHandler<A: Action> { pub struct SequenceHandler<A: Action> {
sequences: alloc::vec::Vec<(alloc::vec::Vec<Key>, A)>, sequences: alloc::vec::Vec<(alloc::vec::Vec<Key>, A)>,
current: alloc::vec::Vec<Key>, current: alloc::vec::Vec<Key>,
#[cfg(feature = "std")]
last_timestamp: Option<Instant>,
#[cfg(feature = "std")]
timeout: Duration,
} }
#[cfg(feature = "sequences")] #[cfg(feature = "sequences")]
@@ -21,6 +29,17 @@ impl<A: Action> SequenceHandler<A> {
Self { Self {
sequences: alloc::vec::Vec::new(), sequences: alloc::vec::Vec::new(),
current: 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<A: Action> SequenceHandler<A> {
} }
pub fn handle(&mut self, key: Key) -> MatchResult<A> { pub fn handle(&mut self, key: Key) -> MatchResult<A> {
#[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); self.current.push(key);
for (seq, action) in &self.sequences { for (seq, action) in &self.sequences {

View File

@@ -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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyCode { pub enum KeyCode {
Char(char), Char(char),
@@ -20,6 +22,9 @@ pub enum KeyCode {
Null, 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct KeyModifiers { pub struct KeyModifiers {
pub control: bool, 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Key { pub struct Key {
pub code: KeyCode, 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 { pub fn display_string(&self) -> alloc::string::String {
let mut out = alloc::string::String::new(); let mut out = alloc::string::String::new();
if self.modifiers.control { if self.modifiers.control {

View File

@@ -5,11 +5,13 @@ pub mod bindings;
pub mod handler; pub mod handler;
pub mod key; pub mod key;
pub mod result; pub mod result;
pub mod source;
pub use action::Action; pub use action::{Action, ComponentAction};
pub use bindings::Bindings; pub use bindings::{default_bindings, Bindings};
pub use key::{Key, KeyCode, KeyModifiers}; pub use key::{Key, KeyCode, KeyModifiers};
pub use result::MatchResult; pub use result::MatchResult;
pub use source::{InputError, InputSource};
#[cfg(feature = "sequences")] #[cfg(feature = "sequences")]
pub use handler::SequenceHandler; pub use handler::SequenceHandler;

View File

@@ -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)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum MatchResult<A> { pub enum MatchResult<A> {
Match(A), Match(A),

38
src/input/source.rs Normal file
View File

@@ -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<Key, InputError> {
/// 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<Key, InputError>;
}