inkferro-core 0.1.0

Layout, text measurement, ANSI render, and frame-diff engine for inkferro — a Rust-backed, byte-for-byte drop-in for the ink terminal UI library.
Documentation
//! Terminal input parsing: a pure bytes-in / events-out state machine.
//!
//! This is a 1:1 port of ink's two-stage terminal input pipeline:
//!
//! - The (private) `segmenter` module ports `input-parser.ts` — the stateful
//!   segmentation layer that splits a raw byte stream into opaque key/text byte
//!   runs and decoded bracketed-paste payloads, with partial-sequence buffering
//!   across chunks. It is an internal detail wrapped by [`Parser`].
//! - [`keypress`] ports `parse-keypress.ts` (plus the kitty-protocol decoding
//!   from `kitty-keyboard.ts`) — the pure function that decodes one key
//!   sequence into a [`Key`], trying the kitty CSI-u and kitty-enhanced
//!   special-key parsers **first**, then the legacy enquirer-derived table.
//!
//! There is **no I/O and no stream coupling**: callers feed bytes and receive a
//! `Vec<InputEvent>`. This mirrors the future napi `push_input(bytes) ->
//! Vec<InputEvent>` surface.
//!
//! # Layering rationale
//!
//! The two ink files are genuinely different layers and are tested separately
//! upstream: `input-parser.ts` asserts raw segment strings (`['a', "",
//! 'b']`, `[{paste: 'hello'}]`); `parse-keypress.ts` and the kitty tests assert
//! `Key` fields. Keeping the segmenter and [`parse_keypress`] as distinct,
//! independently testable units lets both upstream test suites be ported
//! verbatim. [`Parser`] composes them for the napi consumer.

pub mod keypress;
mod kitty;
mod segmenter;

pub use keypress::{EventType, Key, parse_keypress};
use segmenter::{Segment, Segmenter};

/// A high-level input event handed to the host (the napi `push_input`
/// consumer). Mirrors ink's `InputEvent` (`Key` from `parseKeypress`, plus the
/// `{paste}` arm), composed from the two ported layers.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputEvent {
    /// A decoded key/text segment.
    Key(Key),
    /// A bracketed-paste payload, delivered verbatim as raw bytes.
    Paste(Vec<u8>),
}

/// The public input parser: bytes in, [`InputEvent`]s out, with partial-sequence
/// buffering held across [`feed`](Parser::feed) calls.
#[derive(Debug, Default, Clone)]
pub struct Parser {
    segmenter: Segmenter,
}

impl Parser {
    pub fn new() -> Self {
        Parser::default()
    }

    /// Feed a chunk of raw terminal bytes; returns the input events decided so
    /// far. Each non-paste segment is decoded through [`parse_keypress`]; paste
    /// payloads pass through verbatim. Trailing undecided bytes are buffered for
    /// the next call.
    pub fn feed(&mut self, bytes: &[u8]) -> Vec<InputEvent> {
        self.segmenter
            .push(bytes)
            .into_iter()
            .map(|segment| match segment {
                Segment::Bytes(raw) => InputEvent::Key(parse_keypress(&raw)),
                Segment::Paste(payload) => InputEvent::Paste(payload),
            })
            .collect()
    }

    /// Whether a bare/partial escape is buffered (host escape-flush timer hook).
    pub fn has_pending_escape(&self) -> bool {
        self.segmenter.has_pending_escape()
    }

    /// Take the buffered escape bytes as literal input, or `None` if no escape
    /// is pending.
    pub fn flush_pending_escape(&mut self) -> Option<Vec<u8>> {
        self.segmenter.flush_pending_escape()
    }

    /// Clear pending input state.
    pub fn reset(&mut self) {
        self.segmenter.reset();
    }
}

#[cfg(test)]
mod tests;