use ansi::{CellWidth, Cursor};
use vterm::Term;
fn row_text(t: &Term, row: usize) -> String {
let s = t.screen();
(0..s.cols()).map(|c| s.cell(row, c).ch).collect()
}
fn run(rows: usize, cols: usize, bytes: &[u8]) -> Term {
let mut t = Term::new(rows, cols);
t.feed(bytes);
t
}
#[test]
fn prints_text_and_advances_cursor() {
let t = run(4, 10, b"hi");
assert_eq!(t.screen().cell(0, 0).ch, 'h');
assert_eq!(t.screen().cell(0, 1).ch, 'i');
assert_eq!(t.screen().cursor(), Cursor::new(0, 2));
}
#[test]
fn cup_and_erase_then_print() {
let t = run(24, 80, b"\x1b[2J\x1b[3;5HX");
assert_eq!(t.screen().cell(2, 4).ch, 'X');
assert_eq!(t.screen().cursor(), Cursor::new(2, 5));
}
#[test]
fn hvp_f_is_same_as_cup() {
let t = run(10, 10, b"\x1b[2;3fZ");
assert_eq!(t.screen().cell(1, 2).ch, 'Z');
assert_eq!(t.screen().cursor(), Cursor::new(1, 3));
}
#[test]
fn cup_defaults_and_clamps() {
assert_eq!(run(5, 5, b"\x1b[H").screen().cursor(), Cursor::new(0, 0));
assert_eq!(
run(5, 5, b"\x1b[99;99H").screen().cursor(),
Cursor::new(4, 4)
);
}
#[test]
fn carriage_return_to_col0() {
let t = run(4, 10, b"abc\rX");
assert_eq!(t.screen().cell(0, 0).ch, 'X');
assert_eq!(t.screen().cell(0, 1).ch, 'b');
assert_eq!(t.screen().cursor(), Cursor::new(0, 1));
}
#[test]
fn line_feed_moves_down_same_column() {
let t = run(4, 10, b"ab\nc");
assert_eq!(t.screen().cell(0, 0).ch, 'a');
assert_eq!(t.screen().cell(1, 2).ch, 'c');
assert_eq!(t.screen().cursor(), Cursor::new(1, 3));
}
#[test]
fn vt_and_ff_behave_like_lf() {
assert_eq!(run(4, 5, b"a\x0bb").screen().cursor(), Cursor::new(1, 2));
assert_eq!(run(4, 5, b"a\x0cb").screen().cursor(), Cursor::new(1, 2));
}
#[test]
fn backspace_moves_left_and_clamps() {
let t = run(4, 10, b"abc\x08\x08");
assert_eq!(t.screen().cursor(), Cursor::new(0, 1));
assert_eq!(run(4, 10, b"\x08").screen().cursor(), Cursor::new(0, 0)); }
#[test]
fn tab_advances_to_next_stop() {
let t = run(4, 40, b"\tX");
assert_eq!(t.screen().cell(0, 8).ch, 'X');
assert_eq!(t.screen().cursor(), Cursor::new(0, 9));
let t2 = run(4, 40, b"abc\tY");
assert_eq!(t2.screen().cell(0, 8).ch, 'Y');
}
#[test]
fn cursor_up_down_left_right() {
let t = run(10, 10, b"\x1b[H\x1b[2B\x1b[3C\x1b[A\x1b[D");
assert_eq!(t.screen().cursor(), Cursor::new(1, 2));
}
#[test]
fn cnl_and_cpl_go_to_col0() {
let t = run(10, 10, b"\x1b[5;5H\x1b[2EX");
assert_eq!(t.screen().cursor(), Cursor::new(6, 1)); assert_eq!(t.screen().cell(6, 0).ch, 'X');
let t2 = run(10, 10, b"\x1b[5;5H\x1b[2F");
assert_eq!(t2.screen().cursor(), Cursor::new(2, 0));
}
#[test]
fn cha_and_vpa_absolute() {
let t = run(10, 10, b"\x1b[5;5H\x1b[3G"); assert_eq!(t.screen().cursor(), Cursor::new(4, 2));
let t2 = run(10, 10, b"\x1b[5;5H\x1b[2d"); assert_eq!(t2.screen().cursor(), Cursor::new(1, 4));
}
#[test]
fn autowrap_at_right_edge() {
let t = run(4, 3, b"abcd");
assert_eq!(row_text(&t, 0), "abc");
assert_eq!(t.screen().cell(1, 0).ch, 'd');
assert_eq!(t.screen().cursor(), Cursor::new(1, 1));
}
#[test]
fn pending_wrap_is_deferred() {
let t = run(4, 3, b"abc");
assert_eq!(t.screen().cursor(), Cursor::new(0, 2));
assert_eq!(row_text(&t, 0), "abc");
}
#[test]
fn autowrap_off_overwrites_last_column() {
let t = run(4, 3, b"\x1b[?7labcd");
assert_eq!(row_text(&t, 0), "abd");
assert_eq!(t.screen().cursor(), Cursor::new(0, 2));
}
#[test]
fn scroll_when_lf_at_bottom() {
let t = run(2, 3, b"A\r\nB\r\n");
assert_eq!(t.screen().cell(0, 0).ch, 'B');
assert_eq!(t.screen().cell(1, 0).ch, ' ');
assert_eq!(t.screen().cursor(), Cursor::new(1, 0));
}
#[test]
fn scroll_loses_top_line() {
let t = run(3, 3, b"1\r\n2\r\n3\r\n4");
assert_eq!(t.screen().cell(0, 0).ch, '2');
assert_eq!(t.screen().cell(1, 0).ch, '3');
assert_eq!(t.screen().cell(2, 0).ch, '4');
}
#[test]
fn erase_display_variants() {
let t = run(2, 3, b"XXX\rXXX"); let mut t = t;
t.feed(b"\x1b[1;2H\x1b[0J"); assert_eq!(t.screen().cell(0, 0).ch, 'X');
assert_eq!(t.screen().cell(0, 1).ch, ' ');
assert_eq!(t.screen().cell(0, 2).ch, ' ');
let mut t = run(1, 5, b"ABCDE");
t.feed(b"\x1b[1;3H\x1b[1J"); assert_eq!(row_text(&t, 0), " DE");
let mut t = run(2, 3, b"abcdef");
t.feed(b"\x1b[2J");
assert_eq!(row_text(&t, 0), " ");
assert_eq!(row_text(&t, 1), " ");
}
#[test]
fn erase_line_variants() {
let mut t = run(1, 5, b"ABCDE");
t.feed(b"\x1b[1;3H\x1b[0K"); assert_eq!(row_text(&t, 0), "AB ");
let mut t = run(1, 5, b"ABCDE");
t.feed(b"\x1b[1;3H\x1b[1K"); assert_eq!(row_text(&t, 0), " DE");
let mut t = run(1, 5, b"ABCDE");
t.feed(b"\x1b[1;3H\x1b[2K"); assert_eq!(row_text(&t, 0), " ");
}
#[test]
fn sgr_sets_style_on_written_cells() {
let t = run(2, 10, b"\x1b[1;31mR\x1b[0mN");
let r = t.screen().cell(0, 0);
assert!(r.style.bold);
assert_eq!(r.style.fg, ansi::Color::Indexed(1));
let n = t.screen().cell(0, 1);
assert!(!n.style.bold);
assert_eq!(n.style.fg, ansi::Color::Default);
}
#[test]
fn bare_sgr_m_resets() {
let t = run(2, 10, b"\x1b[1m\x1b[mX"); assert!(!t.screen().cell(0, 0).style.bold);
}
#[test]
fn scroll_region_confines_scrolling() {
let mut t = run(4, 3, b"top");
t.feed(b"\x1b[2;3r"); t.feed(b"\x1b[3;1HA\r\nB\r\nC");
assert_eq!(row_text(&t, 0), "top"); assert_eq!(t.screen().cell(1, 0).ch, 'B');
assert_eq!(t.screen().cell(2, 0).ch, 'C');
}
#[test]
fn scroll_region_resets_cursor_to_origin() {
let mut t = run(6, 6, b"\x1b[3;4H");
t.feed(b"\x1b[2;5r");
assert_eq!(t.screen().cursor(), Cursor::new(0, 0));
}
#[test]
fn insert_lines_shifts_down() {
let mut t = run(3, 3, b"A\r\nB\r\nC");
t.feed(b"\x1b[2;1H\x1b[L");
assert_eq!(t.screen().cell(0, 0).ch, 'A');
assert_eq!(t.screen().cell(1, 0).ch, ' ');
assert_eq!(t.screen().cell(2, 0).ch, 'B');
}
#[test]
fn delete_lines_shifts_up() {
let mut t = run(3, 3, b"A\r\nB\r\nC");
t.feed(b"\x1b[2;1H\x1b[M");
assert_eq!(t.screen().cell(0, 0).ch, 'A');
assert_eq!(t.screen().cell(1, 0).ch, 'C');
assert_eq!(t.screen().cell(2, 0).ch, ' ');
}
#[test]
fn insert_chars_shifts_right() {
let mut t = run(1, 5, b"ABCDE");
t.feed(b"\x1b[1;2H\x1b[2@"); assert_eq!(row_text(&t, 0), "A BC");
}
#[test]
fn delete_chars_shifts_left() {
let mut t = run(1, 5, b"ABCDE");
t.feed(b"\x1b[1;2H\x1b[2P"); assert_eq!(row_text(&t, 0), "ADE ");
}
#[test]
fn csi_save_restore_cursor() {
let t = run(10, 10, b"\x1b[3;4H\x1b[s\x1b[8;8H\x1b[u");
assert_eq!(t.screen().cursor(), Cursor::new(2, 3));
}
#[test]
fn esc_decsc_decrc() {
let t = run(10, 10, b"\x1b[3;4H\x1b7\x1b[8;8H\x1b8");
assert_eq!(t.screen().cursor(), Cursor::new(2, 3));
}
#[test]
fn restore_also_restores_style() {
let t = run(2, 10, b"\x1b[1m\x1b7\x1b[0m\x1b8X");
assert!(t.screen().cell(0, 0).style.bold); }
#[test]
fn alt_screen_hides_and_restores_primary() {
let mut t = run(4, 5, b"PRIME");
assert_eq!(t.screen().cell(0, 0).ch, 'P');
t.feed(b"\x1b[?1049h"); assert_eq!(t.screen().cell(0, 0).ch, ' ');
t.feed(b"\x1b[HALT");
assert_eq!(t.screen().cell(0, 0).ch, 'A');
t.feed(b"\x1b[?1049l"); assert_eq!(t.screen().cell(0, 0).ch, 'P');
}
#[test]
fn the_cursor_moves_with_an_alt_screen_switch_in_both_directions() {
let mut t = run(6, 10, b"\x1b[3;4H");
assert_eq!(t.screen().cursor(), Cursor::new(2, 3));
t.feed(b"\x1b[?1049h");
assert_eq!(t.screen().cursor(), Cursor::new(2, 3), "entering keeps it");
t.feed(b"\x1b[5;7H");
t.feed(b"\x1b[?1049l");
assert_eq!(
t.screen().cursor(),
Cursor::new(4, 6),
"leaving brings the alt position back to the primary"
);
}
#[test]
fn a_saved_cursor_restores_across_buffers_and_a_resize_clamps_it_in_alt() {
let mut t = run(6, 10, b"\x1b[2;2H\x1b7\x1b[?1049h\x1b[5;5H\x1b8");
assert_eq!(t.screen().cursor(), Cursor::new(1, 1));
t.feed(b"\x1b[6;10H");
t.resize(3, 3);
assert_eq!(t.screen().cursor(), Cursor::new(2, 2));
t.feed(b"\x1b[?1049l");
assert_eq!(t.screen().cursor(), Cursor::new(2, 2));
}
#[test]
fn a_synchronized_update_shows_the_cursor_of_the_last_whole_frame() {
let mut t = run(6, 10, b"\x1b[2;3H\x1b[?2026h\x1b[4;4HX");
assert_eq!(t.screen().cursor(), Cursor::new(1, 2));
t.feed(b"\x1b[?2026l");
assert_eq!(t.screen().cursor(), Cursor::new(3, 4));
}
#[test]
fn entering_alt_and_opening_a_sync_update_in_one_sequence_snapshots_the_live_cursor() {
let mut t = run(10, 20, b"\x1b[3;4H\x1b[?1049;2026h");
assert!(t.in_sync());
assert_eq!(t.screen().cursor(), Cursor::new(2, 3));
t.feed(b"\x1b[?2026l");
assert_eq!(t.screen().cursor(), Cursor::new(2, 3));
}
#[test]
fn alt_screen_47_and_1047_also_work() {
let mut t = run(4, 5, b"X");
t.feed(b"\x1b[?47h");
assert_eq!(t.screen().cell(0, 0).ch, ' ');
t.feed(b"\x1b[?47l");
assert_eq!(t.screen().cell(0, 0).ch, 'X');
}
#[test]
fn cursor_hide_show_is_accepted() {
let t = run(4, 5, b"\x1b[?25lAB\x1b[?25h");
assert_eq!(t.screen().cell(0, 0).ch, 'A');
assert_eq!(t.screen().cursor(), Cursor::new(0, 2));
}
#[test]
fn resize_preserves_top_left_and_clamps_cursor() {
let mut t = run(4, 4, b"\x1b[4;4Hxy"); t.feed(b"\x1b[1;1HAB"); t.resize(2, 2);
assert_eq!(t.screen().rows(), 2);
assert_eq!(t.screen().cols(), 2);
assert_eq!(t.screen().cell(0, 0).ch, 'A');
assert_eq!(t.screen().cell(0, 1).ch, 'B');
let Cursor { row, col } = t.screen().cursor();
assert!(row < 2 && col < 2);
}
#[test]
fn chunk_split_yields_identical_screen() {
let script = b"\x1b[2J\x1b[3;5m\x1b[1;1mHello\r\n\x1b[1;31mWorld\x1b[0m\x1b[2;3r\nmore\ttext\x1b[?1049h\x1b[HALT\x1b[?1049l";
let oneshot = run(8, 20, script);
let mut split = Term::new(8, 20);
for &b in script.iter() {
split.feed(&[b]);
}
let (a, b) = (oneshot.screen(), split.screen());
assert_eq!(a.rows(), b.rows());
assert_eq!(a.cols(), b.cols());
assert_eq!(a.cursor(), b.cursor());
for r in 0..a.rows() {
for c in 0..a.cols() {
assert_eq!(a.cell(r, c), b.cell(r, c), "mismatch at ({r},{c})");
}
}
let mut split3 = Term::new(8, 20);
for chunk in script.chunks(3) {
split3.feed(chunk);
}
let c = split3.screen();
assert_eq!(a.cursor(), c.cursor());
for r in 0..a.rows() {
for col in 0..a.cols() {
assert_eq!(a.cell(r, col), c.cell(r, col));
}
}
}
#[test]
fn sync_hides_partial_frame_until_close() {
let mut t = run(2, 5, b"OLD");
assert_eq!(row_text(&t, 0), "OLD ");
assert!(!t.in_sync());
t.feed(b"\x1b[?2026h");
assert!(t.in_sync());
t.feed(b"\x1b[2J\x1b[HNE"); assert_eq!(row_text(&t, 0), "OLD ", "partial frame leaked during sync");
t.feed(b"W"); assert_eq!(row_text(&t, 0), "OLD ", "still mid-update");
t.feed(b"\x1b[?2026l");
assert!(!t.in_sync());
assert_eq!(row_text(&t, 0), "NEW ");
}
#[test]
fn sync_nested_open_keeps_first_snapshot() {
let mut t = run(1, 5, b"AAA");
t.feed(b"\x1b[?2026h");
t.feed(b"\x1b[2J\x1b[HB"); t.feed(b"\x1b[?2026h"); t.feed(b"BB");
assert_eq!(row_text(&t, 0), "AAA ");
t.feed(b"\x1b[?2026l");
assert_eq!(row_text(&t, 0), "BBB ");
}
#[test]
fn resize_during_sync_reveals_live_buffer() {
let mut t = run(2, 5, b"OLD");
t.feed(b"\x1b[?2026h");
t.feed(b"\x1b[2J\x1b[HNEW");
assert!(t.in_sync());
t.resize(3, 8);
assert!(!t.in_sync());
assert_eq!(t.screen().cols(), 8);
assert_eq!(row_text(&t, 0), "NEW ");
}
#[test]
fn ech_blanks_cells_without_shifting() {
let t = run(1, 5, b"ABCDE\x1b[1;2H\x1b[2X");
assert_eq!(row_text(&t, 0), "A DE");
assert_eq!(t.screen().cursor(), Cursor::new(0, 1)); }
#[test]
fn ech_defaults_to_one_and_clamps() {
assert_eq!(row_text(&run(1, 4, b"WXYZ\x1b[1;1H\x1b[X"), 0), " XYZ");
assert_eq!(row_text(&run(1, 4, b"WXYZ\x1b[1;2H\x1b[99X"), 0), "W ");
}
#[test]
fn su_scrolls_the_screen_up() {
let t = run(3, 3, b"AAA\r\nBBB\r\nCCC\x1b[S");
assert_eq!(row_text(&t, 0), "BBB");
assert_eq!(row_text(&t, 1), "CCC");
assert_eq!(row_text(&t, 2), " ");
}
#[test]
fn sd_scrolls_the_screen_down() {
let t = run(3, 3, b"AAA\r\nBBB\r\nCCC\x1b[T");
assert_eq!(row_text(&t, 0), " ");
assert_eq!(row_text(&t, 1), "AAA");
assert_eq!(row_text(&t, 2), "BBB");
}
#[test]
fn take_dirty_is_false_on_a_fresh_term() {
let mut t = Term::new(4, 10);
assert!(!t.take_dirty(), "new term must not be dirty");
}
#[test]
fn take_dirty_is_true_after_feed_and_resets_on_second_call() {
let mut t = Term::new(4, 10);
t.feed(b"hello");
assert!(t.take_dirty(), "dirty after feed");
assert!(!t.take_dirty(), "dirty resets on second take_dirty");
}
#[test]
fn take_dirty_is_false_after_empty_feed() {
let mut t = Term::new(4, 10);
t.feed(b"");
assert!(!t.take_dirty(), "empty feed must not set dirty");
}
#[test]
fn take_dirty_is_true_after_resize() {
let mut t = Term::new(4, 10);
t.take_dirty(); t.resize(6, 20);
assert!(t.take_dirty(), "resize must set dirty");
assert!(!t.take_dirty(), "dirty resets after resize take");
}
#[test]
fn take_dirty_accumulates_across_multiple_feeds_before_reset() {
let mut t = Term::new(4, 10);
t.feed(b"A");
t.feed(b"B");
assert!(t.take_dirty(), "dirty after two feeds");
assert!(!t.take_dirty(), "dirty resets after single take");
}
#[test]
fn wide_glyph_occupies_two_cells_and_advances_by_two() {
let t = run(4, 10, "世".as_bytes());
let s = t.screen();
assert_eq!(s.cell(0, 0).ch, '世');
assert_eq!(s.cell(0, 0).width, CellWidth::Wide, "lead cell is width 2");
assert_eq!(
s.cell(0, 1).width,
CellWidth::Continuation,
"right half is a width-0 continuation"
);
assert_eq!(s.cursor(), Cursor::new(0, 2));
}
#[test]
fn wide_glyphs_pack_without_drift() {
let t = run(4, 10, "世界X".as_bytes());
let s = t.screen();
assert_eq!(s.cell(0, 0).ch, '世');
assert_eq!(s.cell(0, 1).width, CellWidth::Continuation);
assert_eq!(s.cell(0, 2).ch, '界');
assert_eq!(s.cell(0, 3).width, CellWidth::Continuation);
assert_eq!(s.cell(0, 4).ch, 'X');
assert_eq!(s.cursor(), Cursor::new(0, 5));
}
#[test]
fn emoji_is_width_two() {
let t = run(4, 10, "🚀".as_bytes());
let s = t.screen();
assert_eq!(s.cell(0, 0).ch, '🚀');
assert_eq!(s.cell(0, 0).width, CellWidth::Wide);
assert_eq!(s.cell(0, 1).width, CellWidth::Continuation);
assert_eq!(s.cursor(), Cursor::new(0, 2));
}
#[test]
fn wide_glyph_at_right_edge_wraps_whole_not_split() {
let t = run(3, 5, b"ABCD\xe4\xb8\x96"); let s = t.screen();
assert_eq!(
row_text(&t, 0),
"ABCD ",
"row 0 keeps its blank last column"
);
assert_eq!(
s.cell(0, 4).width,
CellWidth::Single,
"last column is a blank, not a half glyph"
);
assert_eq!(
s.cell(1, 0).ch,
'世',
"wide glyph wrapped whole to next line"
);
assert_eq!(s.cell(1, 0).width, CellWidth::Wide);
assert_eq!(s.cell(1, 1).width, CellWidth::Continuation);
assert_eq!(s.cursor(), Cursor::new(1, 2));
}
#[test]
fn cup_then_wide_lands_at_target_and_advances() {
let t = run(6, 12, b"\x1b[3;4H\xe4\xb8\x96"); let s = t.screen();
assert_eq!(s.cell(2, 3).ch, '世');
assert_eq!(s.cell(2, 3).width, CellWidth::Wide);
assert_eq!(s.cell(2, 4).width, CellWidth::Continuation);
assert_eq!(s.cursor(), Cursor::new(2, 5));
}
#[test]
fn zero_width_combining_mark_is_dropped_no_drift() {
let t = run(4, 10, "e\u{0301}x".as_bytes());
let s = t.screen();
assert_eq!(s.cell(0, 0).ch, 'e');
assert_eq!(s.cell(0, 1).ch, 'x', "combining mark consumed no column");
assert_eq!(s.cursor(), Cursor::new(0, 2));
}
#[test]
fn wide_glyph_wrap_is_chunk_split_invariant() {
let mut t = Term::new(3, 5);
for b in b"ABCD\xe4\xb8\x96" {
t.feed(&[*b]);
}
let s = t.screen();
assert_eq!(s.cell(1, 0).ch, '世');
assert_eq!(s.cursor(), Cursor::new(1, 2));
}
#[test]
fn wide_glyph_at_right_edge_with_autowrap_off_is_dropped_not_split() {
let mut t = Term::new(2, 4);
t.feed(b"\x1b[?7l"); t.feed(b"\x1b[1;4H"); t.feed("世".as_bytes()); let s = t.screen();
assert_ne!(
s.cell(0, 3).ch,
'世',
"wide glyph must not be written at the edge"
);
assert_eq!(
s.cell(0, 3).width,
CellWidth::Single,
"no half-glyph lead left behind"
);
assert_eq!(
s.cursor(),
Cursor::new(0, 3),
"cursor stays put when the glyph is dropped"
);
assert_eq!(s.cell(1, 0).ch, ' ', "nothing wrapped to the next line");
}
#[test]
fn a_blitted_ascii_run_lands_exactly_where_one_by_one_printing_would() {
let mut split = Term::new(3, 10);
split.feed(b"abcde");
split.feed(b"fghijklm");
let whole = run(3, 10, b"abcdefghijklm");
assert_eq!(row_text(&whole, 0), "abcdefghij");
assert_eq!(row_text(&whole, 1), "klm ");
assert_eq!(
row_text(&split, 0),
row_text(&whole, 0),
"chunking changes nothing"
);
assert_eq!(row_text(&split, 1), row_text(&whole, 1));
assert_eq!(split.screen().cursor(), whole.screen().cursor());
let t = run(3, 10, b"0123456789");
assert_eq!(row_text(&t, 0), "0123456789");
assert_eq!(
t.screen().cursor(),
Cursor::new(0, 9),
"cursor waits on the last column (deferred wrap)"
);
let t = run(3, 10, "ab世cd".as_bytes());
let s = t.screen();
assert_eq!((s.cell(0, 0).ch, s.cell(0, 1).ch), ('a', 'b'));
assert_eq!(s.cell(0, 2).ch, '世');
assert_eq!(s.cell(0, 3).width, CellWidth::Continuation);
assert_eq!((s.cell(0, 4).ch, s.cell(0, 5).ch), ('c', 'd'));
assert_eq!(s.cursor(), Cursor::new(0, 6));
let t = run(3, 10, "ab世cd\refg".as_bytes());
assert_eq!(row_text(&t, 0), "efg cd ");
}
#[test]
fn new_line_mode_makes_lf_return_to_column_zero() {
let mut t = Term::new(4, 10);
t.feed(b"ab\ncd");
assert_eq!(t.screen().cell(1, 2).ch, 'c', "plain LF keeps the column");
let mut t = Term::new(4, 10);
t.feed(b"\x1b[20hab\ncd\x0bef");
assert_eq!(t.screen().cell(1, 0).ch, 'c', "LNM: LF also returns");
assert_eq!(t.screen().cell(2, 0).ch, 'e', "VT too");
t.feed(b"\x1b[20l\ngh");
assert_eq!(t.screen().cell(3, 2).ch, 'g', "reset: plain LF again");
let mut t = Term::new(4, 10);
t.feed(b"\x1b[20h\x1b[4lx\ny");
assert_eq!(t.screen().cell(1, 0).ch, 'y');
}
#[test]
fn a_character_split_across_chunks_is_not_skipped_by_the_ascii_path() {
let mut t = Term::new(2, 10);
t.feed(b"caf\xC3");
t.feed(b"\xA9 ok");
let row: String = (0..7).map(|c| t.screen().cell(0, c).ch).collect();
assert_eq!(row, "café ok");
let mut t = Term::new(2, 10);
t.feed(b"caf\xC3");
t.feed(b" ok");
let row: String = (0..7).map(|c| t.screen().cell(0, c).ch).collect();
assert_eq!(row, "caf\u{FFFD} ok");
let mut t = Term::new(2, 10);
t.feed(b"a\xE2\x82");
t.feed(b"bc\r");
let row: String = (0..3).map(|c| t.screen().cell(0, c).ch).collect();
assert_eq!(row, "a\u{FFFD}b");
let mut t = Term::new(2, 10);
t.feed(b"a\xE2\x82\rb");
assert_eq!(t.screen().cell(0, 0).ch, 'b');
assert_eq!(t.screen().cell(0, 1).ch, '\u{FFFD}');
}
#[test]
fn history_holds_what_scrolls_off_and_survives_a_resize() {
let mut t = Term::with_history(3, 12, 100);
for k in 0..10 {
t.feed(format!("line {k}\r\n").as_bytes());
}
let sb = t.scrollback();
assert_eq!(sb.history_len(), 8);
let text =
|s: &ansi::Screen, age| -> String { (0..6).map(|c| s.history_cell(age, c).ch).collect() };
assert_eq!(text(sb, 0), "line 7");
assert_eq!(text(sb, 7), "line 0");
t.resize(4, 20);
assert_eq!(
t.scrollback().history_len(),
8,
"a resize keeps the history"
);
assert_eq!(text(t.scrollback(), 0), "line 7");
t.feed(b"\x1b[3J");
assert_eq!(t.scrollback().history_len(), 0);
t.feed(b"\x1b[?1049h");
assert!(t.in_alternate_screen());
for k in 0..10 {
t.feed(format!("alt {k}\r\n").as_bytes());
}
t.feed(b"\x1b[?1049l");
assert_eq!(t.scrollback().history_len(), 0);
let mut plain = Term::new(3, 12);
plain.feed(b"a\r\nb\r\nc\r\nd\r\n");
assert_eq!(plain.scrollback().history_len(), 0);
}
#[test]
fn a_history_byte_cap_bounds_wide_coloured_scrollback_across_a_resize() {
let mut t = Term::with_history(4, 200, 100_000);
t.set_history_bytes(200 * 1024);
let mut line = Vec::new();
for c in 0..200u32 {
line.extend_from_slice(format!("\x1b[38;5;{}m{}", c % 200, c % 10).as_bytes());
}
line.extend_from_slice(b"\x1b[0m\r\n");
for _ in 0..2_000 {
t.feed(&line);
}
let cap = 200 * 1024 + 2 * 96 * 1024;
let h = t.scrollback();
assert!(h.history_bytes() <= cap, "{} bytes", h.history_bytes());
assert!(h.history_len() < 2_000, "the oldest went");
t.resize(6, 150);
for _ in 0..2_000 {
t.feed(&line);
}
let h = t.scrollback();
assert!(
h.history_bytes() <= cap,
"{} bytes after a resize",
h.history_bytes()
);
}
#[test]
fn mouse_tracking_modes_follow_the_program() {
use vterm::MouseTracking;
let mut t = Term::new(24, 80);
assert_eq!(t.mouse_tracking(), MouseTracking::Off);
assert!(!t.mouse_sgr());
t.feed(b"\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h");
assert_eq!(
t.mouse_tracking(),
MouseTracking::Motion,
"the last one set"
);
assert!(t.mouse_sgr());
t.feed(b"\x1b[?1000;1006l");
assert_eq!(
t.mouse_tracking(),
MouseTracking::Off,
"any reset turns it off"
);
assert!(!t.mouse_sgr());
t.feed(b"\x1b[?1002h");
assert_eq!(t.mouse_tracking(), MouseTracking::Drag);
t.feed(b"\x1b[?1000h");
assert_eq!(
t.mouse_tracking(),
MouseTracking::Click,
"one replaces another"
);
}
#[test]
fn a_sync_snapshot_carries_no_scrollback() {
let mut t = Term::with_history(2, 10, 100);
for i in 0..20 {
t.feed(format!("line{i}\r\n").as_bytes());
}
let before = t.screen().history_len();
assert!(before > 0, "the history filled: {before}");
t.feed(b"\x1b[?2026h");
assert!(t.in_sync());
assert_eq!(
t.screen().history_len(),
0,
"the frame is the visible rows only"
);
t.feed(b"\x1b[?2026l");
assert!(!t.in_sync());
assert_eq!(t.screen().history_len(), before, "the history is untouched");
}