# Input Pipeline Migration Guide ## Goal Migrate `komp_ac_client/src/input_pipeline/` to a generalized `no_std` compatible API in `src/input_pipeline/` that can be used by any TUI application. ## Current State (komp_ac_client) Files in `komp_ac_client/src/input_pipeline/`: - `key_chord.rs` - Uses crossterm::event::{KeyCode, KeyModifiers} - `sequence.rs` - Uses std::time::{Duration, Instant} - `registry.rs` - Uses std::collections::HashMap - `pipeline.rs` - Pure logic - `response.rs` - Pure types Dependencies to remove: - `crossterm::event` - Replace with custom types - `std::time` - Replace with alloc-based solution or make optional - `std::collections` - Use alloc or custom structures ## Target Structure ``` src/input_pipeline/ ├── mod.rs # Routing only ├── key.rs # KeyCode, KeyModifiers enums (no_std) ├── chord.rs # KeyChord type (no_std) ├── sequence.rs # KeySequence type (no_std) ├── key_map.rs # KeyMap: Chord -> Action mapping (no_std) ├── key_registry.rs # Registry for storing key bindings (alloc) ├── sequence_tracker.rs # Track incomplete sequences (optional with std feature) ├── pipeline.rs # Main pipeline logic (no_std) └── response.rs # Response types (no_std) ``` ## Step 1: Core Types (no_std) ### `src/input_pipeline/key.rs` Define backend-agnostic KeyCode and KeyModifiers: ```rust // path_from_the_root: src/input_pipeline/key.rs use core::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum KeyCode { Char(char), Enter, Tab, Esc, Backspace, Delete, Home, End, PageUp, PageDown, Up, Down, Left, Right, F(u8), Null, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct KeyModifiers { pub control: bool, pub alt: bool, pub shift: bool, } impl KeyModifiers { pub const fn new() -> Self { Self { control: false, alt: false, shift: false, } } pub const fn with_control(mut self) -> Self { self.control = true; self } pub const fn with_alt(mut self) -> Self { self.alt = true; self } pub const fn with_shift(mut self) -> Self { self.shift = true; self } pub const fn is_empty(&self) -> bool { !self.control && !self.alt && !self.shift } } ``` ### `src/input_pipeline/chord.rs` Define KeyChord using custom types: ```rust // path_from_the_root: src/input_pipeline/chord.rs use super::key::{KeyCode, KeyModifiers}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct KeyChord { pub code: KeyCode, pub modifiers: KeyModifiers, } impl KeyChord { pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self { Self { code, modifiers } } pub const fn char(c: char) -> Self { Self { code: KeyCode::Char(c), modifiers: KeyModifiers::new(), } } pub fn display_string(&self) -> alloc::string::String { let mut out = alloc::string::String::new(); if self.modifiers.control { out.push_str("Ctrl+"); } if self.modifiers.alt { out.push_str("Alt+"); } if self.modifiers.shift { out.push_str("Shift+"); } match self.code { KeyCode::Char(c) => out.push(c), KeyCode::Enter => out.push_str("Enter"), KeyCode::Tab => out.push_str("Tab"), KeyCode::Esc => out.push_str("Esc"), KeyCode::Backspace => out.push_str("Backspace"), KeyCode::Delete => out.push_str("Delete"), KeyCode::Up => out.push_str("Up"), KeyCode::Down => out.push_str("Down"), KeyCode::Left => out.push_str("Left"), KeyCode::Right => out.push_str("Right"), KeyCode::F(n) => { out.push('F'); out.push(char::from_digit(n as u32, 10).unwrap_or('0')); } KeyCode::Home => out.push_str("Home"), KeyCode::End => out.push_str("End"), KeyCode::PageUp => out.push_str("PageUp"), KeyCode::PageDown => out.push_str("PageDown"), KeyCode::Null => out.push_str("Null"), } out } } impl From for KeyChord { fn from(code: KeyCode) -> Self { Self { code, modifiers: KeyModifiers::new(), } } } ``` ## Step 2: Sequence Types (no_std) ### `src/input_pipeline/sequence.rs` ```rust // path_from_the_root: src/input_pipeline/sequence.rs use super::chord::KeyChord; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct KeySequence { chords: alloc::vec::Vec, } impl KeySequence { pub fn new() -> Self { Self { chords: alloc::vec::Vec::new(), } } pub fn from_chords(chords: impl IntoIterator) -> Self { Self { chords: chords.into_iter().collect(), } } pub fn push(&mut self, chord: KeyChord) { self.chords.push(chord); } pub fn chords(&self) -> &[KeyChord] { &self.chords } pub fn len(&self) -> usize { self.chords.len() } pub fn is_empty(&self) -> bool { self.chords.is_empty() } pub fn starts_with(&self, other: &KeySequence) -> bool { self.chords.starts_with(other.chords()) } } ``` ### `src/input_pipeline/key_map.rs` ```rust // path_from_the_root: src/input_pipeline/key_map.rs use super::chord::KeyChord; use super::sequence::KeySequence; #[derive(Debug, Clone, PartialEq, Eq)] pub enum KeyMapEntry { Chord(KeyChord, Action), Sequence(KeySequence, Action), } impl KeyMapEntry { pub fn chord(chord: KeyChord, action: Action) -> Self { Self::Chord(chord, action) } pub fn sequence(sequence: KeySequence, action: Action) -> Self { Self::Sequence(sequence, action) } pub fn action(&self) -> &Action { match self { Self::Chord(_, action) | Self::Sequence(_, action) => action, } } } ``` ## Step 3: Response Types (no_std) ### `src/input_pipeline/response.rs` ```rust // path_from_the_root: src/input_pipeline/response.rs use super::chord::KeyChord; #[derive(Debug, Clone, PartialEq, Eq)] pub enum PipelineResponse { Execute(Action), Type(KeyChord), Wait(alloc::vec::Vec>), Cancel, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct InputHint { pub chord: KeyChord, pub action: Action, } ``` ## Step 4: Key Registry (alloc) ### `src/input_pipeline/key_registry.rs` ```rust // path_from_the_root: src/input_pipeline/key_registry.rs use super::chord::KeyChord; use super::sequence::KeySequence; use super::key_map::KeyMapEntry; #[derive(Debug, Clone)] pub struct KeyRegistry { chords: alloc::collections::HashMap, sequences: alloc::vec::Vec<(KeySequence, Action)>, } impl KeyRegistry { pub fn new() -> Self { Self { chords: alloc::collections::HashMap::new(), sequences: alloc::vec::Vec::new(), } } pub fn register_chord(&mut self, chord: KeyChord, action: Action) { self.chords.insert(chord, action); } pub fn register_sequence(&mut self, sequence: KeySequence, action: Action) { self.sequences.push((sequence, action)); } pub fn get_chord(&self, chord: &KeyChord) -> Option<&Action> { self.chords.get(chord) } pub fn find_sequences_starting_with( &self, prefix: &KeySequence, ) -> alloc::vec::Vec<&KeySequence> { self.sequences .iter() .filter(|(seq, _)| seq.starts_with(prefix)) .map(|(seq, _)| seq) .collect() } pub fn get_sequence(&self, sequence: &KeySequence) -> Option<&Action> { self.sequences .iter() .find(|(seq, _)| seq == sequence) .map(|(_, action)| action) } } impl Default for KeyRegistry { fn default() -> Self { Self::new() } } ``` ## Step 5: Sequence Tracker (optional, with std feature) ### `src/input_pipeline/sequence_tracker.rs` ```rust // path_from_the_root: src/input_pipeline/sequence_tracker.rs use super::sequence::KeySequence; #[derive(Debug, Clone)] pub struct SequenceTracker { current: KeySequence, #[cfg(feature = "std")] last_input: Option, #[cfg(feature = "std")] timeout: std::time::Duration, } impl SequenceTracker { pub fn new() -> Self { Self { current: KeySequence::new(), #[cfg(feature = "std")] last_input: None, #[cfg(feature = "std")] timeout: std::time::Duration::from_millis(1000), } } #[cfg(feature = "std")] pub fn with_timeout(timeout_ms: u64) -> Self { Self { current: KeySequence::new(), last_input: None, timeout: std::time::Duration::from_millis(timeout_ms), } } pub fn reset(&mut self) { self.current = KeySequence::new(); #[cfg(feature = "std")] { self.last_input = None; } } pub fn add(&mut self, chord: KeyChord) { self.current.push(chord); #[cfg(feature = "std")] { self.last_input = Some(std::time::Instant::now()); } } #[cfg(feature = "std")] pub fn is_expired(&self) -> bool { match self.last_input { None => false, Some(last) => last.elapsed() > self.timeout, } } #[cfg(not(feature = "std"))] pub fn is_expired(&self) -> bool { false } pub fn current(&self) -> &KeySequence { &self.current } pub fn is_empty(&self) -> bool { self.current.is_empty() } } impl Default for SequenceTracker { fn default() -> Self { Self::new() } } ``` ## Step 6: Pipeline Logic (no_std) ### `src/input_pipeline/pipeline.rs` ```rust // path_from_the_root: src/input_pipeline/pipeline.rs use super::chord::KeyChord; use super::key_registry::KeyRegistry; use super::sequence::KeySequence; use super::response::{PipelineResponse, InputHint}; use super::sequence_tracker::SequenceTracker; pub struct KeyPipeline { registry: KeyRegistry, #[cfg(feature = "std")] tracker: SequenceTracker, } impl KeyPipeline { pub fn new() -> Self { Self { registry: KeyRegistry::new(), #[cfg(feature = "std")] tracker: SequenceTracker::new(), } } pub fn register_chord(&mut self, chord: KeyChord, action: Action) { self.registry.register_chord(chord, action); } pub fn register_sequence(&mut self, sequence: KeySequence, action: Action) { self.registry.register_sequence(sequence, action); } pub fn process(&mut self, chord: KeyChord) -> PipelineResponse { #[cfg(feature = "std")] { if self.tracker.is_expired() { self.tracker.reset(); } } #[cfg(feature = "std")] if !self.tracker.is_empty() { self.tracker.add(chord); let current = self.tracker.current(); if let Some(action) = self.registry.get_sequence(current) { self.tracker.reset(); return PipelineResponse::Execute(action.clone()); } let matching = self.registry.find_sequences_starting_with(current); if matching.is_empty() { self.tracker.reset(); PipelineResponse::Cancel } else { let hints: alloc::vec::Vec> = matching .into_iter() .filter_map(|seq| { if seq.len() > current.len() { let next_chord = seq.chords()[current.len()]; self.registry.get_sequence(seq) .map(|action| InputHint { chord: next_chord, action: action.clone(), }) } else { None } }) .collect(); PipelineResponse::Wait(hints) } } else { if let Some(action) = self.registry.get_chord(&chord) { PipelineResponse::Execute(action.clone()) } else { let one_chord_seq = KeySequence::from_chords([chord]); let matching = self.registry.find_sequences_starting_with(&one_chord_seq); if !matching.is_empty() { #[cfg(feature = "std")] { self.tracker.add(chord); let hints: alloc::vec::Vec> = matching .into_iter() .filter_map(|seq| { if seq.len() > 1 { let next_chord = seq.chords()[1]; self.registry.get_sequence(seq) .map(|action| InputHint { chord: next_chord, action: action.clone(), }) } else { None } }) .collect(); PipelineResponse::Wait(hints) } #[cfg(not(feature = "std"))] { PipelineResponse::Cancel } } else { PipelineResponse::Type(chord) } } } } } impl Default for KeyPipeline { fn default() -> Self { Self::new() } } ``` ## Step 7: Module Routing ### `src/input_pipeline/mod.rs` ```rust // path_from_the_root: src/input_pipeline/mod.rs pub mod key; pub mod chord; pub mod sequence; pub mod key_map; pub mod key_registry; pub mod sequence_tracker; pub mod pipeline; pub mod response; pub use key::{KeyCode, KeyModifiers}; pub use chord::KeyChord; pub use sequence::KeySequence; pub use key_map::KeyMapEntry; pub use key_registry::KeyRegistry; pub use sequence_tracker::SequenceTracker; pub use pipeline::KeyPipeline; pub use response::{PipelineResponse, InputHint}; ``` ## Step 8: Update lib.rs ```rust // path_from_the_root: src/lib.rs #![no_std] extern crate alloc; pub mod input_pipeline; pub mod prelude { pub use crate::input_pipeline::*; } ``` ## Step 9: Update Cargo.toml ```toml [package] name = "tui_orchestrator" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" [features] default = ["std"] std = [] alloc = [] [dependencies] [dev-dependencies] ``` ## Step 10: Tests Create `tests/input_pipeline/` directory with tests for each module: ``` tests/input_pipeline/ ├── key_tests.rs ├── chord_tests.rs ├── sequence_tests.rs ├── registry_tests.rs └── pipeline_tests.rs ``` Example test file: ```rust // path_from_the_root: tests/input_pipeline/chord_tests.rs use tui_orchestrator::input_pipeline::{KeyChord, KeyCode, KeyModifiers}; #[test] fn test_chord_creation() { let chord = KeyChord::new( KeyCode::Char('a'), KeyModifiers::new().with_control(), ); assert_eq!(chord.code, KeyCode::Char('a')); assert!(chord.modifiers.control); } #[test] fn test_chord_display() { let chord = KeyChord::new( KeyCode::Char('a'), KeyModifiers::new().with_control().with_shift(), ); let display = chord.display_string(); assert!(display.contains("Ctrl+")); assert!(display.contains("Shift+")); assert!(display.contains('a')); } ``` ## Integration with komp_ac_client After migration, update `komp_ac_client/Cargo.toml`: ```toml [dependencies] tui_orchestrator = { path = "..", features = ["std"] } ``` Then in `komp_ac_client/src/input_pipeline/mod.rs`, replace with: ```rust // path_from_the_root: komp_ac_client/src/input_pipeline/mod.rs pub use tui_orchestrator::input_pipeline::*; // Add crossterm conversion trait use crossterm::event::{KeyCode, KeyModifiers as CrosstermModifiers}; impl From<&crossterm::event::KeyEvent> for KeyChord { fn from(event: &crossterm::event::KeyEvent) -> Self { let code = match event.code { KeyCode::Char(c) => KeyCode::Char(c), KeyCode::Enter => KeyCode::Enter, KeyCode::Tab => KeyCode::Tab, KeyCode::Esc => KeyCode::Esc, KeyCode::Backspace => KeyCode::Backspace, KeyCode::Delete => KeyCode::Delete, KeyCode::Home => KeyCode::Home, KeyCode::End => KeyCode::End, KeyCode::PageUp => KeyCode::PageUp, KeyCode::PageDown => KeyCode::PageDown, KeyCode::Up => KeyCode::Up, KeyCode::Down => KeyCode::Down, KeyCode::Left => KeyCode::Left, KeyCode::Right => KeyCode::Right, KeyCode::F(n) => KeyCode::F(n), KeyCode::Null => KeyCode::Null, }; let modifiers = KeyModifiers { control: event.modifiers.contains(CrosstermModifiers::CONTROL), alt: event.modifiers.contains(CrosstermModifiers::ALT), shift: event.modifiers.contains(CrosstermModifiers::SHIFT), }; KeyChord::new(code, modifiers) } } ``` Delete the migrated files from `komp_ac_client/src/input_pipeline/`. ## Benefits 1. **no_std compatible** - Works on embedded systems and WASM 2. **Backend agnostic** - No crossterm/ratatui dependency 3. **General purpose** - Any TUI can use this API 4. **Type safe** - Strong typing for key codes and modifiers 5. **Testable** - Pure functions, easy to unit test 6. **Flexible** - Applications define their own Action types ## Migration Checklist - [ ] Create `src/input_pipeline/key.rs` - [ ] Create `src/input_pipeline/chord.rs` - [ ] Create `src/input_pipeline/sequence.rs` - [ ] Create `src/input_pipeline/key_map.rs` - [ ] Create `src/input_pipeline/key_registry.rs` - [ ] Create `src/input_pipeline/sequence_tracker.rs` - [ ] Create `src/input_pipeline/pipeline.rs` - [ ] Create `src/input_pipeline/response.rs` - [ ] Create `src/input_pipeline/mod.rs` - [ ] Update `src/lib.rs` - [ ] Update `src/key/mod.rs` (remove old placeholders or convert) - [ ] Create tests in `tests/input_pipeline/` - [ ] Run tests: `cargo test --no-default-features` - [ ] Run tests with std: `cargo test` - [ ] Update komp_ac_client to use library - [ ] Delete migrated files from komp_ac_client - [ ] Run komp_ac_client tests