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
//! Composition tests for the public [`Parser`]: it threads the segmenter into
//! `parse_keypress`, emitting [`InputEvent`]s. These verify the two ported
//! layers wire together as the napi `push_input(bytes) -> Vec<InputEvent>`
//! surface expects; the per-layer behavior is exhaustively pinned in
//! `segmenter::tests` and `keypress::tests`.

use super::*;

#[test]
fn feed_decodes_plain_key_to_input_event_key() {
    let mut parser = Parser::new();
    let events = parser.feed(b"a");
    assert_eq!(events.len(), 1);
    match &events[0] {
        InputEvent::Key(key) => assert_eq!(key.name, "a"),
        other => panic!("expected key, got {other:?}"),
    }
}

#[test]
fn feed_decodes_csi_arrow_to_named_key() {
    let mut parser = Parser::new();
    let events = parser.feed(b"\x1b[A");
    assert_eq!(events.len(), 1);
    match &events[0] {
        InputEvent::Key(key) => {
            assert_eq!(key.name, "up");
            assert!(!key.is_kitty_protocol);
        }
        other => panic!("expected key, got {other:?}"),
    }
}

#[test]
fn feed_decodes_kitty_csi_u_first() {
    let mut parser = Parser::new();
    let events = parser.feed(b"\x1b[97;5u");
    assert_eq!(events.len(), 1);
    match &events[0] {
        InputEvent::Key(key) => {
            assert_eq!(key.name, "a");
            assert!(key.ctrl);
            assert!(key.is_kitty_protocol);
        }
        other => panic!("expected key, got {other:?}"),
    }
}

#[test]
fn feed_emits_paste_event_verbatim() {
    let mut parser = Parser::new();
    let events = parser.feed(b"\x1b[200~hello world\x1b[201~");
    assert_eq!(events, vec![InputEvent::Paste(b"hello world".to_vec())]);
}

#[test]
fn feed_interleaves_text_paste_and_keys() {
    let mut parser = Parser::new();
    let events = parser.feed(b"before\x1b[200~pasted\x1b[201~after");
    assert_eq!(events.len(), 3);
    assert!(matches!(&events[0], InputEvent::Key(k) if k.sequence == "before"));
    assert_eq!(events[1], InputEvent::Paste(b"pasted".to_vec()));
    assert!(matches!(&events[2], InputEvent::Key(k) if k.sequence == "after"));
}

#[test]
fn feed_buffers_partial_sequence_across_calls() {
    let mut parser = Parser::new();
    assert_eq!(parser.feed(b"\x1b["), vec![]);
    assert!(parser.has_pending_escape());
    let events = parser.feed(b"1;5A");
    assert_eq!(events.len(), 1);
    match &events[0] {
        InputEvent::Key(key) => {
            assert_eq!(key.name, "up");
            assert!(key.ctrl);
        }
        other => panic!("expected key, got {other:?}"),
    }
}

#[test]
fn feed_splits_batched_backspace_into_individual_key_events() {
    let mut parser = Parser::new();
    let events = parser.feed(b"\x7f\x7f");
    assert_eq!(events.len(), 2);
    for event in &events {
        match event {
            InputEvent::Key(key) => assert_eq!(key.name, "backspace"),
            other => panic!("expected key, got {other:?}"),
        }
    }
}

#[test]
fn flush_pending_escape_returns_buffered_bytes() {
    let mut parser = Parser::new();
    assert_eq!(parser.feed(b"\x1b"), vec![]);
    assert_eq!(parser.flush_pending_escape(), Some(b"\x1b".to_vec()));
    assert_eq!(parser.flush_pending_escape(), None);
}

#[test]
fn reset_clears_pending_state() {
    let mut parser = Parser::new();
    assert_eq!(parser.feed(b"\x1b["), vec![]);
    parser.reset();
    let events = parser.feed(b"A");
    assert_eq!(events.len(), 1);
    assert!(matches!(&events[0], InputEvent::Key(k) if k.name == "a"));
}

/// Regression guard for task #53 (cold-start paste-decode "flake"). The OS
/// splits stdin reads at arbitrary byte boundaries, so a bracketed paste can
/// arrive in fragments across successive `push_input` -> `Parser::feed` calls.
/// Decoding MUST yield exactly one verbatim `Paste` no matter where the boundary
/// falls — inside the `\x1b[200~` opener, inside the body (control bytes, a lone
/// ESC, a multibyte codepoint), or inside the `\x1b[201~` closer. Every other
/// paste pin feeds ONE atomic chunk; this is the cross-chunk state machine the
/// #53 report implicated. The diagnosis proved it deterministic across 500+
/// trials (cold/warm, real async streams, byte-by-byte) — this locks that in so
/// the "is it flaky?" question is never re-litigated.
#[test]
fn feed_decodes_paste_across_every_fragmentation_boundary() {
    const START: &[u8] = b"\x1b[200~";
    const END: &[u8] = b"\x1b[201~";
    // Bodies chosen to stress the accumulator: plain, carriage returns, a lone
    // ESC + a Ctrl-C byte (must NOT be re-parsed as keys mid-paste), and a 4-byte
    // UTF-8 codepoint (boundary can fall mid-codepoint — the payload is raw bytes).
    let bodies: [&[u8]; 4] = [
        b"hello world",
        b"line1\rline2\r",
        b"esc\x1bmore\x03ctl",
        b"rocket \xf0\x9f\x9a\x80 end",
    ];

    for body in bodies {
        let mut seq = Vec::new();
        seq.extend_from_slice(START);
        seq.extend_from_slice(body);
        seq.extend_from_slice(END);
        let expected = vec![InputEvent::Paste(body.to_vec())];

        // Every 2-way split boundary (1..len; 0 and len are the atomic case).
        for split in 1..seq.len() {
            let mut parser = Parser::new();
            let mut events = parser.feed(&seq[..split]);
            events.extend(parser.feed(&seq[split..]));
            assert_eq!(
                events, expected,
                "2-way split at {split} for body {body:?} did not decode to one verbatim paste"
            );
        }

        // Worst case: one byte per feed.
        let mut parser = Parser::new();
        let mut events = Vec::new();
        for &byte in &seq {
            events.extend(parser.feed(&[byte]));
        }
        assert_eq!(
            events, expected,
            "byte-by-byte feed for body {body:?} did not decode to one verbatim paste"
        );
    }
}