Files
ghostty-rs/crates/ghostty-terminal/src/terminal/tests/p5.rs
T
ddidderr ea6fb5880f feat(terminal): port Terminal, read-only stream handler and StringMap
Ports ZIG/src/terminal/Terminal.zig - printing with grapheme clustering
and Unicode widths, wraparound, insert mode, charsets, cursor movement,
scroll regions and left/right margins, erase/insert/delete, tabs, modes,
alternate screens, save/restore cursor, DECALN, full reset, palette,
mouse/focus/paste flags, kitty keyboard, titles/pwd, semantic prompts,
hyperlinks, protected mode, synchronized output and resize.

stream_readonly.rs implements ghostty_vt::stream::Handler, applying all
state-changing actions to a Terminal and ignoring queries; it is the
base for termio's side-effecting handler. StringMap uses the onig crate
for Oniguruma-compatible regexes (needed by Ghostty's URL regex).

Not yet wired: kitty graphics (hooks and dirty flags are in place where
Zig calls into it, storage is being ported separately) and the tmux
control-mode viewer.

Test Plan:
- cargo test -p ghostty-terminal (849 passed, 5 ignored; all 372
  Terminal and 33 stream_readonly tests ported; ignored ones are blocked
  on the PageList parity pass)
2026-10-11 22:39:29 +02:00

650 lines
17 KiB
Rust

use super::*;
use crate::page::ContentTag;
use ghostty_vt::color::Rgb;
use ghostty_vt::sgr::Attribute;
fn prints(t: &mut Terminal, s: &str) {
for c in s.chars() {
t.print(u32::from(c)).unwrap();
}
}
fn nl(t: &mut Terminal) {
t.carriage_return();
t.linefeed().unwrap();
}
fn dirty(t: &Terminal, x: u16, y: u32) -> bool {
t.is_dirty(pt_active(x, y))
}
fn abc_def_ghi(t: &mut Terminal) {
print_str(t, "ABC");
nl(t);
print_str(t, "DEF");
nl(t);
print_str(t, "GHI");
}
fn abcd_lines(t: &mut Terminal) {
for (i, c) in ['A', 'B', 'C', 'D'].into_iter().enumerate() {
if i > 0 {
nl(t);
}
t.print(u32::from(c)).unwrap();
}
}
const RED: Rgb = Rgb {
r: 0xFF,
g: 0,
b: 0,
};
#[test]
fn terminal_cursor_left_extended_reverse_wrap() {
let mut t = init(5, 5);
t.modes.set(Mode::Wraparound, true);
t.modes.set(Mode::ReverseWrapExtended, true);
prints(&mut t, "ABCDE");
nl(&mut t);
t.print(u32::from('1')).unwrap();
t.cursor_left(2);
t.print(u32::from('X')).unwrap();
assert_eq!("ABCDX\n1", plain(&t));
}
#[test]
fn terminal_cursor_left_extended_reverse_wrap_bottom_wraparound() {
let mut t = init(5, 3);
t.modes.set(Mode::Wraparound, true);
t.modes.set(Mode::ReverseWrapExtended, true);
prints(&mut t, "ABCDE");
nl(&mut t);
t.print(u32::from('1')).unwrap();
t.cursor_left(1 + usize::from(t.cols) + 1);
t.print(u32::from('X')).unwrap();
assert_eq!("ABCDE\n1\n X", plain(&t));
}
#[test]
fn terminal_cursor_left_extended_reverse_wrap_is_priority_if_both_set() {
let mut t = init(5, 3);
t.modes.set(Mode::Wraparound, true);
t.modes.set(Mode::ReverseWrap, true);
t.modes.set(Mode::ReverseWrapExtended, true);
prints(&mut t, "ABCDE");
nl(&mut t);
t.print(u32::from('1')).unwrap();
t.cursor_left(1 + usize::from(t.cols) + 1);
t.print(u32::from('X')).unwrap();
assert_eq!("ABCDE\n1\n X", plain(&t));
}
#[test]
fn terminal_cursor_left_extended_reverse_wrap_above_top_scroll_region() {
let mut t = init(5, 5);
t.modes.set(Mode::Wraparound, true);
t.modes.set(Mode::ReverseWrapExtended, true);
t.set_top_and_bottom_margin(3, 0);
t.set_cursor_pos(2, 1);
t.cursor_left(1000);
assert_eq!(0, t.screen().cursor.x);
assert_eq!(0, t.screen().cursor.y);
}
#[test]
fn terminal_cursor_left_reverse_wrap_on_first_row() {
let mut t = init(5, 5);
t.modes.set(Mode::Wraparound, true);
t.modes.set(Mode::ReverseWrap, true);
t.set_top_and_bottom_margin(3, 0);
t.set_cursor_pos(1, 2);
t.cursor_left(1000);
assert_eq!(0, t.screen().cursor.x);
assert_eq!(0, t.screen().cursor.y);
}
#[test]
fn terminal_cursor_down_basic() {
let mut t = init(5, 5);
t.print(u32::from('A')).unwrap();
t.cursor_down(10);
t.print(u32::from('X')).unwrap();
assert_eq!("A\n\n\n\n X", plain(&t));
}
#[test]
fn terminal_cursor_down_above_bottom_scroll_margin() {
let mut t = init(5, 5);
t.set_top_and_bottom_margin(1, 3);
t.print(u32::from('A')).unwrap();
t.cursor_down(10);
t.print(u32::from('X')).unwrap();
assert_eq!("A\n\n X", plain(&t));
}
#[test]
fn terminal_cursor_down_below_bottom_scroll_margin() {
let mut t = init(5, 5);
t.set_top_and_bottom_margin(1, 3);
t.print(u32::from('A')).unwrap();
t.set_cursor_pos(4, 1);
t.cursor_down(10);
t.print(u32::from('X')).unwrap();
assert_eq!("A\n\n\n\nX", plain(&t));
}
#[test]
fn terminal_cursor_down_resets_wrap() {
let mut t = init(5, 5);
prints(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.cursor_down(1);
assert!(!t.screen().cursor.pending_wrap);
t.print(u32::from('X')).unwrap();
assert_eq!("ABCDE\n X", plain(&t));
}
#[test]
fn terminal_cursor_right_resets_wrap() {
let mut t = init(5, 5);
prints(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.cursor_right(1);
assert!(!t.screen().cursor.pending_wrap);
t.print(u32::from('X')).unwrap();
assert_eq!("ABCDX", plain(&t));
}
#[test]
fn terminal_cursor_right_to_the_edge_of_screen() {
let mut t = init(5, 5);
t.cursor_right(100);
t.print(u32::from('X')).unwrap();
assert_eq!(" X", plain(&t));
}
#[test]
fn terminal_cursor_right_left_of_right_margin() {
let mut t = init(5, 5);
t.scrolling_region.right = 2;
t.cursor_right(100);
t.print(u32::from('X')).unwrap();
assert_eq!(" X", plain(&t));
}
#[test]
fn terminal_cursor_right_right_of_right_margin() {
let mut t = init(5, 5);
t.scrolling_region.right = 2;
t.set_cursor_pos(1, 4);
t.cursor_right(100);
t.print(u32::from('X')).unwrap();
assert_eq!(" X", plain(&t));
}
#[test]
fn terminal_delete_lines_simple() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
t.clear_dirty();
t.delete_lines(1);
assert!(!dirty(&t, 0, 0));
assert!(dirty(&t, 0, 1));
assert!(dirty(&t, 0, 2));
assert!(dirty(&t, 0, 3));
assert_eq!("ABC\nGHI", plain(&t));
}
#[test]
fn terminal_delete_lines_colors_with_bg_color() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
t.set_attribute(Attribute::DirectColorBg(RED)).unwrap();
t.delete_lines(1);
assert_eq!("ABC\nGHI", plain(&t));
for x in 0..t.cols {
let c = cell_at(&t, pt_active(x, 4));
assert_eq!(ContentTag::BgColorRgb, c.content_tag());
assert_eq!(RED, c.color_rgb());
}
}
#[test]
fn terminal_delete_lines_across_page_boundary_marks_all_shifted_rows_dirty() {
let mut t = init_scrollback(10, 5, 1024);
let first = t.screen().pages.first.unwrap();
let first_page_nrows = t.screen().page(first).capacity.rows;
// Fill up the first page minus 3 rows
for _ in 0..first_page_nrows - 3 {
t.linefeed().unwrap();
}
// Add content that will cross a page boundary
print_str(&mut t, "1AAAA");
nl(&mut t);
print_str(&mut t, "2BBBB");
nl(&mut t);
print_str(&mut t, "3CCCC");
nl(&mut t);
print_str(&mut t, "4DDDD");
nl(&mut t);
print_str(&mut t, "5EEEE");
// Verify we now have a second page
assert!(t.screen().pages.node(first).unwrap().next.is_some());
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.delete_lines(1);
for y in 0..5 {
assert!(dirty(&t, 0, y));
}
assert_eq!("2BBBB\n3CCCC\n4DDDD\n5EEEE", plain(&t));
}
#[test]
fn terminal_delete_lines_legacy() {
let mut t = init(80, 80);
abcd_lines(&mut t);
t.cursor_up(2);
t.delete_lines(1);
t.print(u32::from('E')).unwrap();
nl(&mut t);
assert_eq!(0, t.screen().cursor.x);
assert_eq!(2, t.screen().cursor.y);
assert_eq!("A\nE\nD", plain(&t));
}
#[test]
fn terminal_delete_lines_with_scroll_region() {
let mut t = init(80, 80);
abcd_lines(&mut t);
t.set_top_and_bottom_margin(1, 3);
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.delete_lines(1);
assert!(dirty(&t, 0, 0));
assert!(dirty(&t, 0, 1));
assert!(dirty(&t, 0, 2));
assert!(!dirty(&t, 0, 3));
t.print(u32::from('E')).unwrap();
nl(&mut t);
assert_eq!("E\nC\n\nD", plain(&t));
}
#[test]
fn terminal_delete_lines_with_scroll_region_large_count() {
let mut t = init(80, 80);
abcd_lines(&mut t);
t.set_top_and_bottom_margin(1, 3);
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.delete_lines(5);
assert!(dirty(&t, 0, 0));
assert!(dirty(&t, 0, 1));
assert!(dirty(&t, 0, 2));
assert!(!dirty(&t, 0, 3));
t.print(u32::from('E')).unwrap();
nl(&mut t);
assert_eq!("E\n\n\nD", plain(&t));
}
#[test]
fn terminal_delete_lines_with_scroll_region_cursor_outside_of_region() {
let mut t = init(80, 80);
abcd_lines(&mut t);
t.set_top_and_bottom_margin(1, 3);
t.set_cursor_pos(4, 1);
t.clear_dirty();
t.delete_lines(1);
for y in 0..4 {
assert!(!dirty(&t, 0, y));
}
assert_eq!("A\nB\nC\nD", plain(&t));
}
#[test]
fn terminal_delete_lines_resets_pending_wrap() {
let mut t = init(5, 5);
prints(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.delete_lines(1);
assert!(!t.screen().cursor.pending_wrap);
t.print(u32::from('B')).unwrap();
assert_eq!("B", plain(&t));
}
#[test]
fn terminal_delete_lines_resets_wrap() {
let mut t = init(3, 3);
t.print(u32::from('1')).unwrap();
nl(&mut t);
prints(&mut t, "ABCDEF");
t.set_top_and_bottom_margin(1, 2);
t.set_cursor_pos(1, 1);
t.delete_lines(1);
t.print(u32::from('X')).unwrap();
assert_eq!("XBC\n\nDEF", plain(&t));
for y in 0..u32::from(t.rows) {
assert!(!row_at(&t, pt_active(0, y)).wrap());
}
}
fn lr_setup() -> Terminal {
let mut t = init(10, 10);
abc123(&mut t);
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t
}
fn abc123(t: &mut Terminal) {
print_str(t, "ABC123");
nl(t);
print_str(t, "DEF456");
nl(t);
print_str(t, "GHI789");
}
#[test]
fn terminal_delete_lines_left_right_scroll_region() {
let mut t = lr_setup();
t.set_cursor_pos(2, 2);
t.clear_dirty();
t.delete_lines(1);
assert!(!dirty(&t, 0, 0));
for y in 1..3 {
assert!(dirty(&t, 0, y));
}
assert_eq!("ABC123\nDHI756\nG 89", plain(&t));
}
#[test]
fn terminal_delete_lines_left_right_scroll_region_from_top() {
let mut t = lr_setup();
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.delete_lines(1);
for y in 0..3 {
assert!(dirty(&t, 0, y));
}
assert_eq!("AEF423\nDHI756\nG 89", plain(&t));
}
#[test]
fn terminal_delete_lines_left_right_scroll_region_high_count() {
let mut t = lr_setup();
t.set_cursor_pos(2, 2);
t.clear_dirty();
t.delete_lines(100);
assert!(!dirty(&t, 0, 0));
for y in 1..3 {
assert!(dirty(&t, 0, y));
}
assert_eq!("ABC123\nD 56\nG 89", plain(&t));
}
#[test]
fn terminal_delete_lines_wide_character_spacer_head() {
let mut t = init(5, 3);
print_str(&mut t, "AAAAABBBB\u{1F600}CCC");
t.set_cursor_pos(1, 1);
t.delete_lines(1);
assert_eq!("BBBB\n\u{1F600}CCC", plain(&t));
assert_eq!("BBBB\n\u{1F600}CCC", plain_unwrapped(&t));
}
#[test]
fn terminal_delete_lines_wide_character_spacer_head_left_scroll_margin() {
let mut t = init(5, 3);
print_str(&mut t, "AAAAABBBB\u{1F600}CCC");
t.scrolling_region.left = 2;
t.set_cursor_pos(1, 3);
t.delete_lines(1);
assert_eq!("AABB\nBBCCC\n\u{1F600}", plain(&t));
assert_eq!("AABB BBCCC\u{1F600}", plain_unwrapped(&t));
}
#[test]
fn terminal_delete_lines_wide_character_spacer_head_right_scroll_margin() {
let mut t = init(5, 3);
print_str(&mut t, "AAAAABBBB\u{1F600}CCC");
t.scrolling_region.right = 3;
t.set_cursor_pos(1, 1);
t.delete_lines(1);
assert_eq!("BBBBA\n\u{1F600}CC\n C", plain(&t));
assert_eq!("BBBBA\u{1F600}CC C", plain_unwrapped(&t));
}
#[test]
fn terminal_delete_lines_wide_character_spacer_head_left_and_right_scroll_margin() {
let mut t = init(5, 3);
print_str(&mut t, "AAAAABBBB\u{1F600}CCC");
t.scrolling_region.right = 3;
t.scrolling_region.left = 2;
t.set_cursor_pos(1, 3);
t.delete_lines(1);
assert_eq!("AABBA\nBBCC\n\u{1F600} C", plain(&t));
assert_eq!("AABBABBCC\u{1F600} C", plain_unwrapped(&t));
}
#[test]
fn terminal_delete_lines_wide_character_spacer_head_left_lt_2_and_right_scroll_margin() {
let mut t = init(5, 3);
print_str(&mut t, "AAAAABBBB\u{1F600}CCC");
t.scrolling_region.right = 3;
t.scrolling_region.left = 1;
t.set_cursor_pos(1, 2);
t.delete_lines(1);
assert_eq!("ABBBA\nB CC\n C", plain(&t));
assert_eq!("ABBBAB CC C", plain_unwrapped(&t));
}
#[test]
fn terminal_delete_lines_wide_characters_split_by_left_right_scroll_region_boundaries() {
let mut t = init(5, 2);
print_str(&mut t, "AAAAA\n\u{1F600}B\u{1F600}");
t.scrolling_region.right = 3;
t.scrolling_region.left = 1;
t.set_cursor_pos(1, 2);
t.delete_lines(1);
assert_eq!("A B A", plain(&t));
}
#[test]
fn terminal_delete_lines_zero() {
let mut t = init(2, 5);
t.set_cursor_pos(1, 1);
t.delete_lines(0);
}
#[test]
fn terminal_default_style_is_empty() {
let mut t = init(5, 5);
t.print(u32::from('A')).unwrap();
let c = cell_at(&t, pt_screen(0, 0));
assert_eq!(u32::from('A'), c.codepoint());
assert_eq!(0, c.style_id());
}
#[test]
fn terminal_bold_style() {
let mut t = init(5, 5);
t.set_attribute(Attribute::Bold).unwrap();
t.print(u32::from('A')).unwrap();
let c = cell_at(&t, pt_screen(0, 0));
assert_eq!(u32::from('A'), c.codepoint());
assert!(c.style_id() != 0);
let id = t.screen().cursor.style_id;
assert!(t.screen().cursor_page().styles.ref_count(id) > 1);
}
#[test]
fn terminal_garbage_collect_overwritten() {
let mut t = init(5, 5);
t.set_attribute(Attribute::Bold).unwrap();
t.print(u32::from('A')).unwrap();
t.set_cursor_pos(1, 1);
t.set_attribute(Attribute::Unset).unwrap();
t.print(u32::from('B')).unwrap();
let c = cell_at(&t, pt_screen(0, 0));
assert_eq!(u32::from('B'), c.codepoint());
assert_eq!(0, c.style_id());
// verify we have no styles in our style map
assert_eq!(0, t.screen().cursor_page().styles.count());
}
#[test]
fn terminal_do_not_garbage_collect_old_styles_in_use() {
let mut t = init(5, 5);
t.set_attribute(Attribute::Bold).unwrap();
t.print(u32::from('A')).unwrap();
t.set_attribute(Attribute::Unset).unwrap();
t.print(u32::from('B')).unwrap();
let c = cell_at(&t, pt_screen(1, 0));
assert_eq!(u32::from('B'), c.codepoint());
assert_eq!(0, c.style_id());
assert_eq!(1, t.screen().cursor_page().styles.count());
}
#[test]
fn terminal_print_with_style_marks_the_row_as_styled() {
let mut t = init(5, 5);
t.set_attribute(Attribute::Bold).unwrap();
t.print(u32::from('A')).unwrap();
t.set_attribute(Attribute::Unset).unwrap();
t.print(u32::from('B')).unwrap();
assert!(row_at(&t, pt_screen(0, 0)).styled());
}
#[test]
fn terminal_decaln() {
let mut t = init(2, 2);
t.print(u32::from('A')).unwrap();
nl(&mut t);
t.print(u32::from('B')).unwrap();
t.decaln().unwrap();
assert_eq!(0, t.screen().cursor.y);
assert_eq!(0, t.screen().cursor.x);
for y in 0..u32::from(t.rows) {
assert!(dirty(&t, 0, y));
}
assert_eq!("EE\nEE", plain(&t));
}
#[test]
fn terminal_decaln_reset_margins() {
let mut t = init(3, 3);
t.modes.set(Mode::Origin, true);
t.set_top_and_bottom_margin(2, 3);
t.decaln().unwrap();
t.scroll_down(1);
assert_eq!("\nEEE\nEEE", plain(&t));
}
#[test]
fn terminal_decaln_preserves_color() {
let mut t = init(3, 3);
t.set_attribute(Attribute::DirectColorBg(RED)).unwrap();
t.modes.set(Mode::Origin, true);
t.set_top_and_bottom_margin(2, 3);
t.decaln().unwrap();
t.scroll_down(1);
assert_eq!("\nEEE\nEEE", plain(&t));
let c = cell_at(&t, pt_active(0, 0));
assert_eq!(ContentTag::BgColorRgb, c.content_tag());
assert_eq!(RED, c.color_rgb());
}
#[test]
fn terminal_decaln_resets_graphemes_with_protected_mode() {
let mut t = init(3, 3);
t.set_protected_mode(ProtectedMode::Iso);
t.modes.set(Mode::GraphemeCluster, true);
for cp in [0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F467] {
t.print(cp).unwrap();
}
t.decaln().unwrap();
assert_eq!(0, t.screen().cursor.y);
assert_eq!(0, t.screen().cursor.x);
assert!(t.screen().cursor.protected);
assert_eq!(ProtectedMode::Iso, t.screen().protected_mode);
for y in 0..u32::from(t.rows) {
assert!(dirty(&t, 0, y));
}
assert_eq!("EEE\nEEE\nEEE", plain(&t));
}
#[test]
fn terminal_insert_blanks_zero() {
let mut t = init(5, 2);
prints(&mut t, "ABC");
t.set_cursor_pos(1, 1);
t.insert_blanks(0);
assert_eq!("ABC", plain(&t));
}
#[test]
fn terminal_insert_blanks() {
let mut t = init(5, 2);
prints(&mut t, "ABC");
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.insert_blanks(2);
assert!(dirty(&t, 0, 0));
assert!(!dirty(&t, 0, 1));
assert_eq!(" ABC", plain(&t));
}
#[test]
fn terminal_insert_blanks_pushes_off_end() {
let mut t = init(3, 2);
prints(&mut t, "ABC");
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.insert_blanks(2);
assert!(dirty(&t, 0, 0));
assert_eq!(" A", plain(&t));
}
#[test]
fn terminal_insert_blanks_more_than_size() {
let mut t = init(3, 2);
prints(&mut t, "ABC");
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.insert_blanks(5);
assert!(dirty(&t, 0, 0));
assert_eq!("", plain(&t));
}
#[test]
fn terminal_insert_blanks_no_scroll_region_fits() {
let mut t = init(10, 10);
prints(&mut t, "ABC");
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.insert_blanks(2);
assert!(dirty(&t, 0, 0));
assert_eq!(" ABC", plain(&t));
}
#[test]
fn terminal_insert_blanks_preserves_background_sgr() {
let mut t = init(10, 10);
prints(&mut t, "ABC");
t.set_cursor_pos(1, 1);
t.set_attribute(Attribute::DirectColorBg(RED)).unwrap();
t.insert_blanks(2);
assert_eq!(" ABC", plain(&t));
let c = cell_at(&t, pt_active(0, 0));
assert_eq!(ContentTag::BgColorRgb, c.content_tag());
assert_eq!(RED, c.color_rgb());
}