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
//! Port of `diff.js` from `@alcalzone/ansi-tokenize@0.3.0`.

use super::ansi_codes::is_intensity_code;
use super::types::AnsiToken;
use super::undo::undo_ansi_codes;

/// Returns the minimum amount of ANSI codes necessary to transition from style
/// `from` to style `to`. Both `from` and `to` are expected to be already reduced.
///
/// Equivalent to `diffAnsiCodes(from, to)` in the JS source. The JS builds
/// three `Set`s per call; style stacks hold a handful of entries, so linear
/// scans over the slices are cheaper than hashing (same reasoning as
/// `ansi_codes_to_string`) — this runs once per character transition in
/// `styled_chars_to_string`.
pub fn diff_ansi_codes(from: &[AnsiToken], to: &[AnsiToken]) -> Vec<AnsiToken> {
    // Codes in `from` that need to be disabled
    let to_undo: Vec<AnsiToken> = from
        .iter()
        .filter(|c| {
            if is_intensity_code(c) {
                // Intensity: check by start code
                !to.iter().any(|t| t.code == c.code)
            } else {
                // Regular: check by end code
                !to.iter().any(|t| t.end_code == c.end_code)
            }
        })
        .cloned()
        .collect();

    // Codes in `to` that are not yet in `from`
    let to_add = to
        .iter()
        .filter(|c| !from.iter().any(|f| f.code == c.code))
        .cloned();

    let mut result = undo_ansi_codes(&to_undo);
    result.extend(to_add);
    result
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::text::ansi_tokenize::ansi_codes::get_end_code;

    fn tok(code: &str) -> AnsiToken {
        AnsiToken {
            end_code: get_end_code(code),
            code: code.into(),
        }
    }

    // Test 12: diffAnsiCodes computes correct minimal diff
    #[test]
    fn diff_add_new_code() {
        // from: nothing → to: red; diff should be [red]
        let diff = diff_ansi_codes(&[], &[tok("\x1B[31m")]);
        assert_eq!(diff.len(), 1);
        assert_eq!(diff[0].code, "\x1B[31m");
    }

    #[test]
    fn diff_remove_code() {
        // from: red → to: nothing; diff should be [39m] (undo red)
        let diff = diff_ansi_codes(&[tok("\x1B[31m")], &[]);
        assert_eq!(diff.len(), 1);
        assert_eq!(diff[0].code, "\x1B[39m");
    }

    #[test]
    fn diff_change_code() {
        // from: red → to: blue
        // Both share end_code 39m, so red is NOT in to_undo (blue's 39m covers it).
        // Blue is not in start_codes_in_from, so it is added.
        // Minimal diff = just [blue] — the terminal applies blue which overwrites red.
        let diff = diff_ansi_codes(&[tok("\x1B[31m")], &[tok("\x1B[34m")]);
        assert_eq!(diff.len(), 1);
        assert_eq!(diff[0].code, "\x1B[34m");
    }

    #[test]
    fn diff_change_code_with_extra_style() {
        // from: red + bold → to: blue
        // bold (end=22m) is NOT covered by to's end_codes ({39m}) and is intensity →
        // checked by start code: 1m not in start_codes_in_to({34m}) → to_undo=[bold]
        // Result: [22m (undo bold), 34m (add blue)]
        let diff = diff_ansi_codes(&[tok("\x1B[31m"), tok("\x1B[1m")], &[tok("\x1B[34m")]);
        let codes: Vec<&str> = diff.iter().map(|c| c.code.as_str()).collect();
        assert!(codes.contains(&"\x1B[22m"), "should undo bold");
        assert!(codes.contains(&"\x1B[34m"), "should add blue");
        assert!(
            !codes.contains(&"\x1B[39m"),
            "39m is unnecessary — blue covers red"
        );
    }

    #[test]
    fn diff_same_codes_is_empty() {
        // from: red → to: red; diff should be empty (nothing to do)
        let diff = diff_ansi_codes(&[tok("\x1B[31m")], &[tok("\x1B[31m")]);
        assert!(diff.is_empty());
    }

    #[test]
    fn diff_intensity_preserved() {
        // from: bold → to: bold + italic; diff should only add italic
        let diff = diff_ansi_codes(&[tok("\x1B[1m")], &[tok("\x1B[1m"), tok("\x1B[3m")]);
        assert_eq!(diff.len(), 1);
        assert_eq!(diff[0].code, "\x1B[3m");
    }
}