19 KiB
19 KiB
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::HashMappipeline.rs- Pure logicresponse.rs- Pure types
Dependencies to remove:
crossterm::event- Replace with custom typesstd::time- Replace with alloc-based solution or make optionalstd::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:
// 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:
// 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<KeyCode> for KeyChord {
fn from(code: KeyCode) -> Self {
Self {
code,
modifiers: KeyModifiers::new(),
}
}
}
Step 2: Sequence Types (no_std)
src/input_pipeline/sequence.rs
// 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<KeyChord>,
}
impl KeySequence {
pub fn new() -> Self {
Self {
chords: alloc::vec::Vec::new(),
}
}
pub fn from_chords(chords: impl IntoIterator<Item = KeyChord>) -> 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
// 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<Action> {
Chord(KeyChord, Action),
Sequence(KeySequence, Action),
}
impl<Action: Clone> KeyMapEntry<Action> {
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
// path_from_the_root: src/input_pipeline/response.rs
use super::chord::KeyChord;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PipelineResponse<Action> {
Execute(Action),
Type(KeyChord),
Wait(alloc::vec::Vec<InputHint<Action>>),
Cancel,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InputHint<Action> {
pub chord: KeyChord,
pub action: Action,
}
Step 4: Key Registry (alloc)
src/input_pipeline/key_registry.rs
// 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<Action> {
chords: alloc::collections::HashMap<KeyChord, Action>,
sequences: alloc::vec::Vec<(KeySequence, Action)>,
}
impl<Action: Clone> KeyRegistry<Action> {
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<Action: Clone> Default for KeyRegistry<Action> {
fn default() -> Self {
Self::new()
}
}
Step 5: Sequence Tracker (optional, with std feature)
src/input_pipeline/sequence_tracker.rs
// 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<std::time::Instant>,
#[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
// 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<Action: Clone> {
registry: KeyRegistry<Action>,
#[cfg(feature = "std")]
tracker: SequenceTracker,
}
impl<Action: Clone> KeyPipeline<Action> {
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<Action> {
#[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<InputHint<Action>> = 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<InputHint<Action>> = 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<Action: Clone> Default for KeyPipeline<Action> {
fn default() -> Self {
Self::new()
}
}
Step 7: Module Routing
src/input_pipeline/mod.rs
// 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
// 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
[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:
// 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:
[dependencies]
tui_orchestrator = { path = "..", features = ["std"] }
Then in komp_ac_client/src/input_pipeline/mod.rs, replace with:
// 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
- no_std compatible - Works on embedded systems and WASM
- Backend agnostic - No crossterm/ratatui dependency
- General purpose - Any TUI can use this API
- Type safe - Strong typing for key codes and modifiers
- Testable - Pure functions, easy to unit test
- 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