use super::*;
#[test]
fn alt_screen_save_restore() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"Hello");
assert_eq!(screen.grid.cells[0][0].c, 'H');
assert_eq!(screen.grid.cells[0][4].c, 'o');
screen.process(b"\x1b[?1049h");
assert!(screen.state.in_alt_screen);
assert_eq!(screen.grid.cells[0][0].c, ' ');
screen.process(b"Alt");
assert_eq!(screen.grid.cells[0][0].c, 'A');
screen.process(b"\x1b[?1049l");
assert!(!screen.state.in_alt_screen);
assert_eq!(screen.grid.cells[0][0].c, 'H');
assert_eq!(screen.grid.cells[0][4].c, 'o');
}
#[test]
fn scrollback_on_scroll() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"Line1\r\nLine2\r\nLine3\r\nLine4");
let scrollback = screen.take_pending_scrollback();
assert!(!scrollback.is_empty());
let first = String::from_utf8_lossy(&scrollback[0]);
assert!(first.contains("Line1"), "scrollback should contain Line1, got: {}", first);
}
#[test]
fn no_scrollback_in_alt_screen() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"\x1b[?1049h"); screen.process(b"A\r\nB\r\nC\r\nD"); let scrollback = screen.take_pending_scrollback();
assert!(scrollback.is_empty(), "alt screen should not generate scrollback");
}
#[test]
fn history_preserved_across_sessions() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"A\r\nB\r\nC\r\nD\r\nE");
let _ = screen.take_pending_scrollback();
let history = screen.get_history();
assert!(!history.is_empty());
}
#[test]
fn deferred_wrap_cr_stays_on_same_line() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"% ");
assert!(screen.grid.wrap_pending);
assert_eq!(screen.grid.cursor_y, 0);
screen.process(b"\r");
assert!(!screen.grid.wrap_pending);
assert_eq!(screen.grid.cursor_x, 0);
assert_eq!(screen.grid.cursor_y, 0);
screen.process(b" ");
assert_eq!(screen.grid.cells[0][0].c, ' ');
}
#[test]
fn deferred_wrap_next_print_wraps() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"ABCDE");
assert!(screen.grid.wrap_pending);
assert_eq!(screen.grid.cursor_y, 0);
screen.process(b"F");
assert_eq!(screen.grid.cursor_y, 1);
assert_eq!(screen.grid.cells[1][0].c, 'F');
}
#[test]
fn dsr_cursor_position_report() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[5;10H"); screen.process(b"\x1b[6n"); let responses = screen.take_responses();
assert_eq!(responses.len(), 1);
assert_eq!(responses[0], b"\x1b[5;10R");
}
#[test]
fn da1_primary_device_attributes() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[c");
let responses = screen.take_responses();
assert_eq!(responses.len(), 1);
assert_eq!(responses[0], b"\x1b[?62;c");
}
#[test]
fn da2_secondary_device_attributes() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[>c");
let responses = screen.take_responses();
assert_eq!(responses.len(), 1);
assert_eq!(responses[0], b"\x1b[>0;10;1c");
}
#[test]
fn dec_line_drawing_charset() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b(0"); screen.process(b"lqk"); assert_eq!(screen.grid.cells[0][0].c, '\u{250C}'); assert_eq!(screen.grid.cells[0][1].c, '\u{2500}'); assert_eq!(screen.grid.cells[0][2].c, '\u{2510}'); screen.process(b"\x1b(B");
screen.process(b"l");
assert_eq!(screen.grid.cells[0][3].c, 'l'); }
#[test]
fn rep_repeats_last_char() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"A\x1b[3b"); assert_eq!(screen.grid.cells[0][0].c, 'A');
assert_eq!(screen.grid.cells[0][1].c, 'A');
assert_eq!(screen.grid.cells[0][2].c, 'A');
assert_eq!(screen.grid.cells[0][3].c, 'A');
}
#[test]
fn wide_character_occupies_two_cells() {
let mut screen = Screen::new(80, 24, 100);
screen.process("你".as_bytes());
assert_eq!(screen.grid.cells[0][0].c, '你');
assert_eq!(screen.grid.cells[0][0].width, 2);
assert_eq!(screen.grid.cells[0][1].width, 0);
assert_eq!(screen.grid.cursor_x, 2);
}
#[test]
fn wide_char_wraps_at_end_of_line() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"ABCD"); screen.process("你".as_bytes()); assert_eq!(screen.grid.cells[0][4].c, ' ');
assert_eq!(screen.grid.cells[1][0].c, '你');
assert_eq!(screen.grid.cells[1][0].width, 2);
assert_eq!(screen.grid.cells[1][1].width, 0);
}
#[test]
fn esc_c_full_reset() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[?2004h"); screen.process(b"\x1b[5;10H"); screen.process(b"Hello");
screen.process(b"\x1b[2 q"); screen.process(b"\x1bc"); assert_eq!(screen.grid.cursor_x, 0);
assert_eq!(screen.grid.cursor_y, 0);
assert!(!screen.grid.modes.bracketed_paste);
assert_eq!(screen.grid.modes.cursor_shape, grid::CursorShape::Default);
assert!(screen.grid.cursor_visible);
assert_eq!(screen.grid.cells[0][0].c, ' ');
assert!(screen.state.title.is_empty());
}
#[test]
fn osc_sets_title() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b]0;My Terminal\x07");
assert_eq!(screen.state.title, "My Terminal");
screen.process(b"\x1b]2;New Title\x07");
assert_eq!(screen.state.title, "New Title");
}
#[test]
fn osc_passthrough_non_title() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b]777;notify;Test;Hello\x07");
let pt = screen.take_passthrough();
assert_eq!(pt.len(), 1, "should have one passthrough sequence");
assert_eq!(pt[0], b"\x1b]777;notify;Test;Hello\x07");
assert_eq!(screen.state.title, "");
}
#[test]
fn osc_title_not_passedthrough() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b]0;My Title\x07");
let pt = screen.take_passthrough();
assert!(pt.is_empty(), "OSC 0 should not be passedthrough");
assert_eq!(screen.state.title, "My Title");
}
#[test]
fn bell_forwarded_as_passthrough() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x07");
let pt = screen.take_passthrough();
assert_eq!(pt.len(), 1, "standalone BEL should produce one passthrough");
assert_eq!(pt[0], b"\x07");
}
#[test]
fn bell_does_not_affect_screen_state() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"Hello");
let (cx, cy) = (screen.grid.cursor_x, screen.grid.cursor_y);
screen.process(b"\x07");
assert_eq!(screen.grid.cursor_x, cx, "BEL should not move cursor x");
assert_eq!(screen.grid.cursor_y, cy, "BEL should not move cursor y");
assert_eq!(screen.grid.cells[0][0].c, 'H', "BEL should not alter cell content");
}
#[test]
fn bell_drained_after_take() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x07");
let pt1 = screen.take_passthrough();
assert_eq!(pt1.len(), 1);
let pt2 = screen.take_passthrough();
assert!(pt2.is_empty(), "BEL should not persist after take_passthrough()");
}
#[test]
fn bell_not_resent_on_render() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x07");
let _ = screen.take_passthrough(); let mut cache = RenderCache::new();
let _ = screen.render(true, &mut cache);
let pt = screen.take_passthrough();
assert!(pt.is_empty(), "BEL must not be re-sent on full redraw");
}
#[test]
fn bell_not_resent_on_incremental_render() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x07");
let _ = screen.take_passthrough(); let mut cache = RenderCache::new();
let _ = screen.render(false, &mut cache);
let pt = screen.take_passthrough();
assert!(pt.is_empty(), "BEL must not be re-sent on incremental render");
}
#[test]
fn bell_not_resent_on_resize() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x07");
let _ = screen.take_passthrough(); screen.resize(120, 40);
let pt = screen.take_passthrough();
assert!(pt.is_empty(), "BEL must not be re-sent after resize");
}
#[test]
fn osc_777_drained_after_take() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b]777;notify;title;body\x07");
let pt1 = screen.take_passthrough();
assert_eq!(pt1.len(), 1);
let pt2 = screen.take_passthrough();
assert!(pt2.is_empty(), "OSC 777 should not persist after take_passthrough()");
}
#[test]
fn osc_777_not_resent_on_render() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b]777;notify;title;body\x07");
let _ = screen.take_passthrough(); let mut cache = RenderCache::new();
let _ = screen.render(true, &mut cache);
let pt = screen.take_passthrough();
assert!(pt.is_empty(), "OSC 777 must not be re-sent on full redraw");
}
#[test]
fn osc_777_not_resent_on_resize() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b]777;notify;title;body\x07");
let _ = screen.take_passthrough(); screen.resize(120, 40);
let pt = screen.take_passthrough();
assert!(pt.is_empty(), "OSC 777 must not be re-sent after resize");
}
#[test]
fn osc_777_not_resent_on_resize_then_render() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b]777;notify;title;body\x07");
let _ = screen.take_passthrough(); screen.resize(40, 10);
let mut cache = RenderCache::new();
let _ = screen.render(true, &mut cache);
let pt = screen.take_passthrough();
assert!(pt.is_empty(), "OSC 777 must not re-appear after resize + full redraw");
}
#[test]
fn multiple_bells_all_forwarded() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x07\x07\x07");
let pt = screen.take_passthrough();
assert_eq!(pt.len(), 3, "three BELs should produce three passthrough entries");
for entry in &pt {
assert_eq!(entry, &vec![0x07u8]);
}
}
#[test]
fn bell_and_osc_777_interleaved() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x07\x1b]777;notify;t;b\x07\x07");
let pt = screen.take_passthrough();
assert_eq!(pt.len(), 3, "BEL + OSC 777 + BEL = 3 passthrough entries");
assert_eq!(pt[0], b"\x07", "first should be standalone BEL");
assert_eq!(pt[1], b"\x1b]777;notify;t;b\x07", "second should be OSC 777");
assert_eq!(pt[2], b"\x07", "third should be standalone BEL");
}
#[test]
fn bell_in_osc_is_terminator_not_separate_bell() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b]777;notify;title;body\x07");
let pt = screen.take_passthrough();
assert_eq!(pt.len(), 1, "OSC terminated by BEL should produce exactly 1 passthrough");
assert!(pt[0].starts_with(b"\x1b]"), "passthrough should be the OSC sequence");
}
#[test]
fn bell_not_resent_on_render_with_scrollback() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"A\r\nB\r\nC\r\nD");
let scrollback = screen.take_pending_scrollback();
screen.process(b"\x07");
let _ = screen.take_passthrough(); let mut cache = RenderCache::new();
let _ = screen.render_with_scrollback(&scrollback, &mut cache);
let pt = screen.take_passthrough();
assert!(pt.is_empty(), "BEL must not re-appear after render_with_scrollback");
}
#[test]
fn osc_777_not_resent_on_render_with_scrollback() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"A\r\nB\r\nC\r\nD");
let scrollback = screen.take_pending_scrollback();
screen.process(b"\x1b]777;notify;title;body\x07");
let _ = screen.take_passthrough(); let mut cache = RenderCache::new();
let _ = screen.render_with_scrollback(&scrollback, &mut cache);
let pt = screen.take_passthrough();
assert!(pt.is_empty(), "OSC 777 must not re-appear after render_with_scrollback");
}
#[test]
fn mode_flags_bracketed_paste() {
let mut screen = Screen::new(80, 24, 100);
assert!(!screen.grid.modes.bracketed_paste);
screen.process(b"\x1b[?2004h");
assert!(screen.grid.modes.bracketed_paste);
screen.process(b"\x1b[?2004l");
assert!(!screen.grid.modes.bracketed_paste);
}
#[test]
fn mode_flags_cursor_key_mode() {
let mut screen = Screen::new(80, 24, 100);
assert!(!screen.grid.modes.cursor_key_mode);
screen.process(b"\x1b[?1h");
assert!(screen.grid.modes.cursor_key_mode);
screen.process(b"\x1b[?1l");
assert!(!screen.grid.modes.cursor_key_mode);
}
#[test]
fn mode_flags_mouse() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[?1000h");
assert_eq!(screen.grid.modes.mouse_mode, 1000);
screen.process(b"\x1b[?1006h");
assert_eq!(screen.grid.modes.mouse_encoding, 1006);
screen.process(b"\x1b[?1000l");
assert_eq!(screen.grid.modes.mouse_mode, 0);
}
#[test]
fn keypad_app_mode() {
let mut screen = Screen::new(80, 24, 100);
assert!(!screen.grid.modes.keypad_app_mode);
screen.process(b"\x1b=");
assert!(screen.grid.modes.keypad_app_mode);
screen.process(b"\x1b>");
assert!(!screen.grid.modes.keypad_app_mode);
}
#[test]
fn cursor_shape_decscusr() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[2 q"); assert_eq!(screen.grid.modes.cursor_shape, grid::CursorShape::SteadyBlock);
screen.process(b"\x1b[5 q"); assert_eq!(screen.grid.modes.cursor_shape, grid::CursorShape::BlinkBar);
screen.process(b"\x1b[0 q"); assert_eq!(screen.grid.modes.cursor_shape, grid::CursorShape::Default);
}
#[test]
fn autowrap_mode_disable_prevents_wrap() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"\x1b[?7l"); screen.process(b"ABCDEF"); assert_eq!(screen.grid.cursor_y, 0);
assert_eq!(screen.grid.cells[0][4].c, 'F');
assert!(!screen.grid.wrap_pending);
}
#[test]
fn sgr_hidden_attribute() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[8m"); screen.process(b"secret");
assert!(screen.grid.cells[0][0].style.hidden);
screen.process(b"\x1b[28m"); screen.process(b"visible");
assert!(!screen.grid.cells[0][6].style.hidden);
}
#[test]
fn cursor_save_restore() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[5;10H"); screen.process(b"\x1b7"); screen.process(b"\x1b[1;1H"); assert_eq!(screen.grid.cursor_y, 0);
screen.process(b"\x1b8"); assert_eq!(screen.grid.cursor_y, 4);
assert_eq!(screen.grid.cursor_x, 9);
}
#[test]
fn so_si_charset_switching() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b)0"); screen.process(b"\x0E"); screen.process(b"q"); assert_eq!(screen.grid.cells[0][0].c, '\u{2500}');
screen.process(b"\x0F"); screen.process(b"q");
assert_eq!(screen.grid.cells[0][1].c, 'q');
}
#[test]
fn cuu_cud_respects_scroll_region() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[5;15r");
screen.process(b"\x1b[10;1H"); screen.process(b"\x1b[20A"); assert_eq!(screen.grid.cursor_y, 4); screen.process(b"\x1b[20B"); assert_eq!(screen.grid.cursor_y, 14); }
#[test]
fn vt_ff_treated_as_lf() {
let mut screen = Screen::new(80, 3, 100);
screen.process(b"A");
screen.process(&[0x0B]); assert_eq!(screen.grid.cursor_y, 1);
screen.process(&[0x0C]); assert_eq!(screen.grid.cursor_y, 2);
}
#[test]
fn dl_large_count_clamped() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"\x1b[2;1H"); screen.process(b"\x1b[99999M"); assert_eq!(screen.grid.cells.len(), 5);
}
#[test]
fn il_large_count_clamped() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"\x1b[2;1H");
screen.process(b"\x1b[99999L"); assert_eq!(screen.grid.cells.len(), 5);
}
#[test]
fn alt_screen_mode_47_no_cursor_save() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"\x1b[3;4H");
assert_eq!(screen.grid.cursor_y, 2);
assert_eq!(screen.grid.cursor_x, 3);
screen.process(b"\x1b7");
screen.process(b"\x1b[?47h");
screen.process(b"\x1b[1;1H");
assert_eq!(screen.grid.cursor_x, 0);
assert_eq!(screen.grid.cursor_y, 0);
screen.process(b"\x1b[?47l");
assert_eq!(screen.grid.cursor_x, 0);
assert_eq!(screen.grid.cursor_y, 0);
screen.process(b"\x1b8");
assert_eq!(screen.grid.cursor_x, 3);
assert_eq!(screen.grid.cursor_y, 2);
}
#[test]
fn mode_1048_save_restore_cursor() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[5;10H"); screen.process(b"\x1b[?1048h"); screen.process(b"\x1b[1;1H"); screen.process(b"\x1b[?1048l"); assert_eq!(screen.grid.cursor_y, 4);
assert_eq!(screen.grid.cursor_x, 9);
}
#[test]
fn dch_through_wide_char_no_orphan() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"A");
screen.process("你".as_bytes());
screen.process(b"B");
screen.process(b"\x1b[1;2H"); screen.process(b"\x1b[P"); assert_ne!(screen.grid.cells[0][1].width, 0,
"orphaned continuation cell after DCH");
}
#[test]
fn ich_pushes_wide_char_off_right_edge() {
let mut screen = Screen::new(6, 3, 100);
screen.process(b"\x1b[1;5H"); screen.process("你".as_bytes());
assert_eq!(screen.grid.cells[0][4].c, '你');
assert_eq!(screen.grid.cells[0][4].width, 2);
assert_eq!(screen.grid.cells[0][5].width, 0);
screen.process(b"\x1b[1;1H");
screen.process(b"\x1b[@"); assert_ne!(screen.grid.cells[0][5].width, 2,
"orphaned wide char at right edge after ICH");
}
#[test]
fn scrollback_captured_with_partial_scroll_region() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"\x1b[1;3r");
screen.process(b"\x1b[1;1H");
screen.process(b"Line1\r\n");
screen.process(b"Line2\r\n");
screen.process(b"Line3\r\n"); let scrollback = screen.take_pending_scrollback();
assert!(!scrollback.is_empty(),
"scrollback should be captured even with partial scroll region (scroll_top==0)");
}
#[test]
fn csi_s_u_save_restore_cursor() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[5;10H"); screen.process(b"\x1b[s"); screen.process(b"\x1b[1;1H"); assert_eq!(screen.grid.cursor_y, 0);
assert_eq!(screen.grid.cursor_x, 0);
screen.process(b"\x1b[u"); assert_eq!(screen.grid.cursor_y, 4); assert_eq!(screen.grid.cursor_x, 9); }
#[test]
fn cursor_movement_cuf_cub() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[5C"); assert_eq!(screen.grid.cursor_x, 5);
screen.process(b"\x1b[2D"); assert_eq!(screen.grid.cursor_x, 3);
screen.process(b"\x1b[100D");
assert_eq!(screen.grid.cursor_x, 0);
screen.process(b"\x1b[200C");
assert_eq!(screen.grid.cursor_x, 79);
}
#[test]
fn cursor_movement_cnl_cpl() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[10;15H"); assert_eq!(screen.grid.cursor_y, 9);
assert_eq!(screen.grid.cursor_x, 14);
screen.process(b"\x1b[3E");
assert_eq!(screen.grid.cursor_y, 12);
assert_eq!(screen.grid.cursor_x, 0);
screen.process(b"\x1b[5;20H"); screen.process(b"\x1b[2F");
assert_eq!(screen.grid.cursor_y, 2); assert_eq!(screen.grid.cursor_x, 0);
screen.process(b"\x1b[100E");
assert_eq!(screen.grid.cursor_y, 23);
assert_eq!(screen.grid.cursor_x, 0);
screen.process(b"\x1b[100F");
assert_eq!(screen.grid.cursor_y, 0);
assert_eq!(screen.grid.cursor_x, 0);
}
#[test]
fn cursor_horizontal_absolute() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[20G");
assert_eq!(screen.grid.cursor_x, 19); screen.process(b"\x1b[1G");
assert_eq!(screen.grid.cursor_x, 0);
screen.process(b"\x1b[200G");
assert_eq!(screen.grid.cursor_x, 79);
screen.process(b"\x1b[G");
assert_eq!(screen.grid.cursor_x, 0);
}
#[test]
fn cursor_position_cup() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[12;40H");
assert_eq!(screen.grid.cursor_y, 11); assert_eq!(screen.grid.cursor_x, 39); screen.process(b"\x1b[H");
assert_eq!(screen.grid.cursor_y, 0);
assert_eq!(screen.grid.cursor_x, 0);
screen.process(b"\x1b[100;200H");
assert_eq!(screen.grid.cursor_y, 23);
assert_eq!(screen.grid.cursor_x, 79);
}
#[test]
fn vpa_line_position_absolute() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[5;10H"); screen.process(b"\x1b[15d");
assert_eq!(screen.grid.cursor_y, 14); assert_eq!(screen.grid.cursor_x, 9); screen.process(b"\x1b[100d");
assert_eq!(screen.grid.cursor_y, 23);
screen.process(b"\x1b[d");
assert_eq!(screen.grid.cursor_y, 0);
}
#[test]
fn erase_in_display_j0() {
let mut screen = Screen::new(10, 5, 100);
for row in 0..5 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(b"XXXXXXXXXX");
}
screen.process(b"\x1b[3;5H");
screen.process(b"\x1b[0J");
assert_eq!(screen.grid.cells[2][0].c, 'X');
assert_eq!(screen.grid.cells[2][3].c, 'X');
assert_eq!(screen.grid.cells[2][4].c, ' ');
assert_eq!(screen.grid.cells[2][9].c, ' ');
assert_eq!(screen.grid.cells[3][0].c, ' ');
assert_eq!(screen.grid.cells[4][5].c, ' ');
assert_eq!(screen.grid.cells[0][0].c, 'X');
assert_eq!(screen.grid.cells[1][9].c, 'X');
}
#[test]
fn erase_in_display_j1() {
let mut screen = Screen::new(10, 5, 100);
for row in 0..5 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(b"XXXXXXXXXX");
}
screen.process(b"\x1b[3;5H");
screen.process(b"\x1b[1J");
assert_eq!(screen.grid.cells[0][0].c, ' ');
assert_eq!(screen.grid.cells[1][9].c, ' ');
assert_eq!(screen.grid.cells[2][0].c, ' ');
assert_eq!(screen.grid.cells[2][4].c, ' ');
assert_eq!(screen.grid.cells[2][5].c, 'X');
assert_eq!(screen.grid.cells[2][9].c, 'X');
assert_eq!(screen.grid.cells[3][0].c, 'X');
assert_eq!(screen.grid.cells[4][5].c, 'X');
}
#[test]
fn erase_in_display_j2() {
let mut screen = Screen::new(10, 5, 100);
for row in 0..5 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(b"XXXXXXXXXX");
}
screen.process(b"\x1b[3;5H");
screen.process(b"\x1b[2J");
for row in 0..5 {
for col in 0..10 {
assert_eq!(screen.grid.cells[row][col].c, ' ',
"cell [{row}][{col}] should be blank after CSI 2J");
}
}
}
#[test]
fn erase_in_line_k0() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ"); screen.process(b"\x1b[1;4H"); screen.process(b"\x1b[0K");
assert_eq!(screen.grid.cells[0][0].c, 'A');
assert_eq!(screen.grid.cells[0][2].c, 'C');
assert_eq!(screen.grid.cells[0][3].c, ' '); assert_eq!(screen.grid.cells[0][9].c, ' '); }
#[test]
fn erase_in_line_k1() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ"); screen.process(b"\x1b[1;4H"); screen.process(b"\x1b[1K");
assert_eq!(screen.grid.cells[0][0].c, ' '); assert_eq!(screen.grid.cells[0][3].c, ' '); assert_eq!(screen.grid.cells[0][4].c, 'E'); assert_eq!(screen.grid.cells[0][9].c, 'J'); }
#[test]
fn erase_in_line_k2() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ"); screen.process(b"\x1b[1;4H"); screen.process(b"\x1b[2K");
for col in 0..10 {
assert_eq!(screen.grid.cells[0][col].c, ' ',
"col {col} should be blank after CSI 2K");
}
}
#[test]
fn erase_character_ech() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ"); screen.process(b"\x1b[1;3H"); screen.process(b"\x1b[4X");
assert_eq!(screen.grid.cells[0][0].c, 'A');
assert_eq!(screen.grid.cells[0][1].c, 'B');
assert_eq!(screen.grid.cells[0][2].c, ' '); assert_eq!(screen.grid.cells[0][3].c, ' '); assert_eq!(screen.grid.cells[0][4].c, ' '); assert_eq!(screen.grid.cells[0][5].c, ' '); assert_eq!(screen.grid.cells[0][6].c, 'G'); assert_eq!(screen.grid.cells[0][9].c, 'J'); assert_eq!(screen.grid.cursor_x, 2);
}
#[test]
fn delete_character_dch() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ"); screen.process(b"\x1b[1;3H"); screen.process(b"\x1b[2P");
assert_eq!(screen.grid.cells[0][0].c, 'A');
assert_eq!(screen.grid.cells[0][1].c, 'B');
assert_eq!(screen.grid.cells[0][2].c, 'E');
assert_eq!(screen.grid.cells[0][3].c, 'F');
assert_eq!(screen.grid.cells[0][7].c, 'J');
assert_eq!(screen.grid.cells[0][8].c, ' '); assert_eq!(screen.grid.cells[0][9].c, ' '); }
#[test]
fn insert_character_ich() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ"); screen.process(b"\x1b[1;3H"); screen.process(b"\x1b[2@");
assert_eq!(screen.grid.cells[0][0].c, 'A');
assert_eq!(screen.grid.cells[0][1].c, 'B');
assert_eq!(screen.grid.cells[0][2].c, ' '); assert_eq!(screen.grid.cells[0][3].c, ' '); assert_eq!(screen.grid.cells[0][4].c, 'C'); assert_eq!(screen.grid.cells[0][5].c, 'D'); assert_eq!(screen.grid.cells[0][9].c, 'H');
}
#[test]
fn scroll_up_su() {
let mut screen = Screen::new(10, 5, 100);
for row in 0..5 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(format!("Row{}", row).as_bytes());
}
screen.process(b"\x1b[2S");
assert_eq!(screen.grid.cells[0][0].c, 'R');
assert_eq!(screen.grid.cells[0][3].c, '2');
assert_eq!(screen.grid.cells[3][0].c, ' ');
assert_eq!(screen.grid.cells[4][0].c, ' ');
}
#[test]
fn scroll_down_sd() {
let mut screen = Screen::new(10, 5, 100);
for row in 0..5 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(format!("Row{}", row).as_bytes());
}
screen.process(b"\x1b[2T");
assert_eq!(screen.grid.cells[0][0].c, ' ');
assert_eq!(screen.grid.cells[1][0].c, ' ');
assert_eq!(screen.grid.cells[2][0].c, 'R');
assert_eq!(screen.grid.cells[2][3].c, '0');
}
#[test]
fn delete_lines_dl() {
let mut screen = Screen::new(10, 5, 100);
for row in 0..5 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(format!("Line{}", row).as_bytes());
}
screen.process(b"\x1b[2;1H");
screen.process(b"\x1b[2M");
assert_eq!(screen.grid.cells[1][4].c, '3');
assert_eq!(screen.grid.cells[2][4].c, '4');
assert_eq!(screen.grid.cells[3][0].c, ' ');
assert_eq!(screen.grid.cells[4][0].c, ' ');
}
#[test]
fn insert_lines_il() {
let mut screen = Screen::new(10, 5, 100);
for row in 0..5 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(format!("Line{}", row).as_bytes());
}
screen.process(b"\x1b[2;1H");
screen.process(b"\x1b[2L");
assert_eq!(screen.grid.cells[0][4].c, '0');
assert_eq!(screen.grid.cells[1][0].c, ' ');
assert_eq!(screen.grid.cells[2][0].c, ' ');
assert_eq!(screen.grid.cells[3][4].c, '1');
}
#[test]
fn decstbm_set_scroll_region() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[10;20H");
assert_eq!(screen.grid.cursor_y, 9);
assert_eq!(screen.grid.cursor_x, 19);
screen.process(b"\x1b[5;15r");
assert_eq!(screen.grid.scroll_top, 4);
assert_eq!(screen.grid.scroll_bottom, 14);
assert_eq!(screen.grid.cursor_x, 0);
assert_eq!(screen.grid.cursor_y, 0);
assert!(!screen.grid.wrap_pending);
}
#[test]
fn reverse_index_ri() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"\x1b[2;4r");
screen.process(b"\x1b[2;1H");
screen.process(b"LineA");
screen.process(b"\x1b[3;1H");
screen.process(b"LineB");
screen.process(b"\x1b[4;1H");
screen.process(b"LineC");
screen.process(b"\x1b[2;1H");
assert_eq!(screen.grid.cursor_y, 1);
screen.process(b"\x1bM");
assert_eq!(screen.grid.cursor_y, 1);
assert_eq!(screen.grid.cells[1][0].c, ' ');
assert_eq!(screen.grid.cells[2][0].c, 'L');
assert_eq!(screen.grid.cells[2][4].c, 'A');
}
#[test]
fn reverse_index_ri_not_at_top() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"\x1b[3;1H"); screen.process(b"\x1bM"); assert_eq!(screen.grid.cursor_y, 1); }
#[test]
fn focus_reporting_mode() {
let mut screen = Screen::new(80, 24, 100);
assert!(!screen.grid.modes.focus_reporting);
screen.process(b"\x1b[?1004h");
assert!(screen.grid.modes.focus_reporting);
screen.process(b"\x1b[?1004l");
assert!(!screen.grid.modes.focus_reporting);
}
#[test]
fn autowrap_mode_re_enable() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"\x1b[?7l");
assert!(!screen.grid.modes.autowrap_mode);
screen.process(b"ABCDEF");
assert_eq!(screen.grid.cursor_y, 0);
assert_eq!(screen.grid.cells[0][4].c, 'F');
screen.process(b"\x1b[?7h");
assert!(screen.grid.modes.autowrap_mode);
screen.process(b"\x1b[1;1H");
screen.process(b"12345");
assert!(screen.grid.wrap_pending);
screen.process(b"6");
assert_eq!(screen.grid.cursor_y, 1);
assert_eq!(screen.grid.cells[1][0].c, '6');
}
#[test]
fn bce_erase_uses_bg_color() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ"); screen.process(b"\x1b[41m");
screen.process(b"\x1b[1;4H");
screen.process(b"\x1b[0K");
let expected_bg = Some(style::Color::Indexed(1)); assert_eq!(screen.grid.cells[0][3].style.bg, expected_bg,
"erased cell at col 3 should have red background (BCE)");
assert_eq!(screen.grid.cells[0][9].style.bg, expected_bg,
"erased cell at col 9 should have red background (BCE)");
assert_eq!(screen.grid.cells[0][0].style.bg, None,
"cell at col 0 should have default background");
screen.process(b"\x1b[2J");
assert_eq!(screen.grid.cells[1][5].style.bg, expected_bg,
"CSI 2J erased cell should have red background (BCE)");
screen.process(b"\x1b[1;1H");
screen.process(b"XYZ");
screen.process(b"\x1b[1;1H");
screen.process(b"\x1b[2X"); assert_eq!(screen.grid.cells[0][0].style.bg, expected_bg,
"ECH erased cell should have red background (BCE)");
assert_eq!(screen.grid.cells[0][1].style.bg, expected_bg,
"ECH erased cell at col 1 should have red background (BCE)");
}
#[test]
fn tab_advances_to_next_tab_stop() {
let mut screen = Screen::new(80, 3, 100);
screen.process(b"AB"); screen.process(b"\t"); assert_eq!(screen.grid.cursor_x, 8);
screen.process(b"\t"); assert_eq!(screen.grid.cursor_x, 16);
}
#[test]
fn tab_at_end_of_line_clamps() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGH"); screen.process(b"\t"); assert_eq!(screen.grid.cursor_x, 9);
}
#[test]
fn backspace_at_column_zero() {
let mut screen = Screen::new(80, 3, 100);
assert_eq!(screen.grid.cursor_x, 0);
screen.process(b"\x08"); assert_eq!(screen.grid.cursor_x, 0, "BS at column 0 should stay at 0");
}
#[test]
fn backspace_clears_wrap_pending() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"ABCDE"); assert!(screen.grid.wrap_pending);
screen.process(b"\x08"); assert!(!screen.grid.wrap_pending, "BS should clear wrap_pending");
assert_eq!(screen.grid.cursor_x, 3);
}
#[test]
fn erase_scrollback_j3() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"Line1\r\nLine2\r\nLine3\r\nLine4\r\nLine5");
let history = screen.get_history();
assert!(!history.is_empty(), "should have scrollback before J3");
screen.process(b"\x1b[3J");
let history_after = screen.get_history();
assert!(history_after.is_empty(),
"CSI 3J should clear all scrollback, got {} lines", history_after.len());
}
#[test]
fn alt_screen_clears_wrap_pending() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"ABCDE"); assert!(screen.grid.wrap_pending);
screen.process(b"\x1b[?1049h");
assert!(!screen.grid.wrap_pending,
"wrap_pending should be cleared on alt screen enter");
assert_eq!(screen.grid.cursor_x, 0);
assert_eq!(screen.grid.cursor_y, 0);
}
#[test]
fn alt_screen_mode_1047() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"Hello");
assert_eq!(screen.grid.cells[0][0].c, 'H');
screen.process(b"\x1b[?1047h");
assert!(screen.state.in_alt_screen);
assert_eq!(screen.grid.cells[0][0].c, ' ');
screen.process(b"Alt");
assert_eq!(screen.grid.cells[0][0].c, 'A');
screen.process(b"\x1b[?1047l");
assert!(!screen.state.in_alt_screen);
assert_eq!(screen.grid.cells[0][0].c, 'H'); }
#[test]
fn alt_screen_mode_47() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"Main");
assert_eq!(screen.grid.cells[0][0].c, 'M');
screen.process(b"\x1b[?47h");
assert!(screen.state.in_alt_screen);
assert_eq!(screen.grid.cells[0][0].c, ' ');
screen.process(b"\x1b[?47l");
assert!(!screen.state.in_alt_screen);
assert_eq!(screen.grid.cells[0][0].c, 'M');
}
#[test]
fn alt_screen_restores_modes() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"\x1b[?2004h"); screen.process(b"\x1b[?1h"); assert!(screen.grid.modes.bracketed_paste);
assert!(screen.grid.modes.cursor_key_mode);
screen.process(b"\x1b[?1049h");
screen.process(b"\x1b[?2004l");
screen.process(b"\x1b[?1l");
assert!(!screen.grid.modes.bracketed_paste);
assert!(!screen.grid.modes.cursor_key_mode);
screen.process(b"\x1b[?1049l");
assert!(screen.grid.modes.bracketed_paste,
"bracketed paste should be restored on alt screen exit");
assert!(screen.grid.modes.cursor_key_mode,
"cursor key mode should be restored on alt screen exit");
}
#[test]
fn cursor_visibility_mode_25() {
let mut screen = Screen::new(80, 24, 100);
assert!(screen.grid.cursor_visible);
screen.process(b"\x1b[?25l");
assert!(!screen.grid.cursor_visible, "cursor should be hidden after ?25l");
screen.process(b"\x1b[?25h");
assert!(screen.grid.cursor_visible, "cursor should be visible after ?25h");
}
#[test]
fn render_with_hidden_cursor() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"\x1b[?25l"); let mut cache = RenderCache::new();
let result = screen.render(false, &mut cache);
let text = String::from_utf8_lossy(&result);
assert!(!text.contains("\x1b[?25h"),
"hidden cursor should not emit ?25h in render output");
assert!(text.contains("\x1b[?25l"),
"render should always hide cursor during redraw");
}
#[test]
fn render_full_reattach_redraws_all() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"Hello");
let mut cache = RenderCache::new();
let _ = screen.render(false, &mut cache);
let result = screen.render(true, &mut cache);
let text = String::from_utf8_lossy(&result);
assert!(text.contains("\x1b[2J\x1b[H"),
"full render should clear screen");
assert!(text.contains("Hello"),
"full render should include screen content");
}
#[test]
fn pending_scrollback_drained_separately() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"A\r\nB\r\nC\r\nD");
let pending = screen.take_pending_scrollback();
assert!(!pending.is_empty(), "should have pending scrollback");
let pending2 = screen.take_pending_scrollback();
assert!(pending2.is_empty(), "second drain should be empty");
let history = screen.get_history();
assert!(!history.is_empty(), "history should be preserved after drain");
}
#[test]
fn stale_pending_scrollback_after_reattach_simulation() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"Line1\r\nLine2\r\nLine3\r\nLine4");
let _ = screen.take_pending_scrollback();
screen.process(b"\r\nLine5\r\nLine6");
let history = screen.get_history();
let history_count = history.len();
let stale = screen.take_pending_scrollback();
assert!(!stale.is_empty(),
"there should be stale pending scrollback from the disconnect");
screen.process(b"\r\nLine7");
let new_pending = screen.take_pending_scrollback();
let new_history = screen.get_history();
assert_eq!(new_history.len(), history_count + new_pending.len(),
"new scrollback should only contain lines added after reattach drain");
}
#[test]
fn window_ops_ignored() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[14t"); screen.process(b"\x1b[22;0t"); let responses = screen.take_responses();
assert!(responses.is_empty(), "window ops should not generate responses");
}
#[test]
fn scroll_region_il_dl_interaction() {
let mut screen = Screen::new(10, 6, 100);
screen.process(b"\x1b[2;5r");
for row in 0..6 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(format!("R{}", row).as_bytes());
}
screen.process(b"\x1b[3;1H"); screen.process(b"\x1b[L");
assert_eq!(screen.grid.cells[2][0].c, ' ',
"inserted line should be blank");
assert_eq!(screen.grid.cells[0][0].c, 'R',
"row above scroll region should be untouched");
assert_eq!(screen.grid.cells[5][0].c, 'R',
"row below scroll region should be untouched");
}
#[test]
fn render_bce_erase_output() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ"); screen.process(b"\x1b[41m"); screen.process(b"\x1b[1;4H"); screen.process(b"\x1b[0K");
let mut cache = RenderCache::new();
let result = screen.render(true, &mut cache);
let text = String::from_utf8_lossy(&result);
assert!(text.contains("41"), "rendered output should include red bg (41) after BCE erase");
}
#[test]
fn wide_char_scrollback_rendering() {
let mut screen = Screen::new(10, 3, 100);
screen.process("\u{4e16}\u{754c}".as_bytes()); screen.process(b"\r\nLine2\r\nLine3\r\nLine4");
let history = screen.get_history();
assert!(!history.is_empty(), "should have scrollback");
let first_line = String::from_utf8_lossy(&history[0]);
assert!(first_line.contains('\u{4e16}'), "scrollback should contain wide char 世");
assert!(first_line.contains('\u{754c}'), "scrollback should contain wide char 界");
}
#[test]
fn combining_mark_attaches_to_previous_cell() {
let mut screen = Screen::new(80, 24, 100);
screen.process("e\u{0301}".as_bytes());
assert_eq!(screen.grid.cells[0][0].c, 'e');
assert_eq!(screen.grid.cells[0][0].combining, vec!['\u{0301}']);
}
#[test]
fn combining_mark_with_wrap_pending() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"ABCDE");
assert!(screen.grid.wrap_pending, "wrap should be pending after filling line");
screen.process("\u{0308}".as_bytes()); assert_eq!(screen.grid.cells[0][4].c, 'E');
assert_eq!(screen.grid.cells[0][4].combining, vec!['\u{0308}']);
}
#[test]
fn combining_mark_on_wide_char() {
let mut screen = Screen::new(80, 24, 100);
screen.process("\u{4e16}\u{0301}".as_bytes()); assert_eq!(screen.grid.cells[0][0].c, '\u{4e16}');
assert_eq!(screen.grid.cells[0][0].combining, vec!['\u{0301}']);
assert_eq!(screen.grid.cells[0][1].width, 0); }
#[test]
fn combining_mark_renders_in_output() {
let mut screen = Screen::new(80, 24, 100);
screen.process("e\u{0301}".as_bytes());
let mut cache = RenderCache::new();
let output = screen.render(true, &mut cache);
let text = String::from_utf8_lossy(&output);
assert!(text.contains("e\u{0301}"), "rendered output should contain base char + combining mark");
}
#[test]
fn delete_lines_preserves_cursor_x() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[6;11H"); assert_eq!(screen.grid.cursor_x, 10);
assert_eq!(screen.grid.cursor_y, 5);
screen.process(b"\x1b[M");
assert_eq!(screen.grid.cursor_x, 10, "DL must not change cursor_x");
assert_eq!(screen.grid.cursor_y, 5, "DL must not change cursor_y");
}
#[test]
fn insert_lines_preserves_cursor_x() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[6;11H");
assert_eq!(screen.grid.cursor_x, 10);
screen.process(b"\x1b[L");
assert_eq!(screen.grid.cursor_x, 10, "IL must not change cursor_x");
assert_eq!(screen.grid.cursor_y, 5, "IL must not change cursor_y");
}
fn screen_lines(screen: &Screen) -> Vec<String> {
screen
.grid
.cells
.iter()
.map(|row| {
let s: String = row.iter().map(|c| c.c).collect();
s.trim_end().to_string()
})
.collect()
}
fn strip_ansi(bytes: &[u8]) -> String {
let s = String::from_utf8_lossy(bytes);
let mut out = String::new();
let mut in_esc = false;
for ch in s.chars() {
if in_esc {
if ch.is_ascii_alphabetic() || ch == 'm' {
in_esc = false;
}
continue;
}
if ch == '\x1b' {
in_esc = true;
continue;
}
if ch >= ' ' {
out.push(ch);
}
}
out.trim_end().to_string()
}
fn history_texts(screen: &Screen) -> Vec<String> {
screen
.get_history()
.iter()
.map(|b| strip_ansi(b))
.collect()
}
#[test]
fn lf_at_bottom_scrolls_content_up() {
let mut screen = Screen::new(10, 4, 100);
screen.process(b"Row0\r\nRow1\r\nRow2\r\nRow3");
assert_eq!(screen_lines(&screen), vec!["Row0", "Row1", "Row2", "Row3"]);
screen.process(b"\r\nRow4");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Row1", "first visible row after scroll");
assert_eq!(lines[1], "Row2");
assert_eq!(lines[2], "Row3");
assert_eq!(lines[3], "Row4", "new content at bottom");
}
#[test]
fn lf_scroll_captures_scrollback() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"AAA\r\nBBB\r\nCCC\r\nDDD");
let hist = history_texts(&screen);
assert_eq!(hist.len(), 1, "exactly one line scrolled off");
assert_eq!(hist[0], "AAA");
}
#[test]
fn many_lines_overflow_screen() {
let mut screen = Screen::new(10, 5, 100);
for i in 0..20 {
if i > 0 {
screen.process(b"\r\n");
}
screen.process(format!("L{:02}", i).as_bytes());
}
let lines = screen_lines(&screen);
assert_eq!(lines[0], "L15");
assert_eq!(lines[1], "L16");
assert_eq!(lines[2], "L17");
assert_eq!(lines[3], "L18");
assert_eq!(lines[4], "L19");
let hist = history_texts(&screen);
assert_eq!(hist.len(), 15);
assert_eq!(hist[0], "L00", "first scrollback line");
assert_eq!(hist[14], "L14", "last scrollback line");
}
#[test]
fn scrollback_order_preserved() {
let mut screen = Screen::new(10, 3, 100);
for i in 0..10 {
if i > 0 {
screen.process(b"\r\n");
}
screen.process(format!("Line{}", i).as_bytes());
}
let hist = history_texts(&screen);
assert_eq!(hist.len(), 7);
for (idx, h) in hist.iter().enumerate() {
assert_eq!(*h, format!("Line{}", idx),
"scrollback line {} should be Line{}", idx, idx);
}
}
#[test]
fn lf_within_scroll_region_only_scrolls_region() {
let mut screen = Screen::new(10, 6, 100);
for row in 0..6 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(format!("Row{}", row).as_bytes());
}
screen.process(b"\x1b[2;5r");
screen.process(b"\x1b[5;1H");
screen.process(b"\r\n");
screen.process(b"New");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Row0", "row above scroll region must be untouched");
assert_eq!(lines[5], "Row5", "row below scroll region must be untouched");
assert_eq!(lines[1], "Row2", "top of region should have what was row 2");
assert_eq!(lines[2], "Row3");
assert_eq!(lines[3], "Row4");
}
#[test]
fn scroll_region_preserves_outer_content_on_multiple_scrolls() {
let mut screen = Screen::new(10, 6, 100);
screen.process(b"\x1b[1;1HHeader");
screen.process(b"\x1b[6;1HFooter");
screen.process(b"\x1b[2;5r");
screen.process(b"\x1b[2;1H");
for i in 0..12 {
if i > 0 {
screen.process(b"\r\n");
}
screen.process(format!("Msg{:02}", i).as_bytes());
}
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Header", "header must survive region scrolling");
assert_eq!(lines[5], "Footer", "footer must survive region scrolling");
}
#[test]
fn csi_s_within_scroll_region() {
let mut screen = Screen::new(10, 6, 100);
for row in 0..6 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(format!("R{}", row).as_bytes());
}
screen.process(b"\x1b[2;5r");
screen.process(b"\x1b[2S");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "R0", "row above region untouched after CSI S");
assert_eq!(lines[5], "R5", "row below region untouched after CSI S");
assert_eq!(lines[1], "R3", "region top after scroll by 2");
assert_eq!(lines[2], "R4", "region second row after scroll by 2");
assert_eq!(lines[3], "", "blank line after scroll");
assert_eq!(lines[4], "", "blank line after scroll");
}
#[test]
fn csi_t_within_scroll_region() {
let mut screen = Screen::new(10, 6, 100);
for row in 0..6 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(format!("R{}", row).as_bytes());
}
screen.process(b"\x1b[2;5r");
screen.process(b"\x1b[2T");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "R0", "row above region untouched after CSI T");
assert_eq!(lines[5], "R5", "row below region untouched after CSI T");
assert_eq!(lines[1], "", "blank line after scroll down");
assert_eq!(lines[2], "", "blank line after scroll down");
assert_eq!(lines[3], "R1", "shifted content after scroll down");
assert_eq!(lines[4], "R2", "shifted content after scroll down");
}
#[test]
fn scroll_down_does_not_generate_scrollback() {
let mut screen = Screen::new(10, 5, 100);
for row in 0..5 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(format!("Row{}", row).as_bytes());
}
screen.process(b"\x1b[3T");
let scrollback = screen.take_pending_scrollback();
assert!(scrollback.is_empty(),
"CSI T (scroll down) should not generate scrollback");
}
#[test]
fn cursor_stays_in_place_during_lf_scroll() {
let mut screen = Screen::new(20, 3, 100);
screen.process(b"Row0\r\nRow1\r\nRow2");
assert_eq!(screen.grid.cursor_y, 2);
assert_eq!(screen.grid.cursor_x, 4);
screen.process(b"\x1b[1;11H"); screen.process(b"\x1b[3;11H");
screen.process(b"\r\n");
assert_eq!(screen.grid.cursor_y, 2, "cursor_y stays at scroll_bottom");
assert_eq!(screen.grid.cursor_x, 0, "cursor_x reset by CR");
}
#[test]
fn cursor_column_preserved_on_bare_lf_scroll() {
let mut screen = Screen::new(20, 3, 100);
screen.process(b"Row0\r\nRow1\r\n");
screen.process(b"\x1b[3;8H"); assert_eq!(screen.grid.cursor_y, 2);
assert_eq!(screen.grid.cursor_x, 7);
screen.process(b"\n");
assert_eq!(screen.grid.cursor_y, 2, "cursor_y stays at scroll_bottom");
assert_eq!(screen.grid.cursor_x, 7, "bare LF preserves cursor_x");
}
#[test]
fn reverse_index_at_top_of_full_screen_scrolls_down() {
let mut screen = Screen::new(10, 4, 100);
screen.process(b"Row0\r\nRow1\r\nRow2\r\nRow3");
screen.process(b"\x1b[1;1H");
screen.process(b"\x1bM");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "", "new blank line at top");
assert_eq!(lines[1], "Row0", "original row 0 shifted down");
assert_eq!(lines[2], "Row1", "original row 1 shifted down");
assert_eq!(lines[3], "Row2", "original row 2 shifted down");
}
#[test]
fn rapid_scroll_up_down_content_integrity() {
let mut screen = Screen::new(10, 5, 100);
for row in 0..5 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(format!("Row{}", row).as_bytes());
}
screen.process(b"\x1b[2S"); screen.process(b"\x1b[2T");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "", "blank at top after scroll down");
assert_eq!(lines[1], "", "blank at top after scroll down");
assert_eq!(lines[2], "Row2", "surviving content");
assert_eq!(lines[3], "Row3", "surviving content");
assert_eq!(lines[4], "Row4", "surviving content shifted back");
}
#[test]
fn lf_scroll_with_styled_content() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"Row0\r\nRow1\r\nRow2");
screen.process(b"\x1b[41m");
screen.process(b"\r\n");
let cell = &screen.grid.cells[2][0];
assert_eq!(cell.c, ' ', "new line should be blank");
assert_eq!(cell.style.bg, Some(style::Color::Indexed(1)), "blank line should inherit current bg (BCE)");
}
#[test]
fn scroll_region_lf_no_scrollback_when_top_nonzero() {
let mut screen = Screen::new(10, 6, 100);
screen.process(b"\x1b[3;6r");
screen.process(b"\x1b[3;1H");
for i in 0..10 {
if i > 0 {
screen.process(b"\r\n");
}
screen.process(format!("Msg{}", i).as_bytes());
}
let scrollback = screen.take_pending_scrollback();
assert!(scrollback.is_empty(),
"scroll_top > 0: no scrollback should be captured");
}
#[test]
fn tui_app_alt_screen_scroll_region_lf() {
let mut screen = Screen::new(20, 6, 100);
screen.process(b"MainContent");
screen.process(b"\x1b[?1049h");
screen.process(b"\x1b[6;1HStatus: OK");
screen.process(b"\x1b[1;5r");
screen.process(b"\x1b[1;1H");
for i in 0..8 {
if i > 0 {
screen.process(b"\r\n");
}
screen.process(format!("Msg{:02}", i).as_bytes());
}
let lines = screen_lines(&screen);
assert_eq!(lines[5], "Status: OK", "status bar must survive scroll");
assert_eq!(lines[0], "Msg03");
assert_eq!(lines[1], "Msg04");
assert_eq!(lines[2], "Msg05");
assert_eq!(lines[3], "Msg06");
assert_eq!(lines[4], "Msg07");
}
#[test]
fn tui_app_alt_screen_explicit_scroll_up() {
let mut screen = Screen::new(15, 5, 100);
screen.process(b"\x1b[?1049h");
for row in 0..5 {
screen.process(format!("\x1b[{};1H", row + 1).as_bytes());
screen.process(format!("Line{}", row).as_bytes());
}
screen.process(b"\x1b[2S");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Line2", "content shifted up by 2");
assert_eq!(lines[1], "Line3");
assert_eq!(lines[2], "Line4");
assert_eq!(lines[3], "", "blank line at bottom");
assert_eq!(lines[4], "", "blank line at bottom");
let scrollback = screen.take_pending_scrollback();
assert!(scrollback.is_empty(),
"alt screen CSI S should not generate scrollback");
}
#[test]
fn tui_app_alt_screen_scroll_then_exit_restores_main() {
let mut screen = Screen::new(15, 4, 100);
screen.process(b"Original0\r\nOriginal1\r\nOriginal2\r\nOriginal3");
screen.process(b"\x1b[?1049h");
for i in 0..20 {
if i > 0 {
screen.process(b"\r\n");
}
screen.process(format!("Alt{}", i).as_bytes());
}
screen.process(b"\x1b[?1049l");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Original0");
assert_eq!(lines[1], "Original1");
assert_eq!(lines[2], "Original2");
assert_eq!(lines[3], "Original3");
}
#[test]
fn tui_app_scroll_region_with_header_and_footer() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b[?1049h");
screen.process(b"\x1b[1;1H== My App ==");
screen.process(b"\x1b[5;1H[Ctrl+C quit]");
screen.process(b"\x1b[2;4r");
screen.process(b"\x1b[2;1H");
for i in 0..10 {
if i > 0 {
screen.process(b"\r\n");
}
screen.process(format!("item{}", i).as_bytes());
}
let lines = screen_lines(&screen);
assert_eq!(lines[0], "== My App ==", "header must be preserved");
assert_eq!(lines[4], "[Ctrl+C quit]", "footer must be preserved");
assert_eq!(lines[1], "item7");
assert_eq!(lines[2], "item8");
assert_eq!(lines[3], "item9");
}
#[test]
fn tui_app_delete_lines_within_scroll_region() {
let mut screen = Screen::new(15, 6, 100);
screen.process(b"\x1b[?1049h");
screen.process(b"\x1b[1;1HTitle");
screen.process(b"\x1b[6;1HStatus");
screen.process(b"\x1b[2;5r");
screen.process(b"\x1b[2;1HItem-A");
screen.process(b"\x1b[3;1HItem-B");
screen.process(b"\x1b[4;1HItem-C");
screen.process(b"\x1b[5;1HItem-D");
screen.process(b"\x1b[3;1H");
screen.process(b"\x1b[M");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Title", "title preserved");
assert_eq!(lines[5], "Status", "status preserved");
assert_eq!(lines[1], "Item-A", "Item-A stays");
assert_eq!(lines[2], "Item-C", "Item-C shifted up into Item-B's position");
assert_eq!(lines[3], "Item-D", "Item-D shifted up");
assert_eq!(lines[4], "", "blank line at region bottom");
}
#[test]
fn tui_app_insert_lines_within_scroll_region() {
let mut screen = Screen::new(15, 6, 100);
screen.process(b"\x1b[?1049h");
screen.process(b"\x1b[1;1HTitle");
screen.process(b"\x1b[6;1HStatus");
screen.process(b"\x1b[2;5r");
screen.process(b"\x1b[2;1HItem-A");
screen.process(b"\x1b[3;1HItem-B");
screen.process(b"\x1b[4;1HItem-C");
screen.process(b"\x1b[5;1HItem-D");
screen.process(b"\x1b[3;1H");
screen.process(b"\x1b[L"); screen.process(b"NEW ITEM");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Title", "title preserved");
assert_eq!(lines[5], "Status", "status preserved");
assert_eq!(lines[1], "Item-A", "Item-A stays");
assert_eq!(lines[2], "NEW ITEM", "new item inserted");
assert_eq!(lines[3], "Item-B", "Item-B shifted down");
assert_eq!(lines[4], "Item-C", "Item-C shifted down");
}
#[test]
fn tui_app_reverse_index_in_scroll_region() {
let mut screen = Screen::new(15, 6, 100);
screen.process(b"\x1b[?1049h");
screen.process(b"\x1b[1;1HHeader");
screen.process(b"\x1b[6;1HFooter");
screen.process(b"\x1b[2;5r");
screen.process(b"\x1b[2;1HMsg-A");
screen.process(b"\x1b[3;1HMsg-B");
screen.process(b"\x1b[4;1HMsg-C");
screen.process(b"\x1b[5;1HMsg-D");
screen.process(b"\x1b[2;1H");
screen.process(b"\x1bM");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Header", "header preserved");
assert_eq!(lines[5], "Footer", "footer preserved");
assert_eq!(lines[1], "", "new blank line at region top");
assert_eq!(lines[2], "Msg-A", "Msg-A shifted down");
assert_eq!(lines[3], "Msg-B", "Msg-B shifted down");
assert_eq!(lines[4], "Msg-C", "Msg-C shifted down");
}
#[test]
fn tui_app_scroll_region_change_mid_session() {
let mut screen = Screen::new(20, 8, 100);
screen.process(b"\x1b[?1049h");
screen.process(b"\x1b[1;1HTop Bar");
screen.process(b"\x1b[8;1HBottom Bar");
screen.process(b"\x1b[2;7r");
screen.process(b"\x1b[2;1H");
for i in 0..6 {
if i > 0 { screen.process(b"\r\n"); }
screen.process(format!("V1_{}", i).as_bytes());
}
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Top Bar");
assert_eq!(lines[7], "Bottom Bar");
screen.process(b"\x1b[2;4r");
screen.process(b"\x1b[5;1HPanel-A");
screen.process(b"\x1b[6;1HPanel-B");
screen.process(b"\x1b[7;1HPanel-C");
screen.process(b"\x1b[2;1H");
for i in 0..6 {
if i > 0 { screen.process(b"\r\n"); }
screen.process(format!("V2_{}", i).as_bytes());
}
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Top Bar", "top bar survives region change");
assert_eq!(lines[7], "Bottom Bar", "bottom bar survives region change");
assert_eq!(lines[4], "Panel-A", "panel row survives scroll in smaller region");
assert_eq!(lines[5], "Panel-B", "panel row survives scroll in smaller region");
assert_eq!(lines[6], "Panel-C", "panel row survives scroll in smaller region");
}
#[test]
fn scroll_up_count_exceeds_region_size() {
let mut screen = Screen::new(10, 6, 100);
for row in 0..6 {
screen.process(format!("\x1b[{};1HR{}", row + 1, row).as_bytes());
}
screen.process(b"\x1b[2;4r");
screen.process(b"\x1b[100S");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "R0", "row above region untouched");
assert_eq!(lines[5], "R5", "row below region untouched");
assert_eq!(lines[1], "", "region blanked");
assert_eq!(lines[2], "", "region blanked");
assert_eq!(lines[3], "", "region blanked");
}
#[test]
fn scroll_down_count_exceeds_region_size() {
let mut screen = Screen::new(10, 6, 100);
for row in 0..6 {
screen.process(format!("\x1b[{};1HR{}", row + 1, row).as_bytes());
}
screen.process(b"\x1b[2;4r");
screen.process(b"\x1b[100T");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "R0", "row above region untouched");
assert_eq!(lines[5], "R5", "row below region untouched");
assert_eq!(lines[1], "", "region blanked");
assert_eq!(lines[2], "", "region blanked");
assert_eq!(lines[3], "", "region blanked");
}
#[test]
fn lf_at_last_row_outside_scroll_region() {
let mut screen = Screen::new(10, 6, 100);
screen.process(b"\x1b[6;1HBottom");
screen.process(b"\x1b[1;4r");
screen.process(b"\x1b[6;1H");
assert_eq!(screen.grid.cursor_y, 5);
screen.process(b"\n");
assert_eq!(screen.grid.cursor_y, 5, "cursor stuck at last row outside region");
let lines = screen_lines(&screen);
assert_eq!(lines[5], "Bottom", "content at last row unchanged");
}
#[test]
fn lf_between_scroll_top_and_bottom_just_moves_cursor() {
let mut screen = Screen::new(10, 6, 100);
for row in 0..6 {
screen.process(format!("\x1b[{};1HR{}", row + 1, row).as_bytes());
}
screen.process(b"\x1b[2;5r"); screen.process(b"\x1b[3;1H");
screen.process(b"\n");
assert_eq!(screen.grid.cursor_y, 3, "cursor moved down by 1");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "R0");
assert_eq!(lines[1], "R1");
assert_eq!(lines[2], "R2");
assert_eq!(lines[3], "R3");
assert_eq!(lines[4], "R4");
assert_eq!(lines[5], "R5");
}
#[test]
fn autowrap_at_scroll_bottom_triggers_scroll() {
let mut screen = Screen::new(4, 4, 100);
screen.process(b"AAA0\r\nBBB1\r\nCCC2\r\nDDD3");
assert!(screen.grid.wrap_pending, "wrap_pending after filling last cell");
screen.process(b"X");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "BBB1", "scroll happened: AAA0 gone");
assert_eq!(lines[1], "CCC2");
assert_eq!(lines[2], "DDD3");
assert_eq!(lines[3], "X", "new char on fresh line");
}
#[test]
fn autowrap_at_scroll_region_bottom_triggers_region_scroll() {
let mut screen = Screen::new(5, 6, 100);
screen.process(b"\x1b[1;1HHead");
screen.process(b"\x1b[6;1HFoot");
screen.process(b"\x1b[2;5r");
screen.process(b"\x1b[2;1HAAAAA"); screen.process(b"\x1b[3;1HBBBBB"); screen.process(b"\x1b[4;1HCCCCC"); screen.process(b"\x1b[5;1HDDDDD");
screen.process(b"Z");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Head", "header survives autowrap scroll");
assert_eq!(lines[5], "Foot", "footer survives autowrap scroll");
assert_eq!(lines[1], "BBBBB", "AAAAA scrolled off, BBBBB now at region top");
assert_eq!(lines[4], "Z", "Z on new line at region bottom");
}
#[test]
fn wide_char_wrap_at_scroll_bottom_triggers_scroll() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"Row0\r\nRow1\r\n");
screen.process(b"ABCD");
assert_eq!(screen.grid.cursor_x, 4);
screen.process("你".as_bytes());
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Row1", "Row0 scrolled off");
assert_eq!(lines[1], "ABCD", "original row with blank at col 4");
assert_eq!(screen.grid.cells[2][0].c, '你', "wide char on scrolled-in line");
assert_eq!(screen.grid.cells[2][0].width, 2);
}
#[test]
fn csi_r_reset_restores_full_screen_scroll() {
let mut screen = Screen::new(10, 6, 100);
screen.process(b"\x1b[2;5r");
assert_eq!(screen.grid.scroll_top, 1);
assert_eq!(screen.grid.scroll_bottom, 4);
screen.process(b"\x1b[r");
assert_eq!(screen.grid.scroll_top, 0, "scroll_top reset to 0");
assert_eq!(screen.grid.scroll_bottom, 5, "scroll_bottom reset to rows-1");
}
#[test]
fn scroll_single_row_region() {
let mut screen = Screen::new(10, 6, 100);
let old_top = screen.grid.scroll_top;
let old_bottom = screen.grid.scroll_bottom;
screen.process(b"\x1b[3;3r");
assert_eq!(screen.grid.scroll_top, old_top, "invalid region not applied");
assert_eq!(screen.grid.scroll_bottom, old_bottom, "invalid region not applied");
assert_eq!(screen.grid.cursor_x, 0);
assert_eq!(screen.grid.cursor_y, 0);
}
#[test]
fn scroll_region_reversed_params_ignored() {
let mut screen = Screen::new(10, 20, 100);
let old_top = screen.grid.scroll_top;
let old_bottom = screen.grid.scroll_bottom;
screen.process(b"\x1b[15;5r");
assert_eq!(screen.grid.scroll_top, old_top);
assert_eq!(screen.grid.scroll_bottom, old_bottom);
}
#[test]
fn scrollback_limit_enforced_during_scroll() {
let limit = 5;
let mut screen = Screen::new(10, 3, limit);
for i in 0..20 {
if i > 0 { screen.process(b"\r\n"); }
screen.process(format!("L{:02}", i).as_bytes());
}
let hist = history_texts(&screen);
assert_eq!(hist.len(), limit,
"scrollback should be exactly at limit {}, got {}", limit, hist.len());
assert_eq!(hist[0], "L12", "oldest scrollback should be evicted");
}
#[test]
fn csi_s_does_not_move_cursor() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"\x1b[3;5H"); assert_eq!(screen.grid.cursor_y, 2);
assert_eq!(screen.grid.cursor_x, 4);
screen.process(b"\x1b[2S");
assert_eq!(screen.grid.cursor_y, 2, "CSI S must not change cursor_y");
assert_eq!(screen.grid.cursor_x, 4, "CSI S must not change cursor_x");
}
#[test]
fn csi_t_does_not_move_cursor() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"\x1b[3;5H");
assert_eq!(screen.grid.cursor_y, 2);
assert_eq!(screen.grid.cursor_x, 4);
screen.process(b"\x1b[2T");
assert_eq!(screen.grid.cursor_y, 2, "CSI T must not change cursor_y");
assert_eq!(screen.grid.cursor_x, 4, "CSI T must not change cursor_x");
}
#[test]
fn scroll_then_overwrite_last_line() {
let mut screen = Screen::new(20, 4, 100);
screen.process(b"compile a.c\r\n");
screen.process(b"compile b.c\r\n");
screen.process(b"compile c.c\r\n");
screen.process(b"compile d.c");
screen.process(b"\r\ncompile e.c");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "compile b.c");
assert_eq!(lines[1], "compile c.c");
assert_eq!(lines[2], "compile d.c");
assert_eq!(lines[3], "compile e.c");
let hist = history_texts(&screen);
assert_eq!(hist.len(), 1);
assert_eq!(hist[0], "compile a.c");
}
#[test]
fn scroll_then_cup_overwrite_middle() {
let mut screen = Screen::new(20, 5, 100);
for i in 0..5 {
if i > 0 { screen.process(b"\r\n"); }
screen.process(format!("Line{}", i).as_bytes());
}
screen.process(b"\r\nLine5\r\nLine6");
screen.process(b"\x1b[1;1H");
screen.process(b"[=====> ] 50%");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "[=====> ] 50%", "row 0 overwritten after scroll");
assert_eq!(lines[1], "Line3", "row 1 unchanged");
assert_eq!(lines[2], "Line4");
assert_eq!(lines[3], "Line5");
assert_eq!(lines[4], "Line6");
let hist = history_texts(&screen);
assert_eq!(hist.len(), 2);
assert_eq!(hist[0], "Line0");
assert_eq!(hist[1], "Line1");
}
#[test]
fn continuous_scroll_with_bottom_status_update() {
let mut screen = Screen::new(30, 5, 100);
screen.process(b"\x1b[1;4r");
screen.process(b"\x1b[5;1HProgress: 0%");
screen.process(b"\x1b[1;1H");
for i in 0..10 {
if i > 0 { screen.process(b"\r\n"); }
screen.process(format!("Installing pkg-{:02}...", i).as_bytes());
let pct = (i + 1) * 10;
screen.process(format!("\x1b7\x1b[5;1H\x1b[2KProgress: {}%\x1b8", pct).as_bytes());
}
let lines = screen_lines(&screen);
assert_eq!(lines[4], "Progress: 100%", "status line updated");
assert_eq!(lines[0], "Installing pkg-06...");
assert_eq!(lines[1], "Installing pkg-07...");
assert_eq!(lines[2], "Installing pkg-08...");
assert_eq!(lines[3], "Installing pkg-09...");
}
#[test]
fn scroll_with_erase_and_rewrite() {
let mut screen = Screen::new(20, 3, 100);
screen.process(b"Step 1: done\r\n");
screen.process(b"Step 2: done\r\n");
screen.process(b"Step 3: running...");
screen.process(b"\r\nStep 4: running...");
screen.process(b"\x1b[2;9H"); screen.process(b"\x1b[0K"); screen.process(b"done");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Step 2: done");
assert_eq!(lines[1], "Step 3: done", "rewritten after scroll");
assert_eq!(lines[2], "Step 4: running...");
}
#[test]
fn scroll_region_scroll_then_overwrite_fixed_rows() {
let mut screen = Screen::new(25, 6, 100);
screen.process(b"\x1b[1;1HTitle: My App v1.0");
screen.process(b"\x1b[6;1HItems: 0");
screen.process(b"\x1b[2;5r");
screen.process(b"\x1b[2;1H");
for i in 0..8 {
if i > 0 { screen.process(b"\r\n"); }
screen.process(format!("Item #{}", i + 1).as_bytes());
screen.process(format!("\x1b7\x1b[6;1H\x1b[2KItems: {}\x1b8", i + 1).as_bytes());
}
screen.process(b"\x1b7\x1b[1;1H\x1b[2KTitle: My App v2.0\x1b8");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Title: My App v2.0", "header updated after scrolling");
assert_eq!(lines[5], "Items: 8", "footer updated with count");
assert_eq!(lines[1], "Item #5");
assert_eq!(lines[2], "Item #6");
assert_eq!(lines[3], "Item #7");
assert_eq!(lines[4], "Item #8");
}
#[test]
fn scroll_then_overwrite_scrolled_line_content_integrity() {
let mut screen = Screen::new(10, 5, 100);
for i in 0..8 {
if i > 0 { screen.process(b"\r\n"); }
screen.process(format!("L{:02}", i).as_bytes());
}
screen.process(b"\x1b[3;1H\x1b[2K");
screen.process(b"REPLACED");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "L03", "line above overwrite unchanged");
assert_eq!(lines[1], "L04", "line above overwrite unchanged");
assert_eq!(lines[2], "REPLACED", "middle line replaced");
assert_eq!(lines[3], "L06", "line below overwrite unchanged");
assert_eq!(lines[4], "L07", "line below overwrite unchanged");
let hist = history_texts(&screen);
assert_eq!(hist.len(), 3);
assert_eq!(hist[0], "L00");
assert_eq!(hist[1], "L01");
assert_eq!(hist[2], "L02");
}
#[test]
fn interleaved_scroll_and_cup_writes() {
let mut screen = Screen::new(15, 4, 100);
screen.process(b"A\r\nB\r\nC\r\nD");
screen.process(b"\r\nE");
screen.process(b"\x1b[1;1H\x1b[2KHeader");
screen.process(b"\x1b[4;1H"); screen.process(b"\r\nF");
screen.process(b"\x1b[1;1H\x1b[2KNewHdr");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "NewHdr");
assert_eq!(lines[1], "D");
assert_eq!(lines[2], "E");
assert_eq!(lines[3], "F");
let hist = history_texts(&screen);
assert_eq!(hist.len(), 2);
assert_eq!(hist[0], "A", "first scrolled-off line");
assert_eq!(hist[1], "Header", "overwritten row scrolled off");
}
#[test]
fn scroll_and_partial_line_overwrite() {
let mut screen = Screen::new(20, 3, 100);
screen.process(b"AAAAAAAAAAAAAAAAAAAA\r\n"); screen.process(b"BBBBBBBBBBBBBBBBBBBB\r\n"); screen.process(b"CCCCCCCCCCCCCCCCCCCC");
screen.process(b"\r\nDDDDDDDDDDDDDDDDDDDD");
screen.process(b"\x1b[1;1H");
screen.process(b"XYZ");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "XYZBBBBBBBBBBBBBBBBB", "partial overwrite: first 3 replaced, rest intact");
assert_eq!(lines[1], "CCCCCCCCCCCCCCCCCCCC", "row 1 untouched");
assert_eq!(lines[2], "DDDDDDDDDDDDDDDDDDDD", "row 2 untouched");
}
#[test]
fn ind_esc_d_at_scroll_bottom_scrolls() {
let mut screen = Screen::new(10, 4, 100);
screen.process(b"Row0\r\nRow1\r\nRow2\r\nRow3");
screen.process(b"\x1bD");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Row1", "ESC D at bottom should scroll: Row0 gone");
assert_eq!(lines[1], "Row2");
assert_eq!(lines[2], "Row3");
assert_eq!(lines[3], "", "new blank line at bottom after IND scroll");
}
#[test]
fn ind_esc_d_mid_screen_just_moves_cursor() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"\x1b[2;1H"); screen.process(b"\x1bD");
assert_eq!(screen.grid.cursor_y, 2, "ESC D should move cursor down by 1");
}
#[test]
fn save_cursor_scroll_restore_cursor_writes_at_shifted_content() {
let mut screen = Screen::new(10, 4, 100);
screen.process(b"Row0\r\nRow1\r\nRow2\r\nRow3");
screen.process(b"\x1b[2;6H"); screen.process(b"\x1b7");
screen.process(b"\x1b[2S");
screen.process(b"\x1b8");
assert_eq!(screen.grid.cursor_y, 1, "restored cursor_y");
assert_eq!(screen.grid.cursor_x, 5, "restored cursor_x");
screen.process(b"!");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "Row2", "row 0 after scroll");
assert_eq!(lines[1], "Row3 !", "cursor writes at restored position in shifted content");
}
#[test]
fn erase_display_ignores_scroll_region() {
let mut screen = Screen::new(10, 6, 100);
for row in 0..6 {
screen.process(format!("\x1b[{};1HR{}", row + 1, row).as_bytes());
}
screen.process(b"\x1b[2;5r");
screen.process(b"\x1b[2J");
let lines = screen_lines(&screen);
for (i, line) in lines.iter().enumerate() {
assert_eq!(line, &"", "row {} should be erased by CSI 2J (regardless of scroll region)", i);
}
}
#[test]
fn erase_display_0_from_cursor_ignores_scroll_region() {
let mut screen = Screen::new(10, 6, 100);
for row in 0..6 {
screen.process(format!("\x1b[{};1HR{}", row + 1, row).as_bytes());
}
screen.process(b"\x1b[2;4r");
screen.process(b"\x1b[3;1H");
screen.process(b"\x1b[0J");
let lines = screen_lines(&screen);
assert_eq!(lines[0], "R0", "row above cursor preserved");
assert_eq!(lines[1], "R1", "row above cursor preserved");
assert_eq!(lines[2], "", "cursor row erased");
assert_eq!(lines[3], "", "row below cursor erased");
assert_eq!(lines[4], "", "row below region erased (ED ignores region)");
assert_eq!(lines[5], "", "last row erased (ED ignores region)");
}
#[test]
fn reattach_render_after_scroll_and_cup_overwrite() {
let mut screen = Screen::new(15, 4, 100);
for i in 0..7 {
if i > 0 { screen.process(b"\r\n"); }
screen.process(format!("Line{}", i).as_bytes());
}
screen.process(b"\x1b[2;1H\x1b[2KModified");
let mut cache = render::RenderCache::new();
let output = screen.render(true, &mut cache);
let rendered = String::from_utf8_lossy(&output);
assert!(rendered.contains("Line3"), "reattach should show Line3");
assert!(rendered.contains("Modified"), "reattach should show Modified (not Line4)");
assert!(rendered.contains("Line5"), "reattach should show Line5");
assert!(rendered.contains("Line6"), "reattach should show Line6");
assert!(!rendered.contains("Line4"), "Line4 was overwritten, should not appear");
}
#[test]
fn scrollback_history_after_scroll_and_cup_overwrite() {
let mut screen = Screen::new(15, 3, 100);
screen.process(b"Original\r\nRow1\r\nRow2");
screen.process(b"\x1b[1;1H\x1b[2KChanged");
screen.process(b"\x1b[3;1H\r\nRow3");
let hist = history_texts(&screen);
assert_eq!(hist.len(), 1);
assert_eq!(hist[0], "Changed", "scrollback should have the modified content, not 'Original'");
}
#[test]
fn scroll_region_lf_cursor_outside_region_between_bottom_and_last_row() {
let mut screen = Screen::new(10, 8, 100);
for row in 0..8 {
screen.process(format!("\x1b[{};1HR{}", row + 1, row).as_bytes());
}
screen.process(b"\x1b[1;4r");
screen.process(b"\x1b[6;1H");
assert_eq!(screen.grid.cursor_y, 5);
screen.process(b"\n");
assert_eq!(screen.grid.cursor_y, 6, "cursor moves down, no scroll");
let lines = screen_lines(&screen);
for row in 0..8 {
assert_eq!(lines[row], format!("R{}", row), "row {} unchanged", row);
}
}
#[test]
fn combining_mark_at_column_zero_no_previous_cell() {
let mut screen = Screen::new(10, 3, 100);
screen.process("\u{0301}".as_bytes()); assert_eq!(screen.grid.cursor_x, 0);
assert_eq!(screen.grid.cells[0][0].c, ' ');
assert!(screen.grid.cells[0][0].combining.is_empty());
}
#[test]
fn multiple_combining_marks_on_single_cell() {
let mut screen = Screen::new(80, 3, 100);
screen.process("e\u{0301}\u{0308}".as_bytes());
assert_eq!(screen.grid.cells[0][0].c, 'e');
assert_eq!(screen.grid.cells[0][0].combining.len(), 2);
assert_eq!(screen.grid.cells[0][0].combining[0], '\u{0301}');
assert_eq!(screen.grid.cells[0][0].combining[1], '\u{0308}');
}
#[test]
fn wide_char_exactly_fills_line() {
let mut screen = Screen::new(4, 3, 100);
screen.process(b"AB"); screen.process("你".as_bytes()); assert_eq!(screen.grid.cells[0][2].c, '你');
assert_eq!(screen.grid.cells[0][2].width, 2);
assert_eq!(screen.grid.cells[0][3].width, 0);
assert_eq!(screen.grid.cursor_y, 0, "should NOT have wrapped");
assert!(screen.grid.wrap_pending, "wrap should be pending after filling line");
}
#[test]
fn wide_char_on_2_column_terminal() {
let mut screen = Screen::new(2, 3, 100);
screen.process("你".as_bytes());
assert_eq!(screen.grid.cells[0][0].c, '你');
assert_eq!(screen.grid.cells[0][0].width, 2);
assert_eq!(screen.grid.cells[0][1].width, 0);
assert_eq!(screen.grid.cursor_y, 0);
assert!(screen.grid.wrap_pending);
}
#[test]
fn wide_char_on_1_column_terminal() {
let mut screen = Screen::new(1, 3, 100);
screen.process("你".as_bytes());
assert_eq!(screen.grid.cursor_y, 0, "wide char should be dropped on 1-col terminal");
assert_eq!(screen.grid.cursor_x, 0);
assert_eq!(screen.grid.cells[0][0].c, ' ', "cell should remain blank");
}
#[test]
fn wide_char_no_autowrap_at_last_col() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"\x1b[?7l"); screen.process(b"ABCD"); screen.process("你".as_bytes()); assert_eq!(screen.grid.cells[0][3].c, 'D');
assert_eq!(screen.grid.cells[0][4].c, ' ', "wide char dropped, col 4 stays blank");
assert_eq!(screen.grid.cursor_y, 0, "no wrap");
}
#[test]
fn rep_with_no_prior_print() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"\x1b[3b"); assert_eq!(screen.grid.cursor_x, 3);
assert_eq!(screen.grid.cells[0][0].c, ' ');
assert_eq!(screen.grid.cells[0][1].c, ' ');
assert_eq!(screen.grid.cells[0][2].c, ' ');
}
#[test]
fn restore_cursor_with_no_saved_state() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[5;10H"); screen.process(b"\x1b8"); assert_eq!(screen.grid.cursor_y, 4);
assert_eq!(screen.grid.cursor_x, 9);
}
#[test]
fn restore_cursor_csi_u_with_no_saved_state() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[3;5H");
screen.process(b"\x1b[u"); assert_eq!(screen.grid.cursor_y, 2);
assert_eq!(screen.grid.cursor_x, 4);
}
#[test]
fn save_cursor_resize_then_restore_clamps() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[20;70H"); screen.process(b"\x1b7"); screen.resize(40, 10); screen.process(b"\x1b8"); assert_eq!(screen.grid.cursor_x, 39, "restored x should clamp to cols-1");
assert_eq!(screen.grid.cursor_y, 9, "restored y should clamp to rows-1");
}
#[test]
fn double_enter_alt_screen() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"Main");
screen.process(b"\x1b[?1049h"); assert!(screen.state.in_alt_screen);
screen.process(b"Alt1");
screen.process(b"\x1b[?1049h"); assert!(screen.state.in_alt_screen);
assert_eq!(screen.grid.cells[0][0].c, 'A');
screen.process(b"\x1b[?1049l"); assert!(!screen.state.in_alt_screen);
assert_eq!(screen.grid.cells[0][0].c, 'M');
assert_eq!(screen.grid.cells[0][3].c, 'n');
}
#[test]
fn exit_alt_screen_when_not_in_alt() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"Hello");
screen.process(b"\x1b[2;4r");
screen.process(b"\x1b[3;5H"); let sr_top = screen.grid.scroll_top;
let sr_bot = screen.grid.scroll_bottom;
let cx = screen.grid.cursor_x;
let cy = screen.grid.cursor_y;
screen.process(b"\x1b[?1049l");
assert!(!screen.state.in_alt_screen);
assert_eq!(screen.grid.scroll_top, sr_top, "scroll region top must not change");
assert_eq!(screen.grid.scroll_bottom, sr_bot, "scroll region bottom must not change");
assert_eq!(screen.grid.cursor_x, cx, "cursor x must not change");
assert_eq!(screen.grid.cursor_y, cy, "cursor y must not change");
}
#[test]
fn hts_sets_tab_stop() {
let mut screen = Screen::new(80, 3, 100);
screen.process(b"\x1b[1;5H"); screen.process(b"\x1bH"); screen.process(b"\x1b[1;1H"); screen.process(b"\t"); assert_eq!(screen.grid.cursor_x, 4);
}
#[test]
fn tbc_clear_current_tab_stop() {
let mut screen = Screen::new(80, 3, 100);
screen.process(b"\x1b[1;9H"); screen.process(b"\x1b[0g"); screen.process(b"\x1b[1;1H"); screen.process(b"\t"); assert_eq!(screen.grid.cursor_x, 16);
}
#[test]
fn tbc_clear_all_tab_stops() {
let mut screen = Screen::new(80, 3, 100);
screen.process(b"\x1b[3g"); screen.process(b"\t"); assert_eq!(screen.grid.cursor_x, 79);
}
#[test]
fn scroll_region_top_equals_bottom_rejected() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[5;5r"); assert_eq!(screen.grid.scroll_top, 0);
assert_eq!(screen.grid.scroll_bottom, 23);
}
#[test]
fn scroll_region_reversed_params_rejected() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[15;5r"); assert_eq!(screen.grid.scroll_top, 0);
assert_eq!(screen.grid.scroll_bottom, 23);
}
#[test]
fn ech_count_exceeds_remaining_columns() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ");
screen.process(b"\x1b[1;6H"); screen.process(b"\x1b[100X"); for i in 0..5 {
assert_ne!(screen.grid.cells[0][i].c, ' ');
}
for i in 5..10 {
assert_eq!(screen.grid.cells[0][i].c, ' ',
"col {} should be erased", i);
}
}
#[test]
fn dch_at_end_of_line() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ");
screen.process(b"\x1b[1;10H"); screen.process(b"\x1b[P"); assert_eq!(screen.grid.cells[0][8].c, 'I');
assert_eq!(screen.grid.cells[0][9].c, ' ');
}
#[test]
fn dch_count_exceeds_remaining_columns() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ");
screen.process(b"\x1b[1;6H"); screen.process(b"\x1b[100P"); for (i, ch) in "ABCDE".chars().enumerate() {
assert_eq!(screen.grid.cells[0][i].c, ch);
}
for i in 5..10 {
assert_eq!(screen.grid.cells[0][i].c, ' ',
"col {} should be blank after large DCH", i);
}
}
#[test]
fn ich_count_exceeds_remaining_columns() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ");
screen.process(b"\x1b[1;1H"); screen.process(b"\x1b[100@"); for i in 0..10 {
assert_eq!(screen.grid.cells[0][i].c, ' ',
"col {} should be blank after large ICH", i);
}
}
#[test]
fn ind_esc_d_within_scroll_region_nonzero_top() {
let mut screen = Screen::new(10, 6, 100);
for row in 0..6 {
screen.process(format!("\x1b[{};1HR{}", row + 1, row).as_bytes());
}
screen.process(b"\x1b[2;4r");
screen.process(b"\x1b[4;1H");
assert_eq!(screen.grid.cursor_y, 3);
screen.process(b"\x1bD");
assert_eq!(screen.grid.cells[0][0].c, 'R');
assert_eq!(screen.grid.cells[0][1].c, '0');
assert_eq!(screen.grid.cells[5][0].c, 'R');
assert_eq!(screen.grid.cells[5][1].c, '5');
assert_eq!(screen.grid.cells[1][1].c, '2');
}
#[test]
fn ed_0_cursor_on_wide_char_continuation() {
let mut screen = Screen::new(10, 3, 100);
screen.process("AB你CD".as_bytes());
screen.process(b"\x1b[1;4H"); screen.process(b"\x1b[J"); assert_eq!(screen.grid.cells[0][2].c, ' ', "first half of wide char should be blanked");
assert_eq!(screen.grid.cells[0][3].c, ' ', "continuation cell should be blanked");
}
#[test]
fn el_1_cursor_on_wide_char_continuation() {
let mut screen = Screen::new(10, 3, 100);
screen.process("你BCDE".as_bytes());
screen.process(b"\x1b[1;2H"); screen.process(b"\x1b[1K"); assert_eq!(screen.grid.cells[0][0].c, ' ');
assert_eq!(screen.grid.cells[0][1].c, ' ');
assert_eq!(screen.grid.cells[0][2].c, 'B');
}
#[test]
fn ris_preserves_scrollback() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"A\r\nB\r\nC\r\nD\r\nE");
let _ = screen.take_pending_scrollback();
let hist_before = screen.get_history().len();
assert!(hist_before > 0);
screen.process(b"\x1bc"); let hist_after = screen.get_history().len();
assert_eq!(hist_after, hist_before,
"ESC c should not clear scrollback history");
}
#[test]
fn scrollback_limit_zero() {
let mut screen = Screen::new(10, 3, 0);
screen.process(b"A\r\nB\r\nC\r\nD\r\nE");
let hist = screen.get_history();
assert_eq!(hist.len(), 0,
"scrollback with limit=0 should store nothing, got {}", hist.len());
let pending = screen.take_pending_scrollback();
assert!(pending.is_empty(),
"pending scrollback with limit=0 should be empty");
}
#[test]
fn csi_f_is_alias_for_cup() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[5;10f"); assert_eq!(screen.grid.cursor_y, 4);
assert_eq!(screen.grid.cursor_x, 9);
}
#[test]
fn csi_cup_zero_params_default_to_home() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[5;10H"); screen.process(b"\x1b[H"); assert_eq!(screen.grid.cursor_y, 0);
assert_eq!(screen.grid.cursor_x, 0);
}
#[test]
fn csi_cup_params_beyond_screen_clamp() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[999;999H");
assert_eq!(screen.grid.cursor_y, 23);
assert_eq!(screen.grid.cursor_x, 79);
}
#[test]
fn dsr_at_boundary_positions() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[1;1H");
screen.process(b"\x1b[6n");
let r = screen.take_responses();
assert_eq!(r[0], b"\x1b[1;1R");
screen.process(b"\x1b[24;80H");
screen.process(b"\x1b[6n");
let r = screen.take_responses();
assert_eq!(r[0], b"\x1b[24;80R");
}
#[test]
fn responses_drained_after_take() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[6n");
let r1 = screen.take_responses();
assert_eq!(r1.len(), 1);
let r2 = screen.take_responses();
assert!(r2.is_empty(), "responses should be empty after drain");
}
#[test]
fn cursor_movement_with_zero_params_defaults() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[10;10H"); screen.process(b"\x1b[A"); assert_eq!(screen.grid.cursor_y, 8);
screen.process(b"\x1b[B"); assert_eq!(screen.grid.cursor_y, 9);
screen.process(b"\x1b[C"); assert_eq!(screen.grid.cursor_x, 10);
screen.process(b"\x1b[D"); assert_eq!(screen.grid.cursor_x, 9);
}
#[test]
fn cursor_movement_beyond_bounds_clamps() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"\x1b[3;5H"); screen.process(b"\x1b[999A"); assert_eq!(screen.grid.cursor_y, 0);
screen.process(b"\x1b[999B"); assert_eq!(screen.grid.cursor_y, 4);
screen.process(b"\x1b[999D"); assert_eq!(screen.grid.cursor_x, 0);
screen.process(b"\x1b[999C"); assert_eq!(screen.grid.cursor_x, 9);
}
#[test]
fn overwrite_wide_char_first_half() {
let mut screen = Screen::new(10, 3, 100);
screen.process("你好".as_bytes()); screen.process(b"\x1b[1;1H"); screen.process(b"X"); assert_eq!(screen.grid.cells[0][0].c, 'X');
assert_eq!(screen.grid.cells[0][0].width, 1);
assert_eq!(screen.grid.cells[0][1].c, ' ', "continuation should be blanked");
assert_eq!(screen.grid.cells[0][1].width, 1);
}
#[test]
fn overwrite_wide_char_second_half() {
let mut screen = Screen::new(10, 3, 100);
screen.process("你好".as_bytes());
screen.process(b"\x1b[1;2H"); screen.process(b"X"); assert_eq!(screen.grid.cells[0][0].c, ' ', "first half should be blanked");
assert_eq!(screen.grid.cells[0][0].width, 1);
assert_eq!(screen.grid.cells[0][1].c, 'X');
assert_eq!(screen.grid.cells[0][1].width, 1);
}
#[test]
fn lf_at_last_row_without_scroll_region() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"\x1b[3;1H"); screen.process(b"X");
screen.process(b"\n"); assert_eq!(screen.grid.cursor_y, 2);
}
#[test]
fn cr_does_not_scroll() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ"); screen.process(b"\r"); assert_eq!(screen.grid.cursor_x, 0);
assert_eq!(screen.grid.cursor_y, 0, "CR should stay on same row");
}
#[test]
fn sgr_color_reset_39_49() {
let mut screen = Screen::new(80, 3, 100);
screen.process(b"\x1b[31;42m"); screen.process(b"A");
assert!(screen.grid.cells[0][0].style.fg.is_some());
assert!(screen.grid.cells[0][0].style.bg.is_some());
screen.process(b"\x1b[39m"); screen.process(b"B");
assert!(screen.grid.cells[0][1].style.fg.is_none(), "fg should be reset");
assert!(screen.grid.cells[0][1].style.bg.is_some(), "bg should remain");
screen.process(b"\x1b[49m"); screen.process(b"C");
assert!(screen.grid.cells[0][2].style.fg.is_none());
assert!(screen.grid.cells[0][2].style.bg.is_none(), "bg should be reset");
}
#[test]
fn scroll_down_within_region_does_not_produce_scrollback() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"\x1b[2;4r"); screen.process(b"\x1b[2;1H"); screen.process(b"\x1b[3T"); let pending = screen.take_pending_scrollback();
assert!(pending.is_empty(), "scroll down should never produce scrollback");
}
#[test]
fn mouse_mode_disable_resets_to_zero() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[?1003h"); assert_eq!(screen.grid.modes.mouse_mode, 1003);
screen.process(b"\x1b[?1003l"); assert_eq!(screen.grid.modes.mouse_mode, 0);
screen.process(b"\x1b[?1002h");
assert_eq!(screen.grid.modes.mouse_mode, 1002);
screen.process(b"\x1b[?1000l"); assert_eq!(screen.grid.modes.mouse_mode, 0);
}
#[test]
fn mouse_encoding_disable_resets_to_zero() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[?1006h"); assert_eq!(screen.grid.modes.mouse_encoding, 1006);
screen.process(b"\x1b[?1006l");
assert_eq!(screen.grid.modes.mouse_encoding, 0);
}
#[test]
fn cursor_visibility_toggle() {
let mut screen = Screen::new(80, 24, 100);
assert!(screen.grid.cursor_visible);
screen.process(b"\x1b[?25l"); assert!(!screen.grid.cursor_visible);
screen.process(b"\x1b[?25h"); assert!(screen.grid.cursor_visible);
}
#[test]
fn dl_il_outside_scroll_region_no_op() {
let mut screen = Screen::new(10, 6, 100);
for row in 0..6 {
screen.process(format!("\x1b[{};1HR{}", row + 1, row).as_bytes());
}
screen.process(b"\x1b[2;4r"); screen.process(b"\x1b[6;1H"); screen.process(b"\x1b[M"); screen.process(b"\x1b[L"); let lines = screen_lines(&screen);
for row in 0..6 {
assert_eq!(lines[row], format!("R{}", row),
"row {} should be unchanged after DL/IL outside region", row);
}
}
#[test]
fn dl_clears_wrap_pending() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"ABCDE"); assert!(screen.grid.wrap_pending);
screen.process(b"\x1b[M"); assert!(!screen.grid.wrap_pending);
}
#[test]
fn il_clears_wrap_pending() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"ABCDE");
assert!(screen.grid.wrap_pending);
screen.process(b"\x1b[L"); assert!(!screen.grid.wrap_pending);
}