inkferro-rt 0.1.0

Frame composition and diff runtime for inkferro, the Rust-backed, ink-compatible terminal UI renderer.
Documentation
//! ANSI escape builders, byte-for-byte matching the `ansi-escapes` npm package
//! (`ink/node_modules/ansi-escapes/base.js`) for the small subset the
//! incremental line-diff transport uses.
//!
//! Provenance: every sequence below was captured from the oracle
//! (`node` against `ansi-escapes`) inside the ink repo, not written from
//! memory. `ESC` is `[` (CSI).
//!
//! Captured values:
//! - `cursorUp(0)` -> "", `cursorUp(1)` -> "", ...
//! - `cursorTo(0)` -> "" (1-based column)
//! - `cursorNextLine` -> ""
//! - `eraseEndLine` -> ""
//! - `eraseLines(0)` -> "", `eraseLines(1)` -> "",
//!   `eraseLines(2)` -> "", ...

/// CSI introducer: `ESC [`.
const ESC: &str = "\u{001B}[";

/// `ansiEscapes.cursorUp(count)` = `ESC + count + 'A'`.
#[must_use]
pub fn cursor_up(count: usize) -> String {
    format!("{ESC}{count}A")
}

/// `ansiEscapes.cursorDown(count)` = `ESC + count + 'B'`.
///
/// Used by the cursor transport's `buildReturnToBottom`
/// (`ink/src/cursor-helpers.ts:56-57`: `cursorDown(down)` when `down > 0`) to
/// walk the terminal cursor from a previously shown cursor position back down to
/// the bottom of the rendered frame before an erase or repaint. Oracle capture:
/// `cursorDown(1)` -> "[1B", `cursorDown(2)` -> "[2B".
#[must_use]
pub fn cursor_down(count: usize) -> String {
    format!("{ESC}{count}B")
}

/// `showCursorEscape` (`ink/src/cursor-helpers.ts:8`) = `ESC + '?25h'`.
/// Appended by `buildCursorSuffix` after repositioning a shown cursor.
pub const SHOW_CURSOR: &str = "\u{001B}[?25h";

/// `hideCursorEscape` (`ink/src/cursor-helpers.ts:9`) = `ESC + '?25l'`.
/// Emitted by `buildReturnToBottomPrefix`/`buildCursorOnlySequence` (hide-before-
/// move) and by `sync`'s `!activeCursor && cursorWasShown` clear branch.
pub const HIDE_CURSOR: &str = "\u{001B}[?25l";

/// `ansiEscapes.cursorTo(x)` for `y` omitted = `ESC + (x + 1) + 'G'`.
/// The transport only ever uses column 0.
#[must_use]
pub fn cursor_to(x: usize) -> String {
    format!("{ESC}{}G", x + 1)
}

/// `ansiEscapes.cursorNextLine` = `ESC + 'E'`.
#[must_use]
pub fn cursor_next_line() -> &'static str {
    "\u{001B}[E"
}

/// `ansiEscapes.eraseEndLine` = `ESC + 'K'`.
#[must_use]
pub fn erase_end_line() -> &'static str {
    "\u{001B}[K"
}

/// `ansiEscapes.eraseLines(count)`: for each of `count` lines, emit
/// `eraseLine` (`ESC + '2K'`), and between lines (all but the last) a
/// `cursorUp()` (`ESC + '1A'`); after the loop, when `count > 0`, append
/// `cursorLeft` (`ESC + 'G'`). `count == 0` yields `""`.
#[must_use]
pub fn erase_lines(count: usize) -> String {
    let mut out = String::new();
    for i in 0..count {
        out.push_str("\u{001B}[2K");
        if i < count - 1 {
            out.push_str("\u{001B}[1A");
        }
    }
    if count > 0 {
        out.push_str("\u{001B}[G");
    }
    out
}