Files
pages-tui/INPUT_PIPELINE_MIGRATION.md

735 lines
19 KiB
Markdown

# 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<KeyCode> 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<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`
```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<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`
```rust
// 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`
```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<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`
```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<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`
```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<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`
```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