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 deccolm_set_erases_screen_and_homes_cursor() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    // Opt in to column-mode switching; xterm defaults `?40` off.
    process(&mut parser, b"\x1b[?40h");
    process(&mut parser, b"hello");
    process(&mut parser, b"\x1b[10;20H");
    // Set a non-default scroll region so we can observe its reset.
    process(&mut parser, b"\x1b[5;10r");
    assert_eq!(parser.screen().cell(0, 0).unwrap().contents(), "h");

    process(&mut parser, b"\x1b[?3h");

    assert!(!parser.screen().cell(0, 0).unwrap().has_contents());
    assert_eq!(parser.screen().cursor(), Position { row: 0, col: 0 });

    // After DECCOLM, scroll region must be the full screen: positioning
    // at the last row and triggering a line feed should leave the
    // cursor pinned at scroll_bottom = rows - 1.
    process(&mut parser, b"\x1b[24;1H\n");
    assert_eq!(parser.screen().cursor().row, 23);
}

#[test]
fn deccolm_reset_also_erases_and_homes() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?40h");
    // Enable DECCOLM first so the disable transition has a bit to clear.
    process(&mut parser, b"\x1b[?3h");
    process(&mut parser, b"hello");
    process(&mut parser, b"\x1b[10;20H");
    process(&mut parser, b"\x1b[5;10r");
    assert_eq!(parser.screen().cell(0, 0).unwrap().contents(), "h");

    process(&mut parser, b"\x1b[?3l");

    assert!(!parser.screen().cell(0, 0).unwrap().has_contents());
    assert_eq!(parser.screen().cursor(), Position { row: 0, col: 0 });
    process(&mut parser, b"\x1b[24;1H\n");
    assert_eq!(parser.screen().cursor().row, 23);
}

#[test]
fn deccolm_idempotent_set_does_not_re_erase() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"\x1b[?40h");
    // First DECSET 3 establishes the bit and runs side effects on a
    // blank screen.
    process(&mut parser, b"\x1b[?3h");
    process(&mut parser, b"hello");
    assert_eq!(parser.screen().cell(0, 0).unwrap().contents(), "h");

    // A redundant DECSET 3 must not re-erase what the program just
    // wrote.
    process(&mut parser, b"\x1b[?3h");
    assert_eq!(parser.screen().cell(0, 0).unwrap().contents(), "h");

    // Symmetric idempotency for DECRST 3 against an unset bit:
    // sending DECRST 3 twice in a row leaves the second call as a
    // no-op.
    process(&mut parser, b"\x1b[?3l");
    process(&mut parser, b"world");
    assert_eq!(parser.screen().cell(0, 0).unwrap().contents(), "w");
    process(&mut parser, b"\x1b[?3l");
    assert_eq!(parser.screen().cell(0, 0).unwrap().contents(), "w");
}

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

    process(&mut parser, b"\x1b[?3h");
    let events = parser.screen_mut().drain_events();
    assert!(events.contains(&ScreenEvent::ScreenCleared));

    process(&mut parser, b"\x1b[?3l");
    let events = parser.screen_mut().drain_events();
    assert!(events.contains(&ScreenEvent::ScreenCleared));
}

#[test]
fn deccolm_set_no_op_when_dec40_unset() {
    // xterm's `?40` defaults off: a program that issues DECSET 3
    // without first opting in must observe a complete no-op (no
    // screen-clear, no cursor move, no scroll-region reset, no
    // MODE_DECCOLM toggle, no ScreenEvent emitted).
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"hello");
    process(&mut parser, b"\x1b[10;20H");
    process(&mut parser, b"\x1b[5;10r");
    let cursor_before = parser.screen().cursor();
    let _ = parser.screen_mut().drain_events();

    process(&mut parser, b"\x1b[?3h");

    assert_eq!(parser.screen().cell(0, 0).unwrap().contents(), "h");
    assert_eq!(parser.screen().cursor(), cursor_before);
    assert!(!parser.screen().has_mode_bit(MODE_DECCOLM));
    let events = parser.screen_mut().drain_events();
    assert!(!events.contains(&ScreenEvent::ScreenCleared));

    // Symmetric: DECRST 3 with `?40` unset is also a no-op.
    process(&mut parser, b"\x1b[?3l");
    assert_eq!(parser.screen().cell(0, 0).unwrap().contents(), "h");
    assert_eq!(parser.screen().cursor(), cursor_before);
    assert!(!parser.screen().has_mode_bit(MODE_DECCOLM));
    let events = parser.screen_mut().drain_events();
    assert!(!events.contains(&ScreenEvent::ScreenCleared));
}

#[test]
fn deccolm_set_runs_side_effects_when_dec40_set() {
    let mut parser = crate::Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
    process(&mut parser, b"hello");
    assert_eq!(parser.screen().cell(0, 0).unwrap().contents(), "h");

    // Opt in via `?40h`, then `?3h` runs the gated side effects
    // exactly as the standalone DECCOLM tests assert.
    process(&mut parser, b"\x1b[?40h\x1b[?3h");

    assert!(!parser.screen().cell(0, 0).unwrap().has_contents());
    assert_eq!(parser.screen().cursor(), Position { row: 0, col: 0 });
    assert!(parser.screen().has_mode_bit(MODE_DECCOLM));
}

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

    process(&mut parser, b"\x1b[?40h");
    assert!(parser.screen().has_mode_bit(MODE_DEC_ALLOW_80_132));

    process(&mut parser, b"\x1b[?40l");
    assert!(!parser.screen().has_mode_bit(MODE_DEC_ALLOW_80_132));
}