Files
pages-tui/INPUT_PIPELINE_MIGRATION.md

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::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:

// 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

  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