735 lines
19 KiB
Markdown
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
|