tastty-core 0.1.0

Sans-IO core of the tastty terminal session library: VT parser, screen buffer, and byte encoders.
use super::*;

#[test]
fn mode_2027_set_reset_query() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);

    // Initially reset (2 = not set)
    process(&mut parser, b"\x1b[?2027$p");
    assert_eq!(
        drain_replies(&mut parser),
        b"\x1b[?2027;2$y",
        "mode 2027 must report 'reset' (state 2) before any explicit set",
    );

    // Enable
    process(&mut parser, b"\x1b[?2027h");
    process(&mut parser, b"\x1b[?2027$p");
    assert_eq!(
        drain_replies(&mut parser),
        b"\x1b[?2027;1$y",
        "after DECSET 2027 the DECRQM reply must report 'set' (state 1)",
    );

    // Disable
    process(&mut parser, b"\x1b[?2027l");
    process(&mut parser, b"\x1b[?2027$p");
    assert_eq!(
        drain_replies(&mut parser),
        b"\x1b[?2027;2$y",
        "after DECRST 2027 the DECRQM reply must report 'reset' (state 2)",
    );
}

#[test]
fn mode_2027_reset_by_ris() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?2027h");
    process(&mut parser, b"\x1bc"); // RIS
    process(&mut parser, b"\x1b[?2027$p");
    assert_eq!(drain_replies(&mut parser), b"\x1b[?2027;2$y");
}

#[test]
fn grapheme_combining_accent() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?2027h");
    // e + combining acute accent = single grapheme cluster, width 1
    process(&mut parser, "e\u{0301}X".as_bytes());
    let screen = parser.screen();
    assert_eq!(screen.cell(0, 0).unwrap().contents(), "e\u{0301}");
    assert!(!screen.cell(0, 0).unwrap().is_wide());
    assert_eq!(screen.cell(0, 1).unwrap().contents(), "X");
    assert_eq!(screen.cursor(), Position { row: 0, col: 2 });
}

#[test]
fn grapheme_zwj_family_emoji() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?2027h");
    // 👨‍👩‍👧‍👦 (family emoji ZWJ sequence) = single cluster, width 2
    let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}";
    process(&mut parser, format!("{family}X").as_bytes());
    let screen = parser.screen();
    let c0 = screen.cell(0, 0).unwrap();
    assert!(c0.is_wide(), "ZWJ family should be wide");
    assert!(
        screen.cell(0, 1).unwrap().is_wide_continuation(),
        "col 1 should be continuation"
    );
    assert_eq!(screen.cell(0, 2).unwrap().contents(), "X");
}

#[test]
fn grapheme_vs16_emoji() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?2027h");
    // ☃️ = U+2603 + U+FE0F (VS16) = single cluster, width 2
    process(&mut parser, "\u{2603}\u{FE0F}X".as_bytes());
    let screen = parser.screen();
    let c0 = screen.cell(0, 0).unwrap();
    assert!(c0.is_wide(), "snowman+VS16 should be wide");
    assert!(c0.contents().contains('\u{FE0F}'));
    assert!(screen.cell(0, 1).unwrap().is_wide_continuation());
    assert_eq!(screen.cell(0, 2).unwrap().contents(), "X");
}

#[test]
fn grapheme_vs15_text_presentation() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?2027h");
    // ☃︎ = U+2603 + U+FE0E (VS15) = text presentation, width 1
    process(&mut parser, "\u{2603}\u{FE0E}X".as_bytes());
    let screen = parser.screen();
    let c0 = screen.cell(0, 0).unwrap();
    assert!(!c0.is_wide(), "snowman+VS15 should NOT be wide");
    assert_eq!(screen.cell(0, 1).unwrap().contents(), "X");
}

#[test]
fn grapheme_flag_emoji() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?2027h");
    // 🇩🇪 = U+1F1E9 + U+1F1EA (regional indicators) = single cluster, width 2
    process(&mut parser, "\u{1F1E9}\u{1F1EA}X".as_bytes());
    let screen = parser.screen();
    let c0 = screen.cell(0, 0).unwrap();
    assert!(c0.is_wide(), "flag emoji should be wide");
    assert!(screen.cell(0, 1).unwrap().is_wide_continuation());
    assert_eq!(screen.cell(0, 2).unwrap().contents(), "X");
}

#[test]
fn grapheme_skin_tone() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?2027h");
    // 👋🏽 = U+1F44B + U+1F3FD (wave + medium skin tone) = single cluster, width 2
    process(&mut parser, "\u{1F44B}\u{1F3FD}X".as_bytes());
    let screen = parser.screen();
    let c0 = screen.cell(0, 0).unwrap();
    assert!(c0.is_wide(), "skin-toned wave should be wide");
    assert!(screen.cell(0, 1).unwrap().is_wide_continuation());
    assert_eq!(screen.cell(0, 2).unwrap().contents(), "X");
}

#[test]
fn grapheme_mode_off_no_regression() {
    // Without mode 2027, behavior should be identical to before
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, "Hi".as_bytes());
    let screen = parser.screen();
    assert_eq!(screen.cell(0, 0).unwrap().contents(), "H");
    assert_eq!(screen.cell(0, 1).unwrap().contents(), "i");
    assert_eq!(screen.cursor(), Position { row: 0, col: 2 });
}

#[test]
fn grapheme_flush_on_control() {
    // Buffered chars should flush when a control code arrives
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?2027h");
    process(&mut parser, b"AB\rX");
    let screen = parser.screen();
    // A at col 0, B at col 1, then CR returns to col 0, X overwrites A
    assert_eq!(screen.cell(0, 0).unwrap().contents(), "X");
    assert_eq!(screen.cell(0, 1).unwrap().contents(), "B");
}