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 ");
}