inkferro_core/input/mod.rs
1//! Terminal input parsing: a pure bytes-in / events-out state machine.
2//!
3//! This is a 1:1 port of ink's two-stage terminal input pipeline:
4//!
5//! - The (private) `segmenter` module ports `input-parser.ts` — the stateful
6//! segmentation layer that splits a raw byte stream into opaque key/text byte
7//! runs and decoded bracketed-paste payloads, with partial-sequence buffering
8//! across chunks. It is an internal detail wrapped by [`Parser`].
9//! - [`keypress`] ports `parse-keypress.ts` (plus the kitty-protocol decoding
10//! from `kitty-keyboard.ts`) — the pure function that decodes one key
11//! sequence into a [`Key`], trying the kitty CSI-u and kitty-enhanced
12//! special-key parsers **first**, then the legacy enquirer-derived table.
13//!
14//! There is **no I/O and no stream coupling**: callers feed bytes and receive a
15//! `Vec<InputEvent>`. This mirrors the future napi `push_input(bytes) ->
16//! Vec<InputEvent>` surface.
17//!
18//! # Layering rationale
19//!
20//! The two ink files are genuinely different layers and are tested separately
21//! upstream: `input-parser.ts` asserts raw segment strings (`['a', "[A",
22//! 'b']`, `[{paste: 'hello'}]`); `parse-keypress.ts` and the kitty tests assert
23//! `Key` fields. Keeping the segmenter and [`parse_keypress`] as distinct,
24//! independently testable units lets both upstream test suites be ported
25//! verbatim. [`Parser`] composes them for the napi consumer.
26
27pub mod keypress;
28mod kitty;
29mod segmenter;
30
31pub use keypress::{EventType, Key, parse_keypress};
32use segmenter::{Segment, Segmenter};
33
34/// A high-level input event handed to the host (the napi `push_input`
35/// consumer). Mirrors ink's `InputEvent` (`Key` from `parseKeypress`, plus the
36/// `{paste}` arm), composed from the two ported layers.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum InputEvent {
39 /// A decoded key/text segment.
40 Key(Key),
41 /// A bracketed-paste payload, delivered verbatim as raw bytes.
42 Paste(Vec<u8>),
43}
44
45/// The public input parser: bytes in, [`InputEvent`]s out, with partial-sequence
46/// buffering held across [`feed`](Parser::feed) calls.
47#[derive(Debug, Default, Clone)]
48pub struct Parser {
49 segmenter: Segmenter,
50}
51
52impl Parser {
53 pub fn new() -> Self {
54 Parser::default()
55 }
56
57 /// Feed a chunk of raw terminal bytes; returns the input events decided so
58 /// far. Each non-paste segment is decoded through [`parse_keypress`]; paste
59 /// payloads pass through verbatim. Trailing undecided bytes are buffered for
60 /// the next call.
61 pub fn feed(&mut self, bytes: &[u8]) -> Vec<InputEvent> {
62 self.segmenter
63 .push(bytes)
64 .into_iter()
65 .map(|segment| match segment {
66 Segment::Bytes(raw) => InputEvent::Key(parse_keypress(&raw)),
67 Segment::Paste(payload) => InputEvent::Paste(payload),
68 })
69 .collect()
70 }
71
72 /// Whether a bare/partial escape is buffered (host escape-flush timer hook).
73 pub fn has_pending_escape(&self) -> bool {
74 self.segmenter.has_pending_escape()
75 }
76
77 /// Take the buffered escape bytes as literal input, or `None` if no escape
78 /// is pending.
79 pub fn flush_pending_escape(&mut self) -> Option<Vec<u8>> {
80 self.segmenter.flush_pending_escape()
81 }
82
83 /// Clear pending input state.
84 pub fn reset(&mut self) {
85 self.segmenter.reset();
86 }
87}
88
89#[cfg(test)]
90mod tests;