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 hide_cursor_mode() {
    let mut screen = make_screen(24, 80);
    assert!(!screen.mode(TerminalMode::HideCursor));
    screen.set_mode(MODE_HIDE_CURSOR);
    assert!(screen.mode(TerminalMode::HideCursor));
    screen.clear_mode(MODE_HIDE_CURSOR);
    assert!(!screen.mode(TerminalMode::HideCursor));
}

#[test]
fn alternate_screen() {
    let mut screen = make_screen(24, 80);
    screen.text('A');
    screen.enter_alternate_grid();
    assert!(screen.mode(TerminalMode::AlternateScreen));
    assert!(!screen.cell(0, 0).unwrap().has_contents());
    screen.exit_alternate_grid();
    assert!(!screen.mode(TerminalMode::AlternateScreen));
    assert_eq!(screen.cell(0, 0).unwrap().contents(), "A");
}

#[test]
fn application_cursor_mode() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    assert!(!parser.screen().mode(TerminalMode::ApplicationCursor));

    // DECSET 1 enables application cursor
    process(&mut parser, b"\x1b[?1h");
    assert!(parser.screen().mode(TerminalMode::ApplicationCursor));

    // DECRST 1 disables it
    process(&mut parser, b"\x1b[?1l");
    assert!(!parser.screen().mode(TerminalMode::ApplicationCursor));
}

#[test]
fn bracketed_paste_mode() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    assert!(!parser.screen().mode(TerminalMode::BracketedPaste));

    // DECSET 2004 enables bracketed paste
    process(&mut parser, b"\x1b[?2004h");
    assert!(parser.screen().mode(TerminalMode::BracketedPaste));

    // DECRST 2004 disables it
    process(&mut parser, b"\x1b[?2004l");
    assert!(!parser.screen().mode(TerminalMode::BracketedPaste));
}

#[test]
fn ris_clears_modes() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?1h\x1b[?2004h");
    assert!(parser.screen().mode(TerminalMode::ApplicationCursor));
    assert!(parser.screen().mode(TerminalMode::BracketedPaste));

    // RIS (full reset) should clear all modes
    process(&mut parser, b"\x1bc");
    assert!(!parser.screen().mode(TerminalMode::ApplicationCursor));
    assert!(!parser.screen().mode(TerminalMode::BracketedPaste));
}

#[test]
fn focus_and_sync_flags() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    assert!(!parser.screen().mode(TerminalMode::FocusInOut));
    assert!(!parser.screen().mode(TerminalMode::SyncUpdate));
    process(&mut parser, b"\x1b[?1004h\x1b[?2026h");
    assert!(parser.screen().mode(TerminalMode::FocusInOut));
    assert!(parser.screen().mode(TerminalMode::SyncUpdate));
    process(&mut parser, b"\x1b[?1004l\x1b[?2026l");
    assert!(!parser.screen().mode(TerminalMode::FocusInOut));
    assert!(!parser.screen().mode(TerminalMode::SyncUpdate));
}

#[test]
fn decscnm_reverse_video_set_and_clear() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    assert!(!parser.screen().mode(TerminalMode::ReverseVideo));

    process(&mut parser, b"\x1b[?5h");
    assert!(parser.screen().mode(TerminalMode::ReverseVideo));

    process(&mut parser, b"\x1b[?5l");
    assert!(!parser.screen().mode(TerminalMode::ReverseVideo));
}

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

    process(&mut parser, b"\x1b[?5h");
    let events = parser.screen_mut().drain_events();
    assert!(
        events.contains(&ScreenEvent::ModeChanged {
            mode: TerminalMode::ReverseVideo,
            enabled: true,
        }),
        "DECSET 5 must emit ModeChanged{{ReverseVideo, true}}; got {events:?}",
    );

    process(&mut parser, b"\x1b[?5l");
    let events = parser.screen_mut().drain_events();
    assert!(
        events.contains(&ScreenEvent::ModeChanged {
            mode: TerminalMode::ReverseVideo,
            enabled: false,
        }),
        "DECRST 5 must emit ModeChanged{{ReverseVideo, false}}; got {events:?}",
    );
}

#[test]
fn decscnm_reverse_video_idempotent_no_event() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?5h");
    parser.screen_mut().drain_events();

    process(&mut parser, b"\x1b[?5h");
    let events = parser.screen_mut().drain_events();
    assert!(
        !events.iter().any(|e| matches!(
            e,
            ScreenEvent::ModeChanged {
                mode: TerminalMode::ReverseVideo,
                ..
            }
        )),
        "redundant DECSET 5 must not emit a ModeChanged event; got {events:?}",
    );
}

#[test]
fn decset_decbkm_set_and_clear() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    assert!(!parser.screen().mode(TerminalMode::BackspaceBs));

    process(&mut parser, b"\x1b[?67h");
    assert!(parser.screen().mode(TerminalMode::BackspaceBs));

    process(&mut parser, b"\x1b[?67l");
    assert!(!parser.screen().mode(TerminalMode::BackspaceBs));
}

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

    process(&mut parser, b"\x1b[?67h");
    let events = parser.screen_mut().drain_events();
    assert!(
        events.contains(&ScreenEvent::ModeChanged {
            mode: TerminalMode::BackspaceBs,
            enabled: true,
        }),
        "DECSET 67 must emit ModeChanged{{BackspaceBs, true}}; got {events:?}",
    );

    process(&mut parser, b"\x1b[?67l");
    let events = parser.screen_mut().drain_events();
    assert!(
        events.contains(&ScreenEvent::ModeChanged {
            mode: TerminalMode::BackspaceBs,
            enabled: false,
        }),
        "DECRST 67 must emit ModeChanged{{BackspaceBs, false}}; got {events:?}",
    );
}

#[test]
fn decset_decbkm_idempotent_no_event() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?67h");
    parser.screen_mut().drain_events();

    process(&mut parser, b"\x1b[?67h");
    let events = parser.screen_mut().drain_events();
    assert!(
        !events.iter().any(|e| matches!(
            e,
            ScreenEvent::ModeChanged {
                mode: TerminalMode::BackspaceBs,
                ..
            }
        )),
        "redundant DECSET 67 must not emit a ModeChanged event; got {events:?}",
    );
}

#[test]
fn lnm_set_and_clear() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    assert!(!parser.screen().mode(TerminalMode::LineFeedNewLine));

    process(&mut parser, b"\x1b[20h");
    assert!(parser.screen().mode(TerminalMode::LineFeedNewLine));

    process(&mut parser, b"\x1b[20l");
    assert!(!parser.screen().mode(TerminalMode::LineFeedNewLine));
}

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

    process(&mut parser, b"\x1b[20h");
    let events = parser.screen_mut().drain_events();
    assert!(
        events.contains(&ScreenEvent::ModeChanged {
            mode: TerminalMode::LineFeedNewLine,
            enabled: true,
        }),
        "CSI 20 h must emit ModeChanged{{LineFeedNewLine, true}}; got {events:?}",
    );

    process(&mut parser, b"\x1b[20l");
    let events = parser.screen_mut().drain_events();
    assert!(
        events.contains(&ScreenEvent::ModeChanged {
            mode: TerminalMode::LineFeedNewLine,
            enabled: false,
        }),
        "CSI 20 l must emit ModeChanged{{LineFeedNewLine, false}}; got {events:?}",
    );
}

#[test]
fn lnm_idempotent_no_event() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[20h");
    parser.screen_mut().drain_events();

    process(&mut parser, b"\x1b[20h");
    let events = parser.screen_mut().drain_events();
    assert!(
        !events.iter().any(|e| matches!(
            e,
            ScreenEvent::ModeChanged {
                mode: TerminalMode::LineFeedNewLine,
                ..
            }
        )),
        "redundant CSI 20 h must not emit a ModeChanged event; got {events:?}",
    );
}

#[test]
fn lnm_lf_performs_implicit_cr_when_set() {
    // ANSI mode 20 (LNM) set: a bare LF (0x0A) acts as CR+LF, so the
    // cursor column resets to 0 in addition to the row advancing.
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[20h");
    process(&mut parser, b"ABCDE\n");
    let cursor = parser.screen().cursor();
    assert_eq!(
        cursor,
        Position { row: 1, col: 0 },
        "LNM-on LF must reset column to 0 and advance the row; got {cursor:?}",
    );
}

#[test]
fn lnm_lf_no_cr_when_unset() {
    // LNM clear (default): a bare LF advances the row only; the column
    // is preserved. This is the regression guard against accidentally
    // wiring implicit CR unconditionally.
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    assert!(!parser.screen().mode(TerminalMode::LineFeedNewLine));
    process(&mut parser, b"ABCDE\n");
    let cursor = parser.screen().cursor();
    assert_eq!(
        cursor,
        Position { row: 1, col: 5 },
        "LNM-off LF must preserve column and advance the row; got {cursor:?}",
    );
}