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)
This commit is contained in:
ddidderr committed 2026-10-11 22:39:29 +02:00
1 parent ecf16f4a97
commit ea6fb5880f
16 files changed
+11178 -8

No files matched your search

@@ -0,0 +1 @@
Ḡ̴͎͍̜͎͔͕̩̗͕͖̟̜͑̊̌̇̑͒͋͑̄̈́͐̈́́̽͌͂̎̀̔͋̓́̅̌̇͘̕͜͝͝h̶̡̞̫͉̳̬̜̱̥͕͑̾͛̒̆̒̉̒̑͂̄͘ͅǫ̷̨̥͔͔͖̭͚͙̯̟̭̘͇̫̰͚̺̳̙̳̟͚̫̱̹̱͒̂̑͒͜͠ͅş̴̖̰̜̱̹͙̅͒̀̏͆̐̋͂̓͋̃̈̔̂̈͛̐̿̔́̔̄͑̇͑̋̈́͌͋̾̃̽̈́̕͘̚͘͘͘͠͠t̵̢̜̱̦͇͉̬̮̼͖̳̗̥̝̬͇͕̥̜͕̳̱̥̮͉̮̩̘̰̪̤͉͎̲͈͍̳̟̠͈̝̫͋̊̀͐̍̅̀̄̃̈́̔̇̈́̄̃̽̂̌̅̄̋͒̃̈́̍̀̍̇̽̐͊̾̆̅̈̿̓͒̄̾͌̚͝͝͝͝͝t̴̥̼̳̗̬̬͔͎̯͉͇̮̰͖͇̝͔̳̳̗̰͇͎͉̬͇̝̺̯͎͖͔̍͆͒̊̒̔̊̈́̿̊̅͂̐͋̿͂̈̒̄͜͠͠ÿ̴̢̗̜̥͇͖̰͎̝̹̗̪̙̞̣̳͎̯̹͚̲̝̗̳̳̗̖͎̗̬͈͙̝̟͍̥̤͖͇̰͈̺͛̒̂͌̌̏̈̾̓̈́̿͐̂̓̔̓̂̈́͑͛͊͋̔̿̊͑͌̊̏͘͘̕͘͠͝
@@ -0,0 +1,90 @@
//! Tests ported from `Terminal.zig`, split into chunks (`p1`..`p8`) in source
//! order. Shared helpers live here.
#![allow(dead_code, unused_imports)]
pub(super) use super::*;
pub(super) use crate::point::{Coordinate, Point};
mod p1;
mod p2;
mod p3;
mod p4;
mod p5;
mod p6;
mod p7;
mod p8;
/// `Terminal.init(alloc, .{ .cols, .rows })` (max_scrollback keeps the default).
pub(super) fn init(cols: u16, rows: u16) -> Terminal {
Terminal::new(Options::new(cols, rows)).unwrap()
}
/// Like `init` but with an explicit max_scrollback (bytes).
pub(super) fn init_scrollback(cols: u16, rows: u16, max_scrollback: usize) -> Terminal {
let mut o = Options::new(cols, rows);
o.max_scrollback = max_scrollback;
Terminal::new(o).unwrap()
}
pub(super) fn pt_active(x: u16, y: u32) -> Point {
Point::Active(Coordinate { x, y })
}
pub(super) fn pt_screen(x: u16, y: u32) -> Point {
Point::Screen(Coordinate { x, y })
}
pub(super) fn pt_viewport(x: u16, y: u32) -> Point {
Point::Viewport(Coordinate { x, y })
}
pub(super) fn pt_history(x: u16, y: u32) -> Point {
Point::History(Coordinate { x, y })
}
/// Zig `try t.printString(str)`.
pub(super) fn print_str(t: &mut Terminal, s: &str) {
t.print_string(s).unwrap();
}
/// Feed raw bytes through a fresh read-only VT stream (Zig
/// `var s = t.vtStream(); try s.nextSlice(...)`). Parser state does not
/// persist across calls; create a stream with `t.vt_stream()` if it must.
pub(super) fn feed(t: &mut Terminal, bytes: &[u8]) {
let mut s = t.vt_stream();
s.next_slice(bytes).unwrap();
}
/// Zig `t.plainString(alloc)`.
pub(super) fn plain(t: &Terminal) -> String {
t.plain_string().unwrap()
}
/// Zig `t.plainStringUnwrapped(alloc)`.
pub(super) fn plain_unwrapped(t: &Terminal) -> String {
t.plain_string_unwrapped().unwrap()
}
/// Zig `t.screens.active.pages.getCell(pt).?` (a copy of the cell).
pub(super) fn cell_at(t: &Terminal, pt: Point) -> crate::page::Cell {
t.screen().pages.get_cell(pt).unwrap()
}
/// Zig `t.screens.active.dumpStringAlloc(alloc, pt)`.
pub(super) fn dump(t: &Terminal, pt: Point) -> String {
t.screen().dump_string_alloc(pt).unwrap()
}
/// Zig `t.screens.active.pages.pin(pt).?`.
pub(super) fn pin_at(t: &Terminal, pt: Point) -> crate::page_list::Pin {
t.screen().pages.pin(pt).unwrap()
}
/// Zig `pin.rowAndCell().row` for a point (copy of the row).
pub(super) fn row_at(t: &Terminal, pt: Point) -> crate::page::Row {
*pin_at(t, pt).row_and_cell(&t.screen().pages).unwrap().0
}
/// Zig `pin.node.data.lookupGrapheme(cell)` for a point: extra codepoints.
pub(super) fn graphemes_at(t: &Terminal, pt: Point) -> Option<Vec<u32>> {
let p = pin_at(t, pt);
let page = t.screen().page(p.node);
let i = page.cell_index(usize::from(p.x), usize::from(p.y)).unwrap();
page.lookup_grapheme(i).map(<[u32]>::to_vec)
}
@@ -0,0 +1,1211 @@
use super::*;
use crate::page::{Cell, Wide};
use ghostty_vt::color::Rgb;
use ghostty_vt::sgr::Attribute;
fn c(t: &Terminal, x: u16, y: u32) -> Cell {
cell_at(t, pt_screen(x, y))
}
fn dirty(t: &Terminal, x: u16, y: u32) -> bool {
t.is_dirty(pt_screen(x, y))
}
fn print_all(t: &mut Terminal, cps: &[u32]) {
for &cp in cps {
t.print(cp).unwrap();
}
}
fn cur(t: &Terminal) -> (u16, u16) {
(t.screen().cursor.x, t.screen().cursor.y)
}
#[test]
fn terminal_input_with_no_control_characters() {
let mut t = init(40, 40);
for ch in "hello".chars() {
t.print(ch as u32).unwrap();
}
assert_eq!(0, t.screen().cursor.y);
assert_eq!(5, t.screen().cursor.x);
assert_eq!("hello", plain(&t));
// The first row should be dirty
assert!(dirty(&t, 5, 0));
assert!(!dirty(&t, 5, 1));
}
#[test]
fn terminal_input_with_basic_wraparound() {
let mut t = init(5, 40);
for ch in "helloworldabc12".chars() {
t.print(ch as u32).unwrap();
}
assert_eq!(2, t.screen().cursor.y);
assert_eq!(4, t.screen().cursor.x);
assert!(t.screen().cursor.pending_wrap);
assert_eq!("hello\nworld\nabc12", plain(&t));
}
#[test]
fn terminal_input_with_basic_wraparound_dirty() {
let mut t = init(5, 40);
for ch in "hello".chars() {
t.print(ch as u32).unwrap();
}
assert!(dirty(&t, 4, 0));
t.clear_dirty();
t.print('w' as u32).unwrap();
// Old row is dirty because cursor moved from there
assert!(dirty(&t, 4, 0));
assert!(dirty(&t, 0, 1));
}
#[test]
fn terminal_input_that_forces_scroll() {
let mut t = init(1, 5);
for ch in "abcdef".chars() {
t.print(ch as u32).unwrap();
}
assert_eq!(4, t.screen().cursor.y);
assert_eq!(0, t.screen().cursor.x);
assert_eq!("b\nc\nd\ne\nf", plain(&t));
}
#[test]
fn terminal_input_unique_style_per_cell() {
let mut t = init(30, 30);
for y in 0..t.rows {
for x in 0..t.cols {
t.set_cursor_pos(usize::from(y), usize::from(x));
t.set_attribute(Attribute::DirectColorBg(Rgb {
r: x as u8,
g: y as u8,
b: 0,
}))
.unwrap();
t.print('x' as u32).unwrap();
}
}
}
#[test]
fn terminal_input_glitch_text() {
let glitch = include_str!("glitch.txt");
let mut t = init(30, 30);
// Get our initial grapheme capacity.
let first = |t: &Terminal| t.screen().pages.first.unwrap();
let grapheme_cap = t.screen().page(first(&t)).capacity.grapheme_bytes;
// Print glitch text until our capacity changes
loop {
let node = first(&t);
if t.screen().page(node).capacity.grapheme_bytes != grapheme_cap {
break;
}
t.print_string(glitch).unwrap();
}
// We're testing to make sure that grapheme capacity gets increased.
let node = first(&t);
assert!(t.screen().page(node).capacity.grapheme_bytes > grapheme_cap);
}
#[test]
fn terminal_zero_width_character_at_start() {
let mut t = init(80, 80);
// This used to crash the terminal. This is not allowed so we should
// just ignore it.
t.print(0x200D).unwrap();
assert_eq!((0, 0), cur(&t));
// Should not be dirty since we changed nothing.
assert!(!dirty(&t, 0, 0));
}
// https://github.com/mitchellh/ghostty/issues/1400
#[test]
fn terminal_print_single_very_long_line() {
let mut t = init(5, 5);
// This would crash for issue 1400. So the assertion here is
// that we simply do not crash.
for _ in 0..1000 {
t.print('x' as u32).unwrap();
}
}
#[test]
fn terminal_print_wide_char() {
let mut t = init(80, 80);
t.print(0x1F600).unwrap(); // Smiley face
assert_eq!(0, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
let cell = c(&t, 0, 0);
assert_eq!(0x1F600, cell.codepoint());
assert_eq!(Wide::Wide, cell.wide());
assert_eq!(Wide::SpacerTail, c(&t, 1, 0).wide());
assert!(dirty(&t, 0, 0));
}
#[test]
fn terminal_print_wide_char_at_edge_creates_spacer_head() {
let mut t = init(10, 10);
t.set_cursor_pos(1, 10);
t.print(0x1F600).unwrap(); // Smiley face
assert_eq!(1, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
assert_eq!(Wide::SpacerHead, c(&t, 9, 0).wide());
let cell = c(&t, 0, 1);
assert_eq!(0x1F600, cell.codepoint());
assert_eq!(Wide::Wide, cell.wide());
assert_eq!(Wide::SpacerTail, c(&t, 1, 1).wide());
// Old row is dirty because cursor moved from there
assert!(dirty(&t, 0, 0));
assert!(dirty(&t, 0, 1));
}
#[test]
fn terminal_print_wide_char_with_1_column_width() {
let mut t = init(1, 2);
t.print(0x1F600).unwrap();
// This prints a space so we should be dirty.
assert!(dirty(&t, 0, 0));
}
#[test]
fn terminal_print_wide_char_in_single_width_terminal() {
let mut t = init(1, 80);
t.print(0x1F600).unwrap(); // Smiley face
assert_eq!((0, 0), cur(&t));
assert!(t.screen().cursor.pending_wrap);
let cell = c(&t, 0, 0);
assert_eq!(0, cell.codepoint());
assert_eq!(Wide::Narrow, cell.wide());
assert!(dirty(&t, 0, 0));
}
#[test]
fn terminal_print_over_wide_char_at_0_0() {
let mut t = init(80, 80);
t.print(0x1F600).unwrap(); // Smiley face
t.set_cursor_pos(0, 0);
t.print('A' as u32).unwrap();
assert_eq!((1, 0), cur(&t));
let cell = c(&t, 0, 0);
assert_eq!('A' as u32, cell.codepoint());
assert_eq!(Wide::Narrow, cell.wide());
let cell = c(&t, 1, 0);
assert_eq!(0, cell.codepoint());
assert_eq!(Wide::Narrow, cell.wide());
assert!(dirty(&t, 0, 0));
assert!(!dirty(&t, 0, 1));
}
#[test]
fn terminal_print_over_wide_char_at_col_0_corrupts_previous_row() {
// Crash found by AFL++ fuzzer (afl-out/stream/default/crashes/id:000002).
//
// printCell, when overwriting a wide cell with a narrow cell at x<=1
// and y>0, sets the last cell of the previous row to .narrow — even
// when that cell is a .spacer_tail rather than a .spacer_head. This
// orphans the .wide cell at cols-2.
let mut t = init(10, 3);
// Fill rows 0 and 1 with wide chars (5 per row on a 10-col terminal).
for _ in 0..10 {
t.print(0x4E2D).unwrap();
}
// Move cursor to row 1, col 0 (on top of a wide char) and print a
// narrow character.
t.set_cursor_pos(2, 1);
t.print('A' as u32).unwrap();
// Row 1, col 0 should be narrow (we just overwrote the wide char).
assert_eq!(Wide::Narrow, c(&t, 0, 1).wide());
// Row 0, col 8 should still be .wide (the last wide char on the row).
assert_eq!(Wide::Wide, c(&t, 8, 0).wide());
// Row 0, col 9 must remain .spacer_tail to pair with the .wide at col 8.
assert_eq!(Wide::SpacerTail, c(&t, 9, 0).wide());
}
#[test]
fn terminal_print_over_wide_spacer_tail() {
let mut t = init(5, 5);
t.print('橋' as u32).unwrap();
t.set_cursor_pos(1, 2);
t.print('X' as u32).unwrap();
let cell = c(&t, 0, 0);
assert_eq!(0, cell.codepoint());
assert_eq!(Wide::Narrow, cell.wide());
let cell = c(&t, 1, 0);
assert_eq!('X' as u32, cell.codepoint());
assert_eq!(Wide::Narrow, cell.wide());
assert_eq!(" X", plain(&t));
assert!(dirty(&t, 0, 0));
}
#[test]
fn terminal_print_over_wide_char_with_bold() {
let mut t = init(80, 80);
t.set_attribute(Attribute::Bold).unwrap();
t.print(0x1F600).unwrap(); // Smiley face
// verify we have styles in our style map
assert_eq!(1, t.screen().cursor_page().styles.count());
// Go back and overwrite with no style
t.set_cursor_pos(0, 0);
t.set_attribute(Attribute::Unset).unwrap();
t.print('A' as u32).unwrap(); // Smiley face
// verify our style is gone
assert_eq!(0, t.screen().cursor_page().styles.count());
assert!(dirty(&t, 0, 0));
}
#[test]
fn terminal_print_over_wide_char_with_bg_color() {
let mut t = init(80, 80);
t.set_attribute(Attribute::DirectColorBg(Rgb {
r: 0xFF,
g: 0,
b: 0,
}))
.unwrap();
t.print(0x1F600).unwrap(); // Smiley face
// verify we have styles in our style map
assert_eq!(1, t.screen().cursor_page().styles.count());
// Go back and overwrite with no style
t.set_cursor_pos(0, 0);
t.set_attribute(Attribute::Unset).unwrap();
t.print('A' as u32).unwrap(); // Smiley face
// verify our style is gone
assert_eq!(0, t.screen().cursor_page().styles.count());
assert!(dirty(&t, 0, 0));
}
#[test]
fn terminal_print_multicodepoint_grapheme_disabled_mode_2027() {
let mut t = init(80, 80);
// https://github.com/mitchellh/ghostty/issues/289
// This is: 👨‍👩‍👧 (which may or may not render correctly)
print_all(&mut t, &[0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F467]);
// We should have 6 cells taken up
assert_eq!(0, t.screen().cursor.y);
assert_eq!(6, t.screen().cursor.x);
let cell = c(&t, 0, 0);
assert_eq!(0x1F468, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(Wide::Wide, cell.wide());
assert_eq!(1, graphemes_at(&t, pt_screen(0, 0)).unwrap().len());
let cell = c(&t, 1, 0);
assert_eq!(0, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::SpacerTail, cell.wide());
assert!(graphemes_at(&t, pt_screen(1, 0)).is_none());
let cell = c(&t, 2, 0);
assert_eq!(0x1F469, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(Wide::Wide, cell.wide());
assert_eq!(1, graphemes_at(&t, pt_screen(2, 0)).unwrap().len());
let cell = c(&t, 3, 0);
assert_eq!(0, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::SpacerTail, cell.wide());
assert!(graphemes_at(&t, pt_screen(3, 0)).is_none());
let cell = c(&t, 4, 0);
assert_eq!(0x1F467, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::Wide, cell.wide());
assert!(graphemes_at(&t, pt_screen(4, 0)).is_none());
let cell = c(&t, 5, 0);
assert_eq!(0, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::SpacerTail, cell.wide());
assert!(graphemes_at(&t, pt_screen(5, 0)).is_none());
assert!(dirty(&t, 0, 0));
}
#[test]
fn terminal_vs16_doesnt_make_character_with_2027_disabled() {
let mut t = init(5, 5);
// Disable grapheme clustering
t.modes.set(Mode::GraphemeCluster, false);
t.print(0x2764).unwrap(); // Heart
t.print(0xFE0F).unwrap(); // VS16 to make wide
assert_eq!("❤️", plain(&t));
let cell = c(&t, 0, 0);
assert_eq!(0x2764, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
assert_eq!(1, graphemes_at(&t, pt_screen(0, 0)).unwrap().len());
}
#[test]
fn terminal_ignored_vs16_doesnt_mark_dirty() {
let mut t = init(5, 5);
// Disable grapheme clustering
t.modes.set(Mode::GraphemeCluster, false);
t.print(0x2764).unwrap(); // Heart
assert!(dirty(&t, 0, 0));
t.clear_dirty();
t.print(0xFE0F).unwrap(); // VS16 to make wide
assert!(!dirty(&t, 0, 0));
}
#[test]
fn terminal_print_invalid_vs16_non_grapheme() {
let mut t = init(80, 80);
// https://github.com/mitchellh/ghostty/issues/1482
t.print('x' as u32).unwrap();
t.print(0xFE0F).unwrap();
// We should have 1 narrow cell.
assert_eq!(0, t.screen().cursor.y);
assert_eq!(1, t.screen().cursor.x);
let cell = c(&t, 0, 0);
assert_eq!('x' as u32, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
assert_eq!(0, c(&t, 1, 0).codepoint());
}
#[test]
fn terminal_invalid_vs16_doesnt_mark_dirty() {
let mut t = init(5, 5);
// Disable grapheme clustering
t.modes.set(Mode::GraphemeCluster, false);
t.print('x' as u32).unwrap();
assert!(dirty(&t, 0, 0));
t.clear_dirty();
t.print(0xFE0F).unwrap(); // VS16 to make wide
assert!(!dirty(&t, 0, 0));
}
#[test]
fn terminal_print_multicodepoint_grapheme_mode_2027() {
let mut t = init(80, 80);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
// https://github.com/mitchellh/ghostty/issues/289
// This is: 👨‍👩‍👧 (which may or may not render correctly)
print_all(&mut t, &[0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F467]);
// We should have 2 cells taken up. It is one character but "wide".
assert_eq!(0, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
// Row should be dirty
assert!(dirty(&t, 0, 0));
let cell = c(&t, 0, 0);
assert_eq!(0x1F468, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(Wide::Wide, cell.wide());
assert_eq!(4, graphemes_at(&t, pt_screen(0, 0)).unwrap().len());
let cell = c(&t, 1, 0);
assert_eq!(0, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::SpacerTail, cell.wide());
}
#[test]
fn terminal_keypad_sequence_vs15() {
let mut t = init(80, 80);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
// This is: "#︎" (number sign with text presentation selector)
t.print(0x23).unwrap(); // # Number sign (valid base)
t.print(0xFE0E).unwrap(); // VS15 (text presentation selector)
// VS15 should combine with the base character into a single grapheme cluster,
// taking 1 cell (narrow character).
assert_eq!(0, t.screen().cursor.y);
assert_eq!(1, t.screen().cursor.x);
// Row should be dirty
assert!(dirty(&t, 0, 0));
let cell = c(&t, 0, 0);
assert_eq!(0x23, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
}
#[test]
fn terminal_keypad_sequence_vs16() {
let mut t = init(80, 80);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
// This is: "#️" (number sign with emoji presentation selector)
t.print(0x23).unwrap(); // # Number sign (valid base)
t.print(0xFE0F).unwrap(); // VS16 (emoji presentation selector)
// VS16 should combine with the base character into a single grapheme cluster,
// taking 2 cells (wide character).
assert_eq!(0, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
// Row should be dirty
assert!(dirty(&t, 0, 0));
let cell = c(&t, 0, 0);
assert_eq!(0x23, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(Wide::Wide, cell.wide());
}
#[test]
fn terminal_fitzpatrick_skin_tone_next_valid_base() {
let mut t = init(80, 80);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
// This is: "👋🏿" (waving hand with dark skin tone)
t.print(0x1F44B).unwrap(); // 👋 Waving hand (valid base)
t.print(0x1F3FF).unwrap(); // 🏿 Dark skin tone modifier
// The skin tone should combine with the base emoji into a single grapheme cluster,
// taking 2 cells (wide character).
assert_eq!(0, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
// Row should be dirty
assert!(dirty(&t, 0, 0));
let cell = c(&t, 0, 0);
assert_eq!(0x1F44B, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(Wide::Wide, cell.wide());
}
#[test]
fn terminal_fitzpatrick_skin_tone_next_to_non_base() {
let mut t = init(80, 80);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
t.print(0x22).unwrap(); // "
t.print(0x1F3FF).unwrap(); // Dark skin tone
t.print(0x22).unwrap(); // "
// We should have 4 cells taken up. Importantly, the skin tone
// should not join with the quotes.
assert_eq!(0, t.screen().cursor.y);
assert_eq!(4, t.screen().cursor.x);
// Row should be dirty
assert!(dirty(&t, 0, 0));
let cell = c(&t, 0, 0);
assert_eq!(0x22, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
let cell = c(&t, 1, 0);
assert_eq!(0x1F3FF, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::Wide, cell.wide());
let cell = c(&t, 3, 0);
assert_eq!(0x22, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
}
#[test]
fn terminal_multicodepoint_grapheme_marks_dirty_on_every_codepoint() {
let mut t = init(80, 80);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
// https://github.com/mitchellh/ghostty/issues/289
// This is: 👨‍👩‍👧 (which may or may not render correctly)
t.print(0x1F468).unwrap();
assert!(dirty(&t, 0, 0));
t.clear_dirty();
t.print(0x200D).unwrap();
assert!(dirty(&t, 0, 0));
t.clear_dirty();
t.print(0x1F469).unwrap();
assert!(dirty(&t, 0, 0));
t.clear_dirty();
t.print(0x200D).unwrap();
assert!(dirty(&t, 0, 0));
t.clear_dirty();
t.print(0x1F467).unwrap();
assert!(dirty(&t, 0, 0));
// We should have 2 cells taken up. It is one character but "wide".
assert_eq!(0, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
}
#[test]
fn terminal_vs15_to_make_narrow_character() {
let mut t = init(5, 5);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
t.print(0x2614).unwrap(); // Umbrella with rain drops, width=2
assert!(dirty(&t, 0, 0));
t.clear_dirty();
// We should have 2 cells taken up. It is one character but "wide".
assert_eq!(0, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
t.print(0xFE0E).unwrap(); // VS15 to make narrow
assert!(dirty(&t, 0, 0));
t.clear_dirty();
// VS15 should send us back a cell since our char is no longer wide.
assert_eq!(0, t.screen().cursor.y);
assert_eq!(1, t.screen().cursor.x);
assert_eq!("☔︎", plain(&t));
let cell = c(&t, 0, 0);
assert_eq!(0x2614, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
assert_eq!(1, graphemes_at(&t, pt_screen(0, 0)).unwrap().len());
}
#[test]
fn terminal_vs15_on_already_narrow_emoji() {
let mut t = init(5, 5);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
t.print(0x26C8).unwrap(); // Thunder cloud and rain, width=1
assert!(dirty(&t, 0, 0));
t.clear_dirty();
t.print(0xFE0E).unwrap(); // VS15 to make narrow
assert!(dirty(&t, 0, 0));
t.clear_dirty();
// Character takes up one cell
assert_eq!(0, t.screen().cursor.y);
assert_eq!(1, t.screen().cursor.x);
assert_eq!("⛈︎", plain(&t));
let cell = c(&t, 0, 0);
assert_eq!(0x26C8, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
assert_eq!(1, graphemes_at(&t, pt_screen(0, 0)).unwrap().len());
}
#[test]
fn terminal_print_invalid_vs15_following_emoji_is_wide() {
let mut t = init(80, 80);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
t.print(0x1F9E0).unwrap(); // 🧠
t.print(0xFE0E).unwrap(); // not valid with U+1F9E0 as base
// We should have 2 cells taken up. It is one character but "wide".
assert_eq!(0, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
let cell = c(&t, 0, 0);
assert_eq!(0x1F9E0, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::Wide, cell.wide());
let cell = c(&t, 1, 0);
assert_eq!(0, cell.codepoint());
assert_eq!(Wide::SpacerTail, cell.wide());
}
#[test]
fn terminal_print_invalid_vs15_in_emoji_zwj_sequence() {
let mut t = init(80, 80);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
t.print(0x1F469).unwrap(); // 👩
t.print(0xFE0E).unwrap(); // not valid with U+1F469 as base
t.print(0x200D).unwrap(); // ZWJ
t.print(0x1F466).unwrap(); // 👦
// We should have 2 cells taken up. It is one character but "wide".
assert_eq!(0, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
let cell = c(&t, 0, 0);
assert_eq!(0x1F469, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(
vec![0x200D, 0x1F466],
graphemes_at(&t, pt_screen(0, 0)).unwrap()
);
assert_eq!(Wide::Wide, cell.wide());
let cell = c(&t, 1, 0);
assert_eq!(0, cell.codepoint());
assert_eq!(Wide::SpacerTail, cell.wide());
}
#[test]
fn terminal_vs15_to_make_narrow_character_with_pending_wrap() {
let mut t = init(4, 5);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
assert!(t.modes.get(Mode::Wraparound));
t.print(0x1F34B).unwrap(); // Lemon, width=2
t.print(0x2614).unwrap(); // Umbrella with rain drops, width=2
assert!(dirty(&t, 0, 0));
t.clear_dirty();
// We only move to the end of the line because we're in a pending wrap
// state.
assert_eq!(0, t.screen().cursor.y);
assert_eq!(3, t.screen().cursor.x);
assert!(t.screen().cursor.pending_wrap);
t.print(0xFE0E).unwrap(); // VS15 to make narrow
assert!(dirty(&t, 0, 0));
t.clear_dirty();
// VS15 should clear the pending wrap state
assert_eq!(0, t.screen().cursor.y);
assert_eq!(3, t.screen().cursor.x);
assert!(!t.screen().cursor.pending_wrap);
assert_eq!("🍋☔︎", plain(&t));
let cell = c(&t, 2, 0);
assert_eq!(0x2614, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
assert_eq!(1, graphemes_at(&t, pt_screen(2, 0)).unwrap().len());
// VS15 should not affect the previous grapheme
let lemon = c(&t, 0, 0);
assert_eq!(0x1F34B, lemon.codepoint());
assert_eq!(Wide::Wide, lemon.wide());
let spacer = c(&t, 1, 0);
assert_eq!(0, spacer.codepoint());
assert_eq!(Wide::SpacerTail, spacer.wide());
}
#[test]
fn terminal_vs16_to_make_wide_character_on_next_line() {
let mut t = init(3, 5);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
t.cursor_right(2);
t.print('#' as u32).unwrap();
assert_eq!(2, t.screen().cursor.x);
assert!(t.screen().cursor.pending_wrap);
assert!(dirty(&t, 2, 0));
t.clear_dirty();
t.print(0xFE0F).unwrap(); // VS16 to make wide
assert!(dirty(&t, 2, 0));
t.clear_dirty();
assert_eq!(1, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
assert!(!t.screen().cursor.pending_wrap);
// Previous cell turns into spacer_head
let cell = c(&t, 2, 0);
assert_eq!(0, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::SpacerHead, cell.wide());
// '#' cell is wide
let cell = c(&t, 0, 1);
assert_eq!('#' as u32, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(vec![0xFE0F], graphemes_at(&t, pt_screen(0, 1)).unwrap());
assert_eq!(Wide::Wide, cell.wide());
// spacer_tail
let cell = c(&t, 1, 1);
assert_eq!(0, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::SpacerTail, cell.wide());
}
#[test]
fn terminal_vs16_to_make_wide_character_on_next_line_with_hyperlink() {
// Regression test for the crash fixed in print's grapheme `.wide` path:
// writing a spacer_head at the screen edge before row.wrap was set.
let mut t = init(3, 5);
// Enable grapheme clustering and activate a hyperlink so printCell
// calls cursorSetHyperlink (which runs page integrity checks).
t.modes.set(Mode::GraphemeCluster, true);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
t.cursor_right(2);
t.print('#' as u32).unwrap();
assert_eq!(2, t.screen().cursor.x);
assert!(t.screen().cursor.pending_wrap);
// Without the fix, this panicked with UnwrappedSpacerHead.
t.print(0xFE0F).unwrap(); // VS16 to make wide
assert_eq!(1, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
assert!(!t.screen().cursor.pending_wrap);
// Previous cell turns into spacer_head and remains hyperlinked.
let cell = c(&t, 2, 0);
assert_eq!(0, cell.codepoint());
assert_eq!(Wide::SpacerHead, cell.wide());
assert!(cell.hyperlink());
assert!(row_at(&t, pt_screen(2, 0)).wrap());
// '#' cell is now wide and still hyperlinked.
let cell = c(&t, 0, 1);
assert_eq!('#' as u32, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(vec![0xFE0F], graphemes_at(&t, pt_screen(0, 1)).unwrap());
assert_eq!(Wide::Wide, cell.wide());
assert!(cell.hyperlink());
// spacer_tail inherits hyperlink as part of the same grapheme cell.
let cell = c(&t, 1, 1);
assert_eq!(0, cell.codepoint());
assert_eq!(Wide::SpacerTail, cell.wide());
assert!(cell.hyperlink());
}
#[test]
fn terminal_vs16_to_make_wide_character_with_pending_wrap() {
let mut t = init(3, 5);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
t.cursor_right(1);
t.print('#' as u32).unwrap();
assert_eq!(2, t.screen().cursor.x);
assert!(!t.screen().cursor.pending_wrap);
t.print(0xFE0F).unwrap(); // VS16 to make wide
assert_eq!(2, t.screen().cursor.x);
assert_eq!(0, t.screen().cursor.y);
assert!(t.screen().cursor.pending_wrap);
// '#' cell is wide
let cell = c(&t, 1, 0);
assert_eq!('#' as u32, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(vec![0xFE0F], graphemes_at(&t, pt_screen(1, 0)).unwrap());
assert_eq!(Wide::Wide, cell.wide());
// spacer_tail
let cell = c(&t, 2, 0);
assert_eq!(0, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::SpacerTail, cell.wide());
}
#[test]
fn terminal_vs16_to_make_wide_character_with_mode_2027() {
let mut t = init(5, 5);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
t.print(0x2764).unwrap(); // Heart
assert!(dirty(&t, 0, 0));
t.clear_dirty();
t.print(0xFE0F).unwrap(); // VS16 to make wide
assert!(dirty(&t, 0, 0));
t.clear_dirty();
assert_eq!("❤️", plain(&t));
let cell = c(&t, 0, 0);
assert_eq!(0x2764, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(Wide::Wide, cell.wide());
assert_eq!(1, graphemes_at(&t, pt_screen(0, 0)).unwrap().len());
}
#[test]
fn terminal_vs16_repeated_with_mode_2027() {
let mut t = init(5, 5);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
t.print(0x2764).unwrap(); // Heart
t.print(0xFE0F).unwrap(); // VS16 to make wide
t.print(0x2764).unwrap(); // Heart
t.print(0xFE0F).unwrap(); // VS16 to make wide
assert!(dirty(&t, 0, 0));
assert_eq!("❤️❤️", plain(&t));
for x in [0, 2] {
let cell = c(&t, x, 0);
assert_eq!(0x2764, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(Wide::Wide, cell.wide());
assert_eq!(1, graphemes_at(&t, pt_screen(x, 0)).unwrap().len());
}
}
#[test]
fn terminal_print_invalid_vs16_grapheme() {
let mut t = init(80, 80);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
// https://github.com/mitchellh/ghostty/issues/1482
t.print('x' as u32).unwrap();
t.print(0xFE0F).unwrap(); // invalid VS16
// We should have 1 cells taken up, and narrow.
assert_eq!(0, t.screen().cursor.y);
assert_eq!(1, t.screen().cursor.x);
let cell = c(&t, 0, 0);
assert_eq!('x' as u32, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
let cell = c(&t, 1, 0);
assert_eq!(0, cell.codepoint());
assert_eq!(Wide::Narrow, cell.wide());
}
#[test]
fn terminal_print_invalid_vs16_with_second_char() {
let mut t = init(80, 80);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
// https://github.com/mitchellh/ghostty/issues/1482
t.print('x' as u32).unwrap();
t.print(0xFE0F).unwrap();
t.print('y' as u32).unwrap();
// We should have 2 cells taken up, from two separate narrow characters.
assert_eq!(0, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
let cell = c(&t, 0, 0);
assert_eq!('x' as u32, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
let cell = c(&t, 1, 0);
assert_eq!('y' as u32, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
}
#[test]
fn terminal_print_grapheme_o_with_nonspacing_mark_should_be_narrow() {
let mut t = init(5, 5);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
t.print('o' as u32).unwrap();
t.print(0x0300).unwrap(); // combining grave accent
// We should have 1 cell taken up.
assert_eq!(0, t.screen().cursor.y);
assert_eq!(1, t.screen().cursor.x);
let cell = c(&t, 0, 0);
assert_eq!('o' as u32, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(vec![0x0300], graphemes_at(&t, pt_screen(0, 0)).unwrap());
assert_eq!(Wide::Narrow, cell.wide());
}
#[test]
fn terminal_print_devanagari_grapheme_should_be_wide() {
let mut t = init(5, 5);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
// क्‍ष
print_all(&mut t, &[0x0915, 0x094D, 0x200D, 0x0937]);
// We should have 2 cells taken up. It is one character but "wide".
assert_eq!(0, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
let cell = c(&t, 0, 0);
assert_eq!(0x0915, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(
vec![0x094D, 0x200D, 0x0937],
graphemes_at(&t, pt_screen(0, 0)).unwrap()
);
assert_eq!(Wide::Wide, cell.wide());
assert_eq!(Wide::SpacerTail, c(&t, 1, 0).wide());
}
#[test]
fn terminal_print_devanagari_grapheme_should_be_wide_on_next_line() {
let mut t = init(3, 5);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
t.cursor_right(2);
// क्‍ष
print_all(&mut t, &[0x0915, 0x094D, 0x200D]);
assert_eq!(2, t.screen().cursor.x);
assert!(t.screen().cursor.pending_wrap);
// This one increases the width to wide
t.print(0x0937).unwrap();
// We should have 2 cells taken up. It is one character but "wide".
assert_eq!(1, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
assert!(!t.screen().cursor.pending_wrap);
// Previous cell turns into spacer_head
let cell = c(&t, 2, 0);
assert_eq!(0, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::SpacerHead, cell.wide());
// Devanagari grapheme is wide
let cell = c(&t, 0, 1);
assert_eq!(0x0915, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(
vec![0x094D, 0x200D, 0x0937],
graphemes_at(&t, pt_screen(0, 1)).unwrap()
);
assert_eq!(Wide::Wide, cell.wide());
assert_eq!(Wide::SpacerTail, c(&t, 1, 1).wide());
}
#[test]
fn terminal_print_devanagari_grapheme_should_be_wide_on_next_page() {
let rows = crate::page::STD_CAPACITY.rows;
let cols = crate::page::STD_CAPACITY.cols;
let mut t = init(cols, rows);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
t.cursor_down(usize::from(rows - 1));
let first = t.screen().pages.first.unwrap();
let first_rows = t.screen().page(first).capacity.rows;
for _ in rows..first_rows {
t.index().unwrap();
}
t.cursor_right(usize::from(cols - 1));
assert_eq!(cols - 1, t.screen().cursor.x);
assert_eq!(rows - 1, t.screen().cursor.y);
// क्‍ष
print_all(&mut t, &[0x0915, 0x094D, 0x200D]);
assert_eq!(cols - 1, t.screen().cursor.x);
assert!(t.screen().cursor.pending_wrap);
// This one increases the width to wide
t.print(0x0937).unwrap();
// We should have 2 cells taken up. It is one character but "wide".
assert_eq!(rows - 1, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
assert!(!t.screen().cursor.pending_wrap);
// Previous cell turns into spacer_head
let cell = cell_at(&t, pt_active(cols - 1, u32::from(rows) - 2));
assert_eq!(0, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::SpacerHead, cell.wide());
// Devanagari grapheme is wide
let pt = pt_active(0, u32::from(rows) - 1);
let cell = cell_at(&t, pt);
assert_eq!(0x0915, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(vec![0x094D, 0x200D, 0x0937], graphemes_at(&t, pt).unwrap());
assert_eq!(Wide::Wide, cell.wide());
let cell = cell_at(&t, pt_active(1, u32::from(rows) - 1));
assert_eq!(Wide::SpacerTail, cell.wide());
}
#[test]
fn terminal_print_invalid_vs16_with_second_char_combining() {
let mut t = init(80, 80);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
// https://github.com/mitchellh/ghostty/issues/1482
t.print('n' as u32).unwrap();
t.print(0xFE0F).unwrap(); // invalid VS16
t.print(0x0303).unwrap(); // combining tilde
// We should have 1 cells taken up, and narrow.
assert_eq!(0, t.screen().cursor.y);
assert_eq!(1, t.screen().cursor.x);
let cell = c(&t, 0, 0);
assert_eq!('n' as u32, cell.codepoint());
assert!(cell.has_grapheme());
assert_eq!(vec![0x0303], graphemes_at(&t, pt_screen(0, 0)).unwrap());
assert_eq!(Wide::Narrow, cell.wide());
let cell = c(&t, 1, 0);
assert_eq!(0, cell.codepoint());
assert_eq!(Wide::Narrow, cell.wide());
}
#[test]
fn terminal_overwrite_grapheme_should_clear_grapheme_data() {
let mut t = init(5, 5);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
t.print(0x26C8).unwrap(); // Thunder cloud and rain
t.print(0xFE0E).unwrap(); // VS15 to make narrow
assert!(dirty(&t, 0, 0));
t.clear_dirty();
t.set_cursor_pos(1, 1);
t.print('A' as u32).unwrap();
assert!(dirty(&t, 0, 0));
assert_eq!("A", plain(&t));
let cell = c(&t, 0, 0);
assert_eq!('A' as u32, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
}
#[test]
fn terminal_overwrite_multicodepoint_grapheme_clears_grapheme_data() {
let mut t = init(10, 10);
// Enable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
// https://github.com/mitchellh/ghostty/issues/289
// This is: 👨‍👩‍👧 (which may or may not render correctly)
print_all(&mut t, &[0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F467]);
// We should have 2 cells taken up. It is one character but "wide".
assert_eq!(0, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
// We should have one cell with graphemes
assert_eq!(1, t.screen().cursor_page().grapheme_count());
// Move back and overwrite wide
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.print('X' as u32).unwrap();
assert!(dirty(&t, 0, 0));
assert_eq!(0, t.screen().cursor.y);
assert_eq!(1, t.screen().cursor.x);
assert_eq!(0, t.screen().cursor_page().grapheme_count());
assert_eq!("X", plain(&t));
}
@@ -0,0 +1,781 @@
use super::*;
use crate::page::{SemanticPrompt as RowPrompt, Wide};
fn hl_id(t: &Terminal, pt: Point) -> Option<u16> {
let p = pin_at(t, pt);
let page = t.screen().page(p.node);
let i = page.cell_index(usize::from(p.x), usize::from(p.y)).unwrap();
page.lookup_hyperlink(i)
}
fn print_chars(t: &mut Terminal, s: &str) {
for c in s.chars() {
t.print(u32::from(c)).unwrap();
}
}
#[test]
fn terminal_overwrite_multicodepoint_grapheme_tail_clears_grapheme_data() {
let mut t = init(10, 10);
t.modes.set(Mode::GraphemeCluster, true);
for cp in [0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F467] {
t.print(cp).unwrap();
}
assert_eq!(0, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
assert_eq!(1, t.screen().cursor_page().grapheme_count());
t.set_cursor_pos(1, 2);
t.print(u32::from('X')).unwrap();
assert_eq!(" X", plain(&t));
assert_eq!(0, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
assert_eq!(0, t.screen().cursor_page().grapheme_count());
}
#[test]
fn terminal_print_breaks_valid_grapheme_cluster_with_prepend_ascii_for_speed() {
let mut t = init(5, 5);
t.modes.set(Mode::GraphemeCluster, true);
t.print(u32::from('_')).unwrap();
t.print(0x0600).unwrap();
t.print(u32::from('1')).unwrap();
assert_eq!(0, t.screen().cursor.y);
assert_eq!(3, t.screen().cursor.x);
{
let cell = cell_at(&t, pt_screen(1, 0));
assert_eq!(0x0600, cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
}
{
let cell = cell_at(&t, pt_screen(2, 0));
assert_eq!(u32::from('1'), cell.codepoint());
assert!(!cell.has_grapheme());
assert_eq!(Wide::Narrow, cell.wide());
}
}
#[test]
fn terminal_print_writes_to_bottom_if_scrolled() {
let mut t = init(5, 2);
print_chars(&mut t, "hello");
t.set_cursor_pos(0, 0);
t.index().unwrap();
t.index().unwrap();
t.index().unwrap();
assert_eq!("", plain(&t));
t.screen_mut().scroll(Scroll::Top);
assert_eq!("hello", plain(&t));
t.print(u32::from('A')).unwrap();
t.screen_mut().scroll(Scroll::Active);
assert_eq!("\nA", plain(&t));
let (x, y) = (t.screen().cursor.x, t.screen().cursor.y);
assert!(t.is_dirty(pt_active(x, u32::from(y))));
}
#[test]
fn terminal_print_charset() {
let mut t = init(80, 80);
t.configure_charset(Slots::G1, Charset::DecSpecial);
t.configure_charset(Slots::G2, Charset::DecSpecial);
t.configure_charset(Slots::G3, Charset::DecSpecial);
assert!(!t.is_dirty(pt_screen(0, 0)));
t.print(u32::from('`')).unwrap();
t.configure_charset(Slots::G0, Charset::Utf8);
t.print(u32::from('`')).unwrap();
t.configure_charset(Slots::G0, Charset::Ascii);
t.print(u32::from('`')).unwrap();
t.configure_charset(Slots::G0, Charset::DecSpecial);
t.print(u32::from('`')).unwrap();
assert_eq!("```◆", plain(&t));
assert!(t.is_dirty(pt_screen(0, 0)));
}
#[test]
fn terminal_print_charset_outside_of_ascii() {
let mut t = init(80, 80);
t.configure_charset(Slots::G1, Charset::DecSpecial);
t.configure_charset(Slots::G2, Charset::DecSpecial);
t.configure_charset(Slots::G3, Charset::DecSpecial);
assert!(!t.is_dirty(pt_screen(0, 0)));
t.configure_charset(Slots::G0, Charset::DecSpecial);
t.print(u32::from('`')).unwrap();
t.print(0x1F600).unwrap();
assert_eq!("◆ ", plain(&t));
assert!(t.is_dirty(pt_screen(0, 0)));
}
#[test]
fn terminal_print_invoke_charset() {
let mut t = init(80, 80);
t.configure_charset(Slots::G1, Charset::DecSpecial);
t.print(u32::from('`')).unwrap();
t.clear_dirty();
t.invoke_charset(charsets::ActiveSlot::GL, Slots::G1, false);
assert!(!t.is_dirty(pt_screen(0, 0)));
t.print(u32::from('`')).unwrap();
assert!(t.is_dirty(pt_screen(0, 0)));
t.print(u32::from('`')).unwrap();
t.invoke_charset(charsets::ActiveSlot::GL, Slots::G0, false);
t.print(u32::from('`')).unwrap();
assert_eq!("`◆◆`", plain(&t));
}
#[test]
fn terminal_print_invoke_charset_single() {
let mut t = init(80, 80);
t.configure_charset(Slots::G1, Charset::DecSpecial);
t.print(u32::from('`')).unwrap();
t.invoke_charset(charsets::ActiveSlot::GL, Slots::G1, true);
t.print(u32::from('`')).unwrap();
t.print(u32::from('`')).unwrap();
assert_eq!("`◆`", plain(&t));
}
#[test]
fn terminal_print_kitty_unicode_placeholder() {
let mut t = init(10, 10);
t.print(0x10EEEE).unwrap();
assert_eq!(0, t.screen().cursor.y);
assert_eq!(1, t.screen().cursor.x);
{
let cell = cell_at(&t, pt_screen(0, 0));
assert_eq!(0x10EEEE, cell.codepoint());
assert!(row_at(&t, pt_screen(0, 0)).kitty_virtual_placeholder());
}
assert!(t.is_dirty(pt_screen(0, 0)));
}
#[test]
fn terminal_soft_wrap() {
let mut t = init(3, 80);
print_chars(&mut t, "hello");
assert_eq!(1, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
assert_eq!("hel\nlo", plain(&t));
}
#[test]
fn terminal_soft_wrap_with_semantic_prompt() {
let mut t = init(3, 80);
t.semantic_prompt(&osc::SemanticPrompt::init(PromptAction::PromptStart))
.unwrap();
assert!(!t.is_dirty(pt_screen(0, 0)));
print_chars(&mut t, "hello");
assert_eq!(
RowPrompt::Prompt,
row_at(&t, pt_screen(0, 0)).semantic_prompt()
);
assert_eq!(
RowPrompt::PromptContinuation,
row_at(&t, pt_screen(0, 1)).semantic_prompt()
);
}
#[test]
fn terminal_disabled_wraparound_with_wide_char_and_one_space() {
let mut t = init(5, 5);
t.modes.set(Mode::Wraparound, false);
print_str(&mut t, "AAAA");
t.clear_dirty();
t.print(0x1F6A8).unwrap();
assert_eq!(0, t.screen().cursor.y);
assert_eq!(4, t.screen().cursor.x);
assert_eq!("AAAA", plain(&t));
{
let cell = cell_at(&t, pt_screen(4, 0));
assert_eq!(0, cell.codepoint());
assert_eq!(Wide::Narrow, cell.wide());
}
assert!(!t.is_dirty(pt_screen(0, 0)));
}
#[test]
fn terminal_disabled_wraparound_with_wide_char_and_no_space() {
let mut t = init(5, 5);
t.modes.set(Mode::Wraparound, false);
print_str(&mut t, "AAAAA");
t.clear_dirty();
t.print(0x1F6A8).unwrap();
assert_eq!(0, t.screen().cursor.y);
assert_eq!(4, t.screen().cursor.x);
assert_eq!("AAAAA", plain(&t));
{
let cell = cell_at(&t, pt_screen(4, 0));
assert_eq!(u32::from('A'), cell.codepoint());
assert_eq!(Wide::Narrow, cell.wide());
}
assert!(!t.is_dirty(pt_screen(0, 0)));
}
#[test]
fn terminal_disabled_wraparound_with_wide_grapheme_and_half_space() {
let mut t = init(5, 5);
t.modes.set(Mode::GraphemeCluster, true);
t.modes.set(Mode::Wraparound, false);
print_str(&mut t, "AAAA");
t.print(0x2764).unwrap();
t.clear_dirty();
t.print(0xFE0F).unwrap();
assert_eq!(0, t.screen().cursor.y);
assert_eq!(4, t.screen().cursor.x);
assert_eq!("AAAA❤", plain(&t));
{
let cell = cell_at(&t, pt_screen(4, 0));
assert_eq!(0x2764, cell.codepoint());
assert_eq!(Wide::Narrow, cell.wide());
}
assert!(!t.is_dirty(pt_screen(0, 0)));
}
#[test]
fn terminal_print_right_margin_wrap() {
let mut t = init(10, 5);
print_str(&mut t, "123456789");
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(3, 5);
t.set_cursor_pos(1, 5);
print_str(&mut t, "XY");
assert_eq!("1234X6789\n Y", plain(&t));
assert!(!row_at(&t, pt_active(0, 0)).wrap());
}
#[test]
fn terminal_print_right_margin_wrap_dirty_tracking() {
let mut t = init(10, 5);
print_str(&mut t, "123456789");
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(3, 5);
t.set_cursor_pos(1, 5);
t.clear_dirty();
t.print(u32::from('X')).unwrap();
assert!(t.is_dirty(pt_screen(4, 0)));
assert!(!t.is_dirty(pt_screen(2, 1)));
t.clear_dirty();
t.print(u32::from('Y')).unwrap();
assert!(t.is_dirty(pt_screen(4, 0)));
assert!(t.is_dirty(pt_screen(2, 1)));
assert_eq!("1234X6789\n Y", plain(&t));
}
#[test]
fn terminal_print_right_margin_outside() {
let mut t = init(10, 5);
print_str(&mut t, "123456789");
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(3, 5);
t.set_cursor_pos(1, 6);
t.clear_dirty();
print_str(&mut t, "XY");
assert_eq!("12345XY89", plain(&t));
assert!(t.is_dirty(pt_screen(5, 0)));
}
#[test]
fn terminal_print_right_margin_outside_wrap() {
let mut t = init(10, 5);
print_str(&mut t, "123456789");
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(3, 5);
t.set_cursor_pos(1, 10);
print_str(&mut t, "XY");
assert_eq!("123456789X\n Y", plain(&t));
}
#[test]
fn terminal_print_wide_char_at_right_margin_does_not_create_spacer_head() {
let mut t = init(10, 10);
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(3, 5);
t.set_cursor_pos(1, 5);
t.print(0x1F600).unwrap();
assert_eq!(1, t.screen().cursor.y);
assert_eq!(4, t.screen().cursor.x);
assert!(t.is_dirty(pt_screen(4, 0)));
assert!(t.is_dirty(pt_screen(4, 1)));
{
let cell = cell_at(&t, pt_screen(4, 0));
assert_eq!(0, cell.codepoint());
assert_eq!(Wide::Narrow, cell.wide());
assert!(!row_at(&t, pt_screen(4, 0)).wrap());
}
{
let cell = cell_at(&t, pt_screen(2, 1));
assert_eq!(0x1F600, cell.codepoint());
assert_eq!(Wide::Wide, cell.wide());
}
{
let cell = cell_at(&t, pt_screen(3, 1));
assert_eq!(Wide::SpacerTail, cell.wide());
}
}
#[test]
fn terminal_print_with_hyperlink() {
let mut t = init(80, 80);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
print_str(&mut t, "123456");
for x in 0..6 {
assert!(row_at(&t, pt_screen(x, 0)).hyperlink());
assert!(cell_at(&t, pt_screen(x, 0)).hyperlink());
assert_eq!(Some(1), hl_id(&t, pt_screen(x, 0)));
}
assert!(t.is_dirty(pt_screen(0, 0)));
}
#[test]
fn terminal_print_over_cell_with_same_hyperlink() {
let mut t = init(80, 80);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
print_str(&mut t, "123456");
t.set_cursor_pos(1, 1);
print_str(&mut t, "123456");
for x in 0..6 {
assert!(row_at(&t, pt_screen(x, 0)).hyperlink());
assert!(cell_at(&t, pt_screen(x, 0)).hyperlink());
assert_eq!(Some(1), hl_id(&t, pt_screen(x, 0)));
}
assert!(t.is_dirty(pt_screen(0, 0)));
}
#[test]
fn terminal_print_and_end_hyperlink() {
let mut t = init(80, 80);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
print_str(&mut t, "123");
t.screen_mut().end_hyperlink();
print_str(&mut t, "456");
for x in 0..3 {
assert!(row_at(&t, pt_screen(x, 0)).hyperlink());
assert!(cell_at(&t, pt_screen(x, 0)).hyperlink());
assert_eq!(Some(1), hl_id(&t, pt_screen(x, 0)));
}
for x in 3..6 {
assert!(row_at(&t, pt_screen(x, 0)).hyperlink());
assert!(!cell_at(&t, pt_screen(x, 0)).hyperlink());
}
assert!(t.is_dirty(pt_screen(0, 0)));
}
#[test]
fn terminal_print_and_change_hyperlink() {
let mut t = init(80, 80);
t.screen_mut()
.start_hyperlink(b"http://one.example.com", None)
.unwrap();
print_str(&mut t, "123");
t.screen_mut()
.start_hyperlink(b"http://two.example.com", None)
.unwrap();
print_str(&mut t, "456");
for x in 0..3 {
assert!(cell_at(&t, pt_screen(x, 0)).hyperlink());
assert_eq!(Some(1), hl_id(&t, pt_screen(x, 0)));
}
for x in 3..6 {
assert!(cell_at(&t, pt_screen(x, 0)).hyperlink());
assert_eq!(Some(2), hl_id(&t, pt_screen(x, 0)));
}
assert!(t.is_dirty(pt_screen(0, 0)));
}
#[test]
fn terminal_overwrite_hyperlink() {
let mut t = init(80, 80);
t.screen_mut()
.start_hyperlink(b"http://one.example.com", None)
.unwrap();
print_str(&mut t, "123");
t.set_cursor_pos(1, 1);
t.screen_mut().end_hyperlink();
print_str(&mut t, "456");
for x in 0..3 {
let p = pin_at(&t, pt_screen(x, 0));
assert!(!row_at(&t, pt_screen(x, 0)).hyperlink());
assert!(!cell_at(&t, pt_screen(x, 0)).hyperlink());
assert_eq!(None, hl_id(&t, pt_screen(x, 0)));
assert_eq!(0, t.screen().page(p.node).hyperlink_set.count());
}
assert!(t.is_dirty(pt_screen(0, 0)));
}
#[test]
fn terminal_print_wide_char_at_right_edge_with_hyperlink() {
let mut t = init(10, 5);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
t.set_cursor_pos(1, 10);
t.print(0x4E2D).unwrap();
assert_eq!(1, t.screen().cursor.y);
assert_eq!(2, t.screen().cursor.x);
{
let cell = cell_at(&t, pt_screen(9, 0));
assert_eq!(Wide::SpacerHead, cell.wide());
assert!(cell.hyperlink());
assert!(row_at(&t, pt_screen(9, 0)).wrap());
}
{
let cell = cell_at(&t, pt_screen(0, 1));
assert_eq!(0x4E2D, cell.codepoint());
assert_eq!(Wide::Wide, cell.wide());
assert!(cell.hyperlink());
}
{
let cell = cell_at(&t, pt_screen(1, 1));
assert_eq!(Wide::SpacerTail, cell.wide());
assert!(cell.hyperlink());
}
}
#[test]
fn terminal_linefeed_and_carriage_return() {
let mut t = init(80, 80);
print_chars(&mut t, "hello");
t.clear_dirty();
t.carriage_return();
assert!(!t.is_dirty(pt_screen(0, 0)));
t.linefeed().unwrap();
assert!(t.is_dirty(pt_screen(0, 0)));
assert!(t.is_dirty(pt_screen(0, 1)));
print_chars(&mut t, "world");
assert_eq!(1, t.screen().cursor.y);
assert_eq!(5, t.screen().cursor.x);
assert_eq!("hello\nworld", plain(&t));
}
#[test]
fn terminal_linefeed_unsets_pending_wrap() {
let mut t = init(5, 80);
print_chars(&mut t, "hello");
assert!(t.screen().cursor.pending_wrap);
t.clear_dirty();
t.linefeed().unwrap();
assert!(t.is_dirty(pt_screen(0, 0)));
assert!(t.is_dirty(pt_screen(0, 1)));
assert!(!t.screen().cursor.pending_wrap);
}
#[test]
fn terminal_linefeed_mode_automatic_carriage_return() {
let mut t = init(10, 10);
t.modes.set(Mode::Linefeed, true);
print_str(&mut t, "123456");
t.linefeed().unwrap();
t.print(u32::from('X')).unwrap();
assert_eq!("123456\nX", plain(&t));
}
#[test]
fn terminal_carriage_return_unsets_pending_wrap() {
let mut t = init(5, 80);
print_chars(&mut t, "hello");
assert!(t.screen().cursor.pending_wrap);
t.carriage_return();
assert!(!t.screen().cursor.pending_wrap);
}
#[test]
fn terminal_carriage_return_origin_mode_moves_to_left_margin() {
let mut t = init(5, 80);
t.modes.set(Mode::Origin, true);
t.screen_mut().cursor.x = 0;
t.scrolling_region.left = 2;
t.carriage_return();
assert_eq!(2, t.screen().cursor.x);
}
#[test]
fn terminal_carriage_return_left_of_left_margin_moves_to_zero() {
let mut t = init(5, 80);
t.screen_mut().cursor.x = 1;
t.scrolling_region.left = 2;
t.carriage_return();
assert_eq!(0, t.screen().cursor.x);
}
#[test]
fn terminal_carriage_return_right_of_left_margin_moves_to_left_margin() {
let mut t = init(5, 80);
t.screen_mut().cursor.x = 3;
t.scrolling_region.left = 2;
t.carriage_return();
assert_eq!(2, t.screen().cursor.x);
}
#[test]
fn terminal_backspace() {
let mut t = init(80, 80);
print_chars(&mut t, "hello");
t.backspace();
t.print(u32::from('y')).unwrap();
assert_eq!(0, t.screen().cursor.y);
assert_eq!(5, t.screen().cursor.x);
assert_eq!("helly", plain(&t));
}
#[test]
fn terminal_horizontal_tabs() {
let mut t = init(20, 5);
t.print(u32::from('1')).unwrap();
t.horizontal_tab();
assert_eq!(8, t.screen().cursor.x);
t.horizontal_tab();
assert_eq!(16, t.screen().cursor.x);
t.horizontal_tab();
assert_eq!(19, t.screen().cursor.x);
t.horizontal_tab();
assert_eq!(19, t.screen().cursor.x);
}
#[test]
fn terminal_horizontal_tabs_starting_on_tabstop() {
let mut t = init(20, 5);
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y, 9);
t.print(u32::from('X')).unwrap();
t.set_cursor_pos(y, 9);
t.horizontal_tab();
t.print(u32::from('A')).unwrap();
assert_eq!(" X A", plain(&t));
}
#[test]
fn terminal_horizontal_tabs_with_right_margin() {
let mut t = init(20, 5);
t.scrolling_region.left = 2;
t.scrolling_region.right = 5;
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y, 1);
t.print(u32::from('X')).unwrap();
t.horizontal_tab();
t.print(u32::from('A')).unwrap();
assert_eq!("X A", plain(&t));
}
#[test]
fn terminal_horizontal_tabs_back() {
let mut t = init(20, 5);
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y, 20);
t.horizontal_tab_back();
assert_eq!(16, t.screen().cursor.x);
t.horizontal_tab_back();
assert_eq!(8, t.screen().cursor.x);
t.horizontal_tab_back();
assert_eq!(0, t.screen().cursor.x);
t.horizontal_tab_back();
assert_eq!(0, t.screen().cursor.x);
}
#[test]
fn terminal_horizontal_tabs_back_starting_on_tabstop() {
let mut t = init(20, 5);
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y, 9);
t.print(u32::from('X')).unwrap();
t.set_cursor_pos(y, 9);
t.horizontal_tab_back();
t.print(u32::from('A')).unwrap();
assert_eq!("A X", plain(&t));
}
#[test]
fn terminal_horizontal_tabs_with_left_margin_in_origin_mode() {
let mut t = init(20, 5);
t.modes.set(Mode::Origin, true);
t.scrolling_region.left = 2;
t.scrolling_region.right = 5;
t.set_cursor_pos(1, 2);
t.print(u32::from('X')).unwrap();
t.horizontal_tab_back();
t.print(u32::from('A')).unwrap();
assert_eq!(" AX", plain(&t));
}
#[test]
fn terminal_horizontal_tab_back_with_cursor_before_left_margin() {
let mut t = init(20, 5);
t.modes.set(Mode::Origin, true);
t.save_cursor();
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(5, 0);
t.restore_cursor();
t.horizontal_tab_back();
t.print(u32::from('X')).unwrap();
assert_eq!("X", plain(&t));
}
#[test]
fn terminal_cursor_pos_resets_wrap() {
let mut t = init(5, 5);
print_chars(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.set_cursor_pos(1, 1);
assert!(!t.screen().cursor.pending_wrap);
t.print(u32::from('X')).unwrap();
assert_eq!("XBCDE", plain(&t));
}
#[test]
fn terminal_cursor_pos_off_the_screen() {
let mut t = init(5, 5);
t.set_cursor_pos(500, 500);
t.print(u32::from('X')).unwrap();
assert_eq!("\n\n\n\n X", plain(&t));
}
#[test]
fn terminal_cursor_pos_relative_to_origin() {
let mut t = init(5, 5);
t.scrolling_region.top = 2;
t.scrolling_region.bottom = 3;
t.modes.set(Mode::Origin, true);
t.set_cursor_pos(1, 1);
t.print(u32::from('X')).unwrap();
assert_eq!("\n\nX", plain(&t));
}
#[test]
fn terminal_cursor_pos_relative_to_origin_with_left_right() {
let mut t = init(5, 5);
t.scrolling_region.top = 2;
t.scrolling_region.bottom = 3;
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
t.modes.set(Mode::Origin, true);
t.set_cursor_pos(1, 1);
t.print(u32::from('X')).unwrap();
assert_eq!("\n\n X", plain(&t));
}
#[test]
fn terminal_cursor_pos_limits_with_full_scroll_region() {
let mut t = init(5, 5);
t.scrolling_region.top = 2;
t.scrolling_region.bottom = 3;
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
t.modes.set(Mode::Origin, true);
t.set_cursor_pos(500, 500);
t.print(u32::from('X')).unwrap();
assert_eq!("\n\n\n X", plain(&t));
}
#[test]
fn terminal_set_cursor_pos_original_test() {
let mut t = init(80, 80);
assert_eq!(0, t.screen().cursor.x);
assert_eq!(0, t.screen().cursor.y);
t.set_cursor_pos(0, 0);
assert_eq!(0, t.screen().cursor.x);
assert_eq!(0, t.screen().cursor.y);
t.set_cursor_pos(81, 81);
assert_eq!(79, t.screen().cursor.x);
assert_eq!(79, t.screen().cursor.y);
t.set_cursor_pos(0, 80);
t.print(u32::from('c')).unwrap();
assert!(t.screen().cursor.pending_wrap);
t.set_cursor_pos(0, 80);
assert!(!t.screen().cursor.pending_wrap);
t.modes.set(Mode::Origin, true);
t.set_cursor_pos(81, 81);
assert_eq!(79, t.screen().cursor.x);
assert_eq!(79, t.screen().cursor.y);
t.set_top_and_bottom_margin(10, usize::from(t.rows));
t.set_cursor_pos(0, 0);
assert_eq!(0, t.screen().cursor.x);
assert_eq!(9, t.screen().cursor.y);
t.set_cursor_pos(1, 1);
assert_eq!(0, t.screen().cursor.x);
assert_eq!(9, t.screen().cursor.y);
t.set_cursor_pos(100, 0);
assert_eq!(0, t.screen().cursor.x);
assert_eq!(79, t.screen().cursor.y);
t.set_top_and_bottom_margin(10, 11);
t.set_cursor_pos(2, 0);
assert_eq!(0, t.screen().cursor.x);
assert_eq!(10, t.screen().cursor.y);
}
fn abc_def_ghi(t: &mut Terminal) {
print_str(t, "ABC");
t.carriage_return();
t.linefeed().unwrap();
print_str(t, "DEF");
t.carriage_return();
t.linefeed().unwrap();
print_str(t, "GHI");
}
#[test]
fn terminal_set_top_and_bottom_margin_simple() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_top_and_bottom_margin(0, 0);
t.clear_dirty();
t.scroll_down(1);
for y in 0..4 {
assert!(t.is_dirty(pt_active(0, y)));
}
assert_eq!("\nABC\nDEF\nGHI", plain(&t));
}
#[test]
fn terminal_set_top_and_bottom_margin_top_only() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_top_and_bottom_margin(2, 0);
t.clear_dirty();
t.scroll_down(1);
for y in 0..4 {
assert!(t.is_dirty(pt_active(0, y)));
}
assert_eq!("ABC\n\nDEF\nGHI", plain(&t));
}
@@ -0,0 +1,1061 @@
use super::*;
use crate::page::ContentTag;
use crate::point::Tag;
use ghostty_vt::color::Rgb;
use ghostty_vt::modes::Mode;
use ghostty_vt::sgr::Attribute as A;
fn cr_lf(t: &mut Terminal) {
t.carriage_return();
t.linefeed().unwrap();
}
/// "ABC\nDEF\nGHI" written with CR+LF between lines (no trailing CR/LF).
fn abc_def_ghi(t: &mut Terminal) {
print_str(t, "ABC");
cr_lf(t);
print_str(t, "DEF");
cr_lf(t);
print_str(t, "GHI");
}
fn dirty(t: &Terminal, y: u32) -> bool {
t.is_dirty(pt_active(0, y))
}
fn print_chars(t: &mut Terminal, s: &str) {
for c in s.chars() {
t.print(u32::from(c)).unwrap();
}
}
/// (row.hyperlink, cell.hyperlink, lookup id, hyperlink_set.count)
fn link_info(t: &Terminal, pt: Point) -> (bool, bool, Option<u16>, usize) {
let p = pin_at(t, pt);
let page = t.screen().page(p.node);
let (row, cell) = page
.get_row_and_cell(usize::from(p.x), usize::from(p.y))
.unwrap();
let i = page.cell_index(usize::from(p.x), usize::from(p.y)).unwrap();
(
row.hyperlink(),
cell.hyperlink(),
page.lookup_hyperlink(i),
page.hyperlink_set.count(),
)
}
fn expect_linked(t: &Terminal, x: u16, y: u32) {
let (row, cell, id, count) = link_info(t, pt_viewport(x, y));
assert!(row);
assert!(cell);
assert_eq!(Some(1), id);
assert_eq!(1, count);
}
fn expect_unlinked_cell(t: &Terminal, x: u16, y: u32) {
let (_, cell, id, _) = link_info(t, pt_viewport(x, y));
assert!(!cell);
assert!(id.is_none());
}
fn expect_unlinked_row(t: &Terminal, x: u16, y: u32) {
let (row, cell, id, _) = link_info(t, pt_viewport(x, y));
assert!(!row);
assert!(!cell);
assert!(id.is_none());
}
#[test]
fn terminal_set_top_and_bottom_margin_top_and_bottom() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_top_and_bottom_margin(1, 2);
t.clear_dirty();
t.scroll_down(1);
assert!(dirty(&t, 0));
assert!(dirty(&t, 1));
assert!(!dirty(&t, 2));
assert_eq!("\nABC\nGHI", plain(&t));
}
#[test]
fn terminal_set_top_and_bottom_margin_top_equal_to_bottom() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_top_and_bottom_margin(2, 2);
t.clear_dirty();
t.scroll_down(1);
for y in 0..4 {
assert!(dirty(&t, y));
}
assert_eq!("\nABC\nDEF\nGHI", plain(&t));
}
#[test]
fn terminal_set_left_and_right_margin_simple() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(0, 0);
t.clear_dirty();
t.erase_chars(1);
assert!(dirty(&t, 0));
assert!(!dirty(&t, 1));
assert_eq!(" BC\nDEF\nGHI", plain(&t));
}
#[test]
fn terminal_set_left_and_right_margin_left_only() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(2, 0);
assert_eq!(1, t.scrolling_region.left);
assert_eq!(t.cols - 1, t.scrolling_region.right);
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.insert_lines(1);
for y in 0..4 {
assert!(dirty(&t, y));
}
assert_eq!("A\nDBC\nGEF\n HI", plain(&t));
}
#[test]
fn terminal_set_left_and_right_margin_left_and_right() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(1, 2);
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.insert_lines(1);
for y in 0..4 {
assert!(dirty(&t, y));
}
assert_eq!(" C\nABF\nDEI\nGH", plain(&t));
}
#[test]
fn terminal_set_left_and_right_margin_left_equal_right() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(2, 2);
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.insert_lines(1);
for y in 0..4 {
assert!(dirty(&t, y));
}
assert_eq!("\nABC\nDEF\nGHI", plain(&t));
}
#[test]
fn terminal_set_left_and_right_margin_mode_69_unset() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.modes.set(Mode::EnableLeftAndRightMargin, false);
t.set_left_and_right_margin(1, 2);
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.insert_lines(1);
for y in 0..4 {
assert!(dirty(&t, y));
}
assert_eq!("\nABC\nDEF\nGHI", plain(&t));
}
#[test]
fn terminal_insert_lines_simple() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
t.clear_dirty();
t.insert_lines(1);
assert!(!dirty(&t, 0));
assert!(dirty(&t, 1));
assert!(dirty(&t, 2));
assert!(dirty(&t, 3));
assert_eq!("ABC\n\nDEF\nGHI", plain(&t));
}
#[test]
fn terminal_insert_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(A::DirectColorBg(Rgb {
r: 0xFF,
g: 0,
b: 0,
}))
.unwrap();
t.insert_lines(1);
assert_eq!("ABC\n\nDEF\nGHI", plain(&t));
for x in 0..t.cols {
let cell = cell_at(&t, pt_active(x, 1));
assert!(cell.content_tag() == ContentTag::BgColorRgb);
assert_eq!(
Rgb {
r: 0xFF,
g: 0,
b: 0
},
cell.color_rgb()
);
}
}
#[test]
fn terminal_insert_lines_handles_style_refs() {
let mut t = init(5, 3);
print_str(&mut t, "ABC");
cr_lf(&mut t);
print_str(&mut t, "DEF");
cr_lf(&mut t);
// For the line being deleted, create a refcounted style
t.set_attribute(A::Bold).unwrap();
print_str(&mut t, "GHI");
t.set_attribute(A::Unset).unwrap();
// verify we have styles in our style map
assert_eq!(1, t.screen().cursor_page().styles.count());
t.set_cursor_pos(2, 2);
t.insert_lines(1);
assert_eq!("ABC\n\nDEF", plain(&t));
// verify we have no styles in our style map
assert_eq!(0, t.screen().cursor_page().styles.count());
}
#[test]
fn terminal_insert_lines_outside_of_scroll_region() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_top_and_bottom_margin(3, 4);
t.set_cursor_pos(2, 2);
t.clear_dirty();
t.insert_lines(1);
assert!(!dirty(&t, 0));
assert!(!dirty(&t, 1));
assert!(!dirty(&t, 2));
assert_eq!("ABC\nDEF\nGHI", plain(&t));
}
#[test]
fn terminal_insert_lines_top_bottom_scroll_region() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
cr_lf(&mut t);
print_str(&mut t, "123");
t.set_top_and_bottom_margin(1, 3);
t.set_cursor_pos(2, 2);
t.clear_dirty();
t.insert_lines(1);
assert!(!dirty(&t, 0));
assert!(dirty(&t, 1));
assert!(dirty(&t, 2));
assert!(!dirty(&t, 3));
assert_eq!("ABC\n\nDEF\n123", plain(&t));
}
#[test]
fn terminal_insert_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 = usize::from(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");
cr_lf(&mut t);
print_str(&mut t, "2BBBB");
cr_lf(&mut t);
print_str(&mut t, "3CCCC");
cr_lf(&mut t);
print_str(&mut t, "4DDDD");
cr_lf(&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.insert_lines(1);
for y in 0..5 {
assert!(dirty(&t, y));
}
assert_eq!("\n1AAAA\n2BBBB\n3CCCC\n4DDDD", plain(&t));
}
#[test]
fn terminal_insert_lines_legacy_test() {
let mut t = init(2, 5);
// Initial value
for (i, c) in "ABCDE".chars().enumerate() {
if i > 0 {
cr_lf(&mut t);
}
t.print(u32::from(c)).unwrap();
}
// Move to row 2
t.set_cursor_pos(2, 1);
// Insert two lines
t.insert_lines(2);
assert_eq!("A\n\n\nB\nC", plain(&t));
}
#[test]
fn terminal_insert_lines_zero() {
let mut t = init(2, 5);
// This should do nothing
t.set_cursor_pos(1, 1);
t.insert_lines(0);
}
#[test]
fn terminal_insert_lines_with_scroll_region() {
let mut t = init(2, 6);
// Initial value
for (i, c) in "ABCDE".chars().enumerate() {
if i > 0 {
cr_lf(&mut t);
}
t.print(u32::from(c)).unwrap();
}
t.set_top_and_bottom_margin(1, 2);
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.insert_lines(1);
assert!(dirty(&t, 0));
assert!(dirty(&t, 1));
assert!(!dirty(&t, 2));
t.print(u32::from('X')).unwrap();
assert_eq!("X\nA\nC\nD\nE", plain(&t));
}
#[test]
fn terminal_insert_lines_more_than_remaining() {
let mut t = init(2, 5);
// Initial value
for (i, c) in "ABCDE".chars().enumerate() {
if i > 0 {
cr_lf(&mut t);
}
t.print(u32::from(c)).unwrap();
}
// Move to row 2
t.set_cursor_pos(2, 1);
// Insert a bunch of lines
t.clear_dirty();
t.insert_lines(20);
assert!(!dirty(&t, 0));
assert!(dirty(&t, 1));
assert!(dirty(&t, 2));
assert_eq!("A", plain(&t));
}
#[test]
fn terminal_insert_lines_resets_pending_wrap() {
let mut t = init(5, 5);
print_chars(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.insert_lines(1);
assert!(!t.screen().cursor.pending_wrap);
t.print(u32::from('B')).unwrap();
assert_eq!("B\nABCDE", plain(&t));
}
#[test]
fn terminal_insert_lines_resets_wrap() {
let mut t = init(3, 3);
t.print(u32::from('1')).unwrap();
cr_lf(&mut t);
print_chars(&mut t, "ABCDEF");
t.set_cursor_pos(1, 1);
t.insert_lines(1);
t.print(u32::from('X')).unwrap();
assert_eq!("X\n1\nABC", plain(&t));
assert!(!row_at(&t, pt_active(0, 2)).wrap());
}
#[test]
fn terminal_insert_lines_multi_codepoint_graphemes() {
let mut t = init(5, 5);
// Disable grapheme clustering
t.modes.set(Mode::GraphemeCluster, true);
print_str(&mut t, "ABC");
cr_lf(&mut t);
// This is: 👨‍👩‍👧 (which may or may not render correctly)
t.print(0x1F468).unwrap();
t.print(0x200D).unwrap();
t.print(0x1F469).unwrap();
t.print(0x200D).unwrap();
t.print(0x1F467).unwrap();
cr_lf(&mut t);
print_str(&mut t, "GHI");
t.set_cursor_pos(2, 2);
t.insert_lines(1);
assert_eq!("ABC\n\n👨‍👩‍👧\nGHI", plain(&t));
}
#[test]
fn terminal_insert_lines_left_right_scroll_region() {
let mut t = init(10, 10);
print_str(&mut t, "ABC123");
cr_lf(&mut t);
print_str(&mut t, "DEF456");
cr_lf(&mut t);
print_str(&mut t, "GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.set_cursor_pos(2, 2);
t.clear_dirty();
t.insert_lines(1);
assert!(!dirty(&t, 0));
assert!(dirty(&t, 1));
assert!(dirty(&t, 2));
assert!(dirty(&t, 3));
assert_eq!("ABC123\nD 56\nGEF489\n HI7", plain(&t));
}
#[test]
fn terminal_scroll_up_simple() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
let (cx, cy) = (t.screen().cursor.x, t.screen().cursor.y);
let viewport_before = t.screen().pages.get_top_left(Tag::Viewport).unwrap();
t.scroll_up(1).unwrap();
assert_eq!(cx, t.screen().cursor.x);
assert_eq!(cy, t.screen().cursor.y);
// Viewport should have moved. Our entire page should've scrolled!
// The viewport moving will cause our render state to make the full
// frame as dirty.
let viewport_after = t.screen().pages.get_top_left(Tag::Viewport).unwrap();
assert!(!viewport_before.eql(viewport_after));
assert_eq!("DEF\nGHI", plain(&t));
}
#[test]
fn terminal_scroll_up_moves_hyperlink() {
let mut t = init(5, 5);
print_str(&mut t, "ABC");
cr_lf(&mut t);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
print_str(&mut t, "DEF");
t.screen_mut().end_hyperlink();
cr_lf(&mut t);
print_str(&mut t, "GHI");
t.set_cursor_pos(2, 2);
t.scroll_up(1).unwrap();
assert_eq!("DEF\nGHI", plain(&t));
for x in 0..3 {
expect_linked(&t, x, 0);
}
for x in 0..3 {
expect_unlinked_row(&t, x, 1);
}
}
#[test]
fn terminal_scroll_up_clears_hyperlink() {
let mut t = init(5, 5);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
print_str(&mut t, "ABC");
t.screen_mut().end_hyperlink();
cr_lf(&mut t);
print_str(&mut t, "DEF");
cr_lf(&mut t);
print_str(&mut t, "GHI");
t.set_cursor_pos(2, 2);
t.scroll_up(1).unwrap();
assert_eq!("DEF\nGHI", plain(&t));
for x in 0..3 {
expect_unlinked_row(&t, x, 0);
}
}
#[test]
fn terminal_scroll_up_top_bottom_scroll_region() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_top_and_bottom_margin(2, 3);
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.scroll_up(1).unwrap();
// This is dirty because the cursor moves from this row
assert!(dirty(&t, 0));
assert!(dirty(&t, 1));
assert!(dirty(&t, 2));
assert_eq!("ABC\nGHI", plain(&t));
}
#[test]
fn terminal_scroll_up_left_right_scroll_region() {
let mut t = init(10, 10);
print_str(&mut t, "ABC123");
cr_lf(&mut t);
print_str(&mut t, "DEF456");
cr_lf(&mut t);
print_str(&mut t, "GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.set_cursor_pos(2, 2);
let (cx, cy) = (t.screen().cursor.x, t.screen().cursor.y);
t.clear_dirty();
t.scroll_up(1).unwrap();
assert_eq!(cx, t.screen().cursor.x);
assert_eq!(cy, t.screen().cursor.y);
assert!(dirty(&t, 0));
assert!(dirty(&t, 1));
assert!(dirty(&t, 2));
assert_eq!("AEF423\nDHI756\nG 89", plain(&t));
}
#[test]
fn terminal_scroll_up_left_right_scroll_region_hyperlink() {
let mut t = init(10, 10);
print_str(&mut t, "ABC123");
cr_lf(&mut t);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
print_str(&mut t, "DEF456");
t.screen_mut().end_hyperlink();
cr_lf(&mut t);
print_str(&mut t, "GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.set_cursor_pos(2, 2);
t.scroll_up(1).unwrap();
assert_eq!("AEF423\nDHI756\nG 89", plain(&t));
// First row gets some hyperlinks
for x in 0..1 {
expect_unlinked_cell(&t, x, 0);
}
for x in 1..4 {
expect_linked(&t, x, 0);
}
for x in 4..6 {
expect_unlinked_cell(&t, x, 0);
}
// Second row preserves hyperlink where we didn't scroll
for x in 0..1 {
expect_linked(&t, x, 1);
}
for x in 1..4 {
expect_unlinked_cell(&t, x, 1);
}
for x in 4..6 {
expect_linked(&t, x, 1);
}
}
#[test]
fn terminal_scroll_up_preserves_pending_wrap() {
let mut t = init(5, 5);
t.set_cursor_pos(1, 5);
t.print(u32::from('A')).unwrap();
t.set_cursor_pos(2, 5);
t.print(u32::from('B')).unwrap();
t.set_cursor_pos(3, 5);
t.print(u32::from('C')).unwrap();
t.scroll_up(1).unwrap();
t.print(u32::from('X')).unwrap();
assert_eq!(" B\n C\n\nX", plain(&t));
}
#[test]
fn terminal_scroll_up_full_top_bottom_region() {
let mut t = init(5, 5);
print_str(&mut t, "top");
t.set_cursor_pos(5, 1);
print_str(&mut t, "ABCDE");
t.set_top_and_bottom_margin(2, 5);
t.clear_dirty();
t.scroll_up(4).unwrap();
// This is dirty because the cursor moves from this row
assert!(dirty(&t, 0));
assert!(dirty(&t, 1));
assert_eq!("top", plain(&t));
}
#[test]
fn terminal_scroll_up_full_top_bottomleft_right_scroll_region() {
let mut t = init(5, 5);
print_str(&mut t, "top");
t.set_cursor_pos(5, 1);
print_str(&mut t, "ABCDE");
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_top_and_bottom_margin(2, 5);
t.set_left_and_right_margin(2, 4);
t.clear_dirty();
t.scroll_up(4).unwrap();
// This is dirty because the cursor moves from this row
assert!(dirty(&t, 0));
for y in 1..5 {
assert!(dirty(&t, y));
}
assert_eq!("top\n\n\n\nA E", plain(&t));
}
#[test]
fn terminal_scroll_up_creates_scrollback_in_primary_screen() {
// When in primary screen with full-width scroll region at top,
// scrollUp (CSI S) should push lines into scrollback like xterm.
let mut t = init_scrollback(5, 5, 10);
// Fill the screen with content
print_str(&mut t, "AAAAA");
cr_lf(&mut t);
print_str(&mut t, "BBBBB");
cr_lf(&mut t);
print_str(&mut t, "CCCCC");
cr_lf(&mut t);
print_str(&mut t, "DDDDD");
cr_lf(&mut t);
print_str(&mut t, "EEEEE");
t.clear_dirty();
// Scroll up by 1, which should push "AAAAA" into scrollback
t.scroll_up(1).unwrap();
// The cursor row (new empty row) should be dirty
assert!(t.screen().cursor_row().dirty());
// The active screen should now show BBBBB through EEEEE plus one blank line
assert_eq!("BBBBB\nCCCCC\nDDDDD\nEEEEE", plain(&t));
// Now scroll to the top to see scrollback - AAAAA should be there
t.screen_mut().scroll(Scroll::Top);
assert_eq!("AAAAA\nBBBBB\nCCCCC\nDDDDD\nEEEEE", plain(&t));
}
#[test]
fn terminal_scroll_up_with_max_scrollback_zero() {
// When max_scrollback is 0, scrollUp should still work but not retain history
let mut t = init_scrollback(5, 5, 0);
print_str(&mut t, "AAAAA");
cr_lf(&mut t);
print_str(&mut t, "BBBBB");
cr_lf(&mut t);
print_str(&mut t, "CCCCC");
t.scroll_up(1).unwrap();
// Active screen should show scrolled content
assert_eq!("BBBBB\nCCCCC", plain(&t));
// Scroll to top - should be same as active since no scrollback
t.screen_mut().scroll(Scroll::Top);
assert_eq!("BBBBB\nCCCCC", plain(&t));
}
#[test]
fn terminal_scroll_up_with_max_scrollback_zero_and_top_margin() {
// When max_scrollback is 0 and top margin is set, should use deleteLines path
let mut t = init_scrollback(5, 5, 0);
print_str(&mut t, "AAAAA");
cr_lf(&mut t);
print_str(&mut t, "BBBBB");
cr_lf(&mut t);
print_str(&mut t, "CCCCC");
cr_lf(&mut t);
print_str(&mut t, "DDDDD");
// Set top margin (not at row 0)
t.set_top_and_bottom_margin(2, 5);
t.scroll_up(1).unwrap();
// First row preserved, rest scrolled
assert_eq!("AAAAA\nCCCCC\nDDDDD", plain(&t));
}
#[test]
fn terminal_scroll_up_with_max_scrollback_zero_and_left_right_margin() {
// When max_scrollback is 0 with left/right margins, uses deleteLines path
let mut t = init_scrollback(10, 5, 0);
print_str(&mut t, "AAAAABBBBB");
cr_lf(&mut t);
print_str(&mut t, "CCCCCDDDDD");
cr_lf(&mut t);
print_str(&mut t, "EEEEEFFFFF");
// Set left/right margins (columns 2-6, 1-indexed = indices 1-5)
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(2, 6);
t.scroll_up(1).unwrap();
// cols 1-5 scroll, col 0 and cols 6+ preserved
assert_eq!("ACCCCDBBBB\nCEEEEFDDDD\nE FFFF", plain(&t));
}
#[test]
fn terminal_scroll_down_simple() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
let (cx, cy) = (t.screen().cursor.x, t.screen().cursor.y);
t.clear_dirty();
t.scroll_down(1);
assert_eq!(cx, t.screen().cursor.x);
assert_eq!(cy, t.screen().cursor.y);
for y in 0..5 {
assert!(dirty(&t, y));
}
assert_eq!("\nABC\nDEF\nGHI", plain(&t));
}
#[test]
fn terminal_scroll_down_hyperlink_moves() {
let mut t = init(5, 5);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
print_str(&mut t, "ABC");
t.screen_mut().end_hyperlink();
cr_lf(&mut t);
print_str(&mut t, "DEF");
cr_lf(&mut t);
print_str(&mut t, "GHI");
t.set_cursor_pos(2, 2);
t.scroll_down(1);
assert_eq!("\nABC\nDEF\nGHI", plain(&t));
for x in 0..3 {
expect_linked(&t, x, 1);
}
for x in 0..3 {
expect_unlinked_row(&t, x, 0);
}
}
#[test]
fn terminal_scroll_down_outside_of_scroll_region() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_top_and_bottom_margin(3, 4);
t.set_cursor_pos(2, 2);
let (cx, cy) = (t.screen().cursor.x, t.screen().cursor.y);
t.clear_dirty();
t.scroll_down(1);
assert_eq!(cx, t.screen().cursor.x);
assert_eq!(cy, t.screen().cursor.y);
assert!(!dirty(&t, 0));
// This is dirty because the cursor moves from this row
assert!(dirty(&t, 1));
assert!(dirty(&t, 2));
assert!(dirty(&t, 3));
assert_eq!("ABC\nDEF\n\nGHI", plain(&t));
}
#[test]
fn terminal_scroll_down_left_right_scroll_region() {
let mut t = init(10, 10);
print_str(&mut t, "ABC123");
cr_lf(&mut t);
print_str(&mut t, "DEF456");
cr_lf(&mut t);
print_str(&mut t, "GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.set_cursor_pos(2, 2);
let (cx, cy) = (t.screen().cursor.x, t.screen().cursor.y);
t.clear_dirty();
t.scroll_down(1);
assert_eq!(cx, t.screen().cursor.x);
assert_eq!(cy, t.screen().cursor.y);
for y in 0..4 {
assert!(dirty(&t, y));
}
assert_eq!("A 23\nDBC156\nGEF489\n HI7", plain(&t));
}
#[test]
fn terminal_scroll_down_left_right_scroll_region_hyperlink() {
let mut t = init(10, 10);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
print_str(&mut t, "ABC123");
t.screen_mut().end_hyperlink();
cr_lf(&mut t);
print_str(&mut t, "DEF456");
cr_lf(&mut t);
print_str(&mut t, "GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.set_cursor_pos(2, 2);
t.scroll_down(1);
assert_eq!("A 23\nDBC156\nGEF489\n HI7", plain(&t));
// First row preserves hyperlink where we didn't scroll
for x in 0..1 {
expect_linked(&t, x, 0);
}
for x in 1..4 {
expect_unlinked_cell(&t, x, 0);
}
for x in 4..6 {
expect_linked(&t, x, 0);
}
// Second row gets some hyperlinks
for x in 0..1 {
expect_unlinked_cell(&t, x, 1);
}
for x in 1..4 {
expect_linked(&t, x, 1);
}
for x in 4..6 {
expect_unlinked_cell(&t, x, 1);
}
}
#[test]
fn terminal_scroll_down_outside_of_left_right_scroll_region() {
let mut t = init(10, 10);
print_str(&mut t, "ABC123");
cr_lf(&mut t);
print_str(&mut t, "DEF456");
cr_lf(&mut t);
print_str(&mut t, "GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.set_cursor_pos(1, 1);
let (cx, cy) = (t.screen().cursor.x, t.screen().cursor.y);
t.clear_dirty();
t.scroll_down(1);
assert_eq!(cx, t.screen().cursor.x);
assert_eq!(cy, t.screen().cursor.y);
for y in 0..4 {
assert!(dirty(&t, y));
}
assert_eq!("A 23\nDBC156\nGEF489\n HI7", plain(&t));
}
#[test]
fn terminal_scroll_down_preserves_pending_wrap() {
let mut t = init(5, 10);
t.set_cursor_pos(1, 5);
t.print(u32::from('A')).unwrap();
t.set_cursor_pos(2, 5);
t.print(u32::from('B')).unwrap();
t.set_cursor_pos(3, 5);
t.print(u32::from('C')).unwrap();
t.scroll_down(1);
t.print(u32::from('X')).unwrap();
assert_eq!("\n A\n B\nX C", plain(&t));
}
#[test]
fn terminal_erase_chars_simple_operation() {
let mut t = init(5, 5);
print_chars(&mut t, "ABC");
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.erase_chars(2);
t.print(u32::from('X')).unwrap();
assert!(dirty(&t, 0));
assert!(!dirty(&t, 1));
assert_eq!("X C", plain(&t));
}
#[test]
fn terminal_erase_chars_minimum_one() {
let mut t = init(5, 5);
print_chars(&mut t, "ABC");
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.erase_chars(0);
t.print(u32::from('X')).unwrap();
assert!(dirty(&t, 0));
assert_eq!("XBC", plain(&t));
}
#[test]
fn terminal_erase_chars_beyond_screen_edge() {
let mut t = init(5, 5);
print_chars(&mut t, " ABC");
t.set_cursor_pos(1, 4);
t.erase_chars(10);
assert_eq!(" A", plain(&t));
}
#[test]
fn terminal_erase_chars_wide_character() {
let mut t = init(5, 5);
t.print(u32::from('橋')).unwrap();
print_chars(&mut t, "BC");
t.set_cursor_pos(1, 1);
t.erase_chars(1);
t.print(u32::from('X')).unwrap();
assert_eq!("X BC", plain(&t));
}
#[test]
fn terminal_erase_chars_resets_pending_wrap() {
let mut t = init(5, 5);
print_chars(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.erase_chars(1);
assert!(!t.screen().cursor.pending_wrap);
t.print(u32::from('X')).unwrap();
assert_eq!("ABCDX", plain(&t));
}
#[test]
fn terminal_erase_chars_resets_wrap() {
let mut t = init(5, 5);
print_chars(&mut t, "ABCDE123");
assert!(row_at(&t, pt_active(0, 0)).wrap());
t.set_cursor_pos(1, 1);
t.erase_chars(1);
assert!(!row_at(&t, pt_active(0, 0)).wrap());
t.print(u32::from('X')).unwrap();
assert_eq!("XBCDE\n123", plain(&t));
}
@@ -0,0 +1,731 @@
use super::*;
use ghostty_vt::color::Rgb;
use ghostty_vt::sgr::Attribute as A;
const RED: Rgb = Rgb {
r: 0xFF,
g: 0,
b: 0,
};
fn chars(t: &mut Terminal, s: &str) {
for c in s.chars() {
t.print(u32::from(c)).unwrap();
}
}
fn set_bg_red(t: &mut Terminal) {
t.set_attribute(A::DirectColorBg(RED)).unwrap();
}
fn assert_bg_red(t: &Terminal, pt: Point) {
let c = cell_at(t, pt);
assert_eq!(ContentTag::BgColorRgb, c.content_tag());
assert_eq!(RED, c.color_rgb());
}
fn hl_id(t: &Terminal, pt: Point) -> (bool, bool, Option<u16>) {
let p = pin_at(t, pt);
let page = t.screen().page(p.node);
let i = page.cell_index(usize::from(p.x), usize::from(p.y)).unwrap();
(
page.rows[usize::from(p.y)].hyperlink(),
page.cells[i].hyperlink(),
page.lookup_hyperlink(i),
)
}
#[test]
fn terminal_erase_chars_preserves_background_sgr() {
let mut t = init(10, 10);
chars(&mut t, "ABC");
t.set_cursor_pos(1, 1);
set_bg_red(&mut t);
t.erase_chars(2);
assert_eq!(" C", plain(&t));
assert_bg_red(&t, pt_active(0, 0));
assert_bg_red(&t, pt_active(1, 0));
}
#[test]
fn terminal_erase_chars_handles_refcounted_styles() {
let mut t = init(10, 10);
t.set_attribute(A::Bold).unwrap();
t.print('A' as u32).unwrap();
t.print('B' as u32).unwrap();
t.set_attribute(A::Unset).unwrap();
t.print('C' as u32).unwrap();
assert_eq!(1, t.screen().cursor_page().styles.count());
t.set_cursor_pos(1, 1);
t.erase_chars(2);
assert_eq!(0, t.screen().cursor_page().styles.count());
}
#[test]
fn terminal_erase_chars_protected_attributes_respected_with_iso() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Iso);
chars(&mut t, "ABC");
t.set_cursor_pos(1, 1);
t.erase_chars(2);
assert_eq!("ABC", plain(&t));
}
#[test]
fn terminal_erase_chars_protected_attributes_ignored_with_dec_most_recent() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Iso);
chars(&mut t, "ABC");
t.set_protected_mode(ProtectedMode::Dec);
t.set_protected_mode(ProtectedMode::Off);
t.set_cursor_pos(1, 1);
t.erase_chars(2);
assert_eq!(" C", plain(&t));
}
#[test]
fn terminal_erase_chars_protected_attributes_ignored_with_dec_set() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Dec);
chars(&mut t, "ABC");
t.set_cursor_pos(1, 1);
t.erase_chars(2);
assert_eq!(" C", plain(&t));
}
#[test]
fn terminal_erase_chars_wide_char_boundary_conditions() {
let mut t = init(8, 1);
print_str(&mut t, "😀a😀b😀");
assert_eq!("😀a😀b😀", plain(&t));
t.set_cursor_pos(1, 2);
t.erase_chars(3);
t.screen().cursor_page().assert_integrity();
assert_eq!(" b😀", plain(&t));
}
#[test]
fn terminal_erase_chars_wide_char_splits_proper_cell_boundaries() {
let mut t = init(30, 1);
print_str(&mut t, "x食べて下さい");
assert_eq!("x食べて下さい", plain(&t));
t.set_cursor_pos(1, 6); // At: て
t.erase_chars(4); // Delete: て下
t.screen().cursor_page().assert_integrity();
assert_eq!("x食べ さい", plain(&t));
}
#[test]
fn terminal_erase_chars_wide_char_wrap_boundary_conditions() {
let mut t = init(8, 3);
print_str(&mut t, ".......😀abcde😀......");
assert_eq!(".......\n😀abcde\n😀......", plain(&t));
assert_eq!(".......😀abcde😀......", plain_unwrapped(&t));
t.set_cursor_pos(2, 2);
t.erase_chars(3);
t.screen().cursor_page().assert_integrity();
assert_eq!(".......\n cde\n😀......", plain(&t));
assert_eq!("....... cde\n😀......", plain_unwrapped(&t));
}
#[test]
fn terminal_reverse_index() {
let mut t = init(2, 5);
t.print('A' as u32).unwrap();
t.carriage_return();
t.linefeed().unwrap();
t.print('B' as u32).unwrap();
t.carriage_return();
t.linefeed().unwrap();
t.print('C' as u32).unwrap();
t.reverse_index();
t.print('D' as u32).unwrap();
t.carriage_return();
t.linefeed().unwrap();
t.carriage_return();
t.linefeed().unwrap();
assert_eq!("A\nBD\nC", plain(&t));
}
#[test]
fn terminal_reverse_index_from_the_top() {
let mut t = init(2, 5);
t.print('A' as u32).unwrap();
t.carriage_return();
t.linefeed().unwrap();
t.print('B' as u32).unwrap();
t.carriage_return();
t.linefeed().unwrap();
t.carriage_return();
t.linefeed().unwrap();
t.set_cursor_pos(1, 1);
t.reverse_index();
t.print('D' as u32).unwrap();
t.carriage_return();
t.linefeed().unwrap();
t.set_cursor_pos(1, 1);
t.reverse_index();
t.print('E' as u32).unwrap();
t.carriage_return();
t.linefeed().unwrap();
assert_eq!("E\nD\nA\nB", plain(&t));
}
#[test]
fn terminal_reverse_index_top_of_scrolling_region() {
let mut t = init(2, 10);
t.set_cursor_pos(2, 1);
for c in ['A', 'B', 'C', 'D'] {
t.print(c as u32).unwrap();
t.carriage_return();
t.linefeed().unwrap();
}
t.set_top_and_bottom_margin(2, 5);
t.set_cursor_pos(2, 1);
t.reverse_index();
t.print('X' as u32).unwrap();
assert_eq!("\nX\nA\nB\nC", plain(&t));
}
#[test]
fn terminal_reverse_index_top_of_screen() {
let mut t = init(5, 5);
t.print('A' as u32).unwrap();
t.set_cursor_pos(2, 1);
t.print('B' as u32).unwrap();
t.set_cursor_pos(3, 1);
t.print('C' as u32).unwrap();
t.set_cursor_pos(1, 1);
t.reverse_index();
t.print('X' as u32).unwrap();
assert_eq!("X\nA\nB\nC", plain(&t));
}
#[test]
fn terminal_reverse_index_not_top_of_screen() {
let mut t = init(5, 5);
t.print('A' as u32).unwrap();
t.set_cursor_pos(2, 1);
t.print('B' as u32).unwrap();
t.set_cursor_pos(3, 1);
t.print('C' as u32).unwrap();
t.set_cursor_pos(2, 1);
t.reverse_index();
t.print('X' as u32).unwrap();
assert_eq!("X\nB\nC", plain(&t));
}
#[test]
fn terminal_reverse_index_top_bottom_margins() {
let mut t = init(5, 5);
t.print('A' as u32).unwrap();
t.set_cursor_pos(2, 1);
t.print('B' as u32).unwrap();
t.set_cursor_pos(3, 1);
t.print('C' as u32).unwrap();
t.set_top_and_bottom_margin(2, 3);
t.set_cursor_pos(2, 1);
t.reverse_index();
assert_eq!("A\n\nB", plain(&t));
}
#[test]
fn terminal_reverse_index_outside_top_bottom_margins() {
let mut t = init(5, 5);
t.print('A' as u32).unwrap();
t.set_cursor_pos(2, 1);
t.print('B' as u32).unwrap();
t.set_cursor_pos(3, 1);
t.print('C' as u32).unwrap();
t.set_top_and_bottom_margin(2, 3);
t.set_cursor_pos(1, 1);
t.reverse_index();
assert_eq!("A\nB\nC", plain(&t));
}
fn abc_def_ghi(t: &mut Terminal) {
print_str(t, "ABC");
t.set_cursor_pos(2, 1);
print_str(t, "DEF");
t.set_cursor_pos(3, 1);
print_str(t, "GHI");
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(2, 3);
}
#[test]
fn terminal_reverse_index_left_right_margins() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_cursor_pos(1, 2);
t.reverse_index();
assert_eq!("A\nDBC\nGEF\n HI", plain(&t));
}
#[test]
fn terminal_reverse_index_outside_left_right_margins() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_cursor_pos(1, 1);
t.reverse_index();
assert_eq!("ABC\nDEF\nGHI", plain(&t));
}
#[test]
fn terminal_index() {
let mut t = init(2, 5);
t.index().unwrap();
t.print('A' as u32).unwrap();
assert!(t.is_dirty(pt_active(0, 0)));
assert!(t.is_dirty(pt_active(0, 1)));
assert_eq!("\nA", plain(&t));
}
#[test]
fn terminal_index_from_the_bottom() {
let mut t = init(2, 5);
t.set_cursor_pos(5, 1);
t.print('A' as u32).unwrap();
t.cursor_left(1); // undo moving right from 'A'
t.clear_dirty();
t.index().unwrap();
t.print('B' as u32).unwrap();
assert!(t.is_dirty(pt_active(0, 3)));
assert!(t.is_dirty(pt_active(0, 4)));
assert_eq!("\n\n\nA\nB", plain(&t));
}
#[test]
fn terminal_index_scrolling_with_hyperlink() {
let mut t = init(2, 5);
t.set_cursor_pos(5, 1);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
t.print('A' as u32).unwrap();
t.screen_mut().end_hyperlink();
t.cursor_left(1); // undo moving right from 'A'
t.index().unwrap();
t.print('B' as u32).unwrap();
assert_eq!("\n\n\nA\nB", plain(&t));
let (row, cell, id) = hl_id(&t, pt_viewport(0, 3));
assert!(row);
assert!(cell);
assert_eq!(Some(1), id);
let (row, cell, id) = hl_id(&t, pt_viewport(0, 4));
assert!(!row);
assert!(!cell);
assert_eq!(None, id);
}
#[test]
fn terminal_index_outside_of_scrolling_region() {
let mut t = init(2, 5);
assert_eq!(0, t.screen().cursor.y);
t.set_top_and_bottom_margin(2, 5);
t.index().unwrap();
assert_eq!(1, t.screen().cursor.y);
}
#[test]
fn terminal_index_from_the_bottom_outside_of_scroll_region() {
let mut t = init(2, 5);
t.set_top_and_bottom_margin(1, 2);
t.set_cursor_pos(5, 1);
t.print('A' as u32).unwrap();
t.clear_dirty();
t.index().unwrap();
t.print('B' as u32).unwrap();
assert!(t.is_dirty(pt_active(0, 4)));
assert_eq!("\n\n\n\nAB", plain(&t));
}
#[test]
fn terminal_index_no_scroll_region_top_of_screen() {
let mut t = init(5, 5);
t.print('A' as u32).unwrap();
t.clear_dirty();
t.index().unwrap();
t.print('X' as u32).unwrap();
assert!(t.is_dirty(pt_active(0, 0)));
assert!(t.is_dirty(pt_active(0, 1)));
assert_eq!("A\n X", plain(&t));
}
#[test]
fn terminal_index_bottom_of_primary_screen() {
let mut t = init(5, 5);
t.set_cursor_pos(5, 1);
t.print('A' as u32).unwrap();
t.clear_dirty();
t.index().unwrap();
t.print('X' as u32).unwrap();
assert!(t.is_dirty(pt_active(0, 3)));
assert!(t.is_dirty(pt_active(0, 4)));
assert_eq!("\n\n\nA\n X", plain(&t));
}
#[test]
fn terminal_index_bottom_of_primary_screen_background_sgr() {
let mut t = init(5, 5);
t.set_cursor_pos(5, 1);
t.print('A' as u32).unwrap();
set_bg_red(&mut t);
t.index().unwrap();
assert_eq!("\n\n\nA", plain(&t));
for x in 0..5 {
assert_bg_red(&t, pt_active(x, 4));
}
}
#[test]
fn terminal_index_inside_scroll_region() {
let mut t = init(5, 5);
t.set_top_and_bottom_margin(1, 3);
t.print('A' as u32).unwrap();
t.clear_dirty();
t.index().unwrap();
t.print('X' as u32).unwrap();
assert!(t.is_dirty(pt_active(0, 0)));
assert!(t.is_dirty(pt_active(0, 1)));
assert_eq!("A\n X", plain(&t));
}
#[test]
fn terminal_index_bottom_of_scroll_region_with_hyperlinks() {
let mut t = init(5, 5);
t.set_top_and_bottom_margin(1, 2);
t.print('A' as u32).unwrap();
t.index().unwrap();
t.carriage_return();
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
t.print('B' as u32).unwrap();
t.screen_mut().end_hyperlink();
t.index().unwrap();
t.carriage_return();
t.print('C' as u32).unwrap();
assert_eq!("B\nC", plain(&t));
let (row, cell, id) = hl_id(&t, pt_viewport(0, 0));
assert!(row);
assert!(cell);
assert_eq!(Some(1), id);
let (row, cell, id) = hl_id(&t, pt_viewport(0, 1));
assert!(!row);
assert!(!cell);
assert_eq!(None, id);
}
#[test]
#[ignore = "pagelist: erase_row_bounded leaves the row hyperlink flag set on the blanked row"]
fn terminal_index_bottom_of_scroll_region_clear_hyperlinks() {
let mut t = init_scrollback(5, 5, 0);
t.set_top_and_bottom_margin(2, 3);
t.set_cursor_pos(2, 1);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
t.print('A' as u32).unwrap();
t.screen_mut().end_hyperlink();
t.index().unwrap();
t.carriage_return();
t.print('B' as u32).unwrap();
t.index().unwrap();
t.carriage_return();
t.print('C' as u32).unwrap();
assert_eq!("\nB\nC", plain(&t));
for y in 1..3 {
let (row, cell, id) = hl_id(&t, pt_viewport(0, y));
assert!(!row);
assert!(!cell);
assert_eq!(None, id);
let p = pin_at(&t, pt_viewport(0, y));
assert_eq!(0, t.screen().page(p.node).hyperlink_set.count());
}
}
#[test]
fn terminal_index_bottom_of_scroll_region_with_background_sgr() {
let mut t = init(5, 5);
t.set_top_and_bottom_margin(1, 3);
t.set_cursor_pos(4, 1);
t.print('B' as u32).unwrap();
t.set_cursor_pos(3, 1);
t.print('A' as u32).unwrap();
set_bg_red(&mut t);
t.index().unwrap();
assert_eq!("\nA\n\nB", plain(&t));
for x in 0..t.cols {
assert_bg_red(&t, pt_active(x, 2));
}
}
#[test]
fn terminal_index_bottom_of_primary_screen_with_scroll_region() {
let mut t = init(5, 5);
t.set_top_and_bottom_margin(1, 3);
t.set_cursor_pos(3, 1);
t.print('A' as u32).unwrap();
t.set_cursor_pos(5, 1);
t.clear_dirty();
t.index().unwrap();
t.index().unwrap();
t.index().unwrap();
t.print('X' as u32).unwrap();
for y in 0..4 {
assert!(!t.is_dirty(pt_active(0, y)));
}
assert!(t.is_dirty(pt_active(0, 4)));
assert_eq!("\n\nA\n\nX", plain(&t));
}
#[test]
fn terminal_index_outside_left_right_margin() {
let mut t = init(10, 5);
t.set_top_and_bottom_margin(1, 3);
t.scrolling_region.left = 3;
t.scrolling_region.right = 5;
t.set_cursor_pos(3, 3);
t.print('A' as u32).unwrap();
t.set_cursor_pos(3, 1);
t.clear_dirty();
t.index().unwrap();
t.print('X' as u32).unwrap();
assert!(t.is_dirty(pt_active(0, 2)));
assert_eq!("\n\nX A", plain(&t));
}
#[test]
fn terminal_index_inside_left_right_margin() {
let mut t = init(10, 5);
print_str(&mut t, "AAAAAA");
t.carriage_return();
t.linefeed().unwrap();
print_str(&mut t, "AAAAAA");
t.carriage_return();
t.linefeed().unwrap();
print_str(&mut t, "AAAAAA");
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_top_and_bottom_margin(1, 3);
t.set_left_and_right_margin(1, 3);
t.set_cursor_pos(3, 1);
t.clear_dirty();
t.index().unwrap();
assert!(t.is_dirty(pt_active(0, 0)));
assert!(t.is_dirty(pt_active(0, 1)));
assert!(t.is_dirty(pt_active(0, 2)));
assert_eq!(2, t.screen().cursor.y);
assert_eq!(0, t.screen().cursor.x);
assert_eq!("AAAAAA\nAAAAAA\n AAA", plain(&t));
}
#[test]
fn terminal_index_bottom_of_scroll_region_creates_scrollback() {
let mut t = init(5, 5);
t.set_top_and_bottom_margin(1, 3);
print_str(&mut t, "1\n2\n3");
t.set_cursor_pos(4, 1);
t.print('X' as u32).unwrap();
t.set_cursor_pos(3, 1);
t.index().unwrap();
t.print('Y' as u32).unwrap();
assert_eq!("2\n3\nY\nX", dump(&t, pt_viewport(0, 0)));
assert_eq!("1\n2\n3\nY\nX", dump(&t, pt_screen(0, 0)));
}
#[test]
fn terminal_index_bottom_of_scroll_region_no_scrollback() {
let mut t = init_scrollback(5, 5, 0);
t.set_top_and_bottom_margin(1, 3);
t.set_cursor_pos(4, 1);
t.print('B' as u32).unwrap();
t.set_cursor_pos(3, 1);
t.print('A' as u32).unwrap();
t.clear_dirty();
t.index().unwrap();
t.print('X' as u32).unwrap();
assert_eq!("\nA\n X\nB", plain(&t));
}
#[test]
fn terminal_index_bottom_of_scroll_region_blank_line_preserves_sgr() {
let mut t = init(5, 5);
t.set_top_and_bottom_margin(1, 3);
print_str(&mut t, "1\n2\n3");
t.set_cursor_pos(4, 1);
t.print('X' as u32).unwrap();
t.set_cursor_pos(3, 1);
set_bg_red(&mut t);
t.index().unwrap();
assert_eq!("2\n3\n\nX", dump(&t, pt_viewport(0, 0)));
assert_eq!("1\n2\n3\n\nX", dump(&t, pt_screen(0, 0)));
for x in 0..t.cols {
assert_bg_red(&t, pt_active(x, 2));
}
}
#[test]
fn terminal_cursor_up_basic() {
let mut t = init(5, 5);
t.set_cursor_pos(3, 1);
t.print('A' as u32).unwrap();
t.cursor_up(10);
t.print('X' as u32).unwrap();
assert_eq!(" X\n\nA", plain(&t));
}
#[test]
fn terminal_cursor_up_below_top_scroll_margin() {
let mut t = init(5, 5);
t.set_top_and_bottom_margin(2, 4);
t.set_cursor_pos(3, 1);
t.print('A' as u32).unwrap();
t.cursor_up(5);
t.print('X' as u32).unwrap();
assert_eq!("\n X\nA", plain(&t));
}
#[test]
fn terminal_cursor_up_above_top_scroll_margin() {
let mut t = init(5, 5);
t.set_top_and_bottom_margin(3, 5);
t.set_cursor_pos(3, 1);
t.print('A' as u32).unwrap();
t.set_cursor_pos(2, 1);
t.cursor_up(10);
t.print('X' as u32).unwrap();
assert_eq!("X\n\nA", plain(&t));
}
#[test]
fn terminal_cursor_up_resets_wrap() {
let mut t = init(5, 5);
chars(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.cursor_up(1);
assert!(!t.screen().cursor.pending_wrap);
t.print('X' as u32).unwrap();
assert_eq!("ABCDX", plain(&t));
}
#[test]
fn terminal_cursor_left_no_wrap() {
let mut t = init(10, 5);
t.print('A' as u32).unwrap();
t.carriage_return();
t.linefeed().unwrap();
t.print('B' as u32).unwrap();
t.cursor_left(10);
assert_eq!("A\nB", plain(&t));
}
#[test]
fn terminal_cursor_left_unsets_pending_wrap_state() {
let mut t = init(5, 5);
chars(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.cursor_left(1);
assert!(!t.screen().cursor.pending_wrap);
t.print('X' as u32).unwrap();
assert_eq!("ABCXE", plain(&t));
}
#[test]
fn terminal_cursor_left_unsets_pending_wrap_state_with_longer_jump() {
let mut t = init(5, 5);
chars(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.cursor_left(3);
assert!(!t.screen().cursor.pending_wrap);
t.print('X' as u32).unwrap();
assert_eq!("AXCDE", plain(&t));
}
#[test]
fn terminal_cursor_left_reverse_wrap_with_pending_wrap_state() {
let mut t = init(5, 5);
t.modes.set(Mode::Wraparound, true);
t.modes.set(Mode::ReverseWrap, true);
chars(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.cursor_left(1);
assert!(!t.screen().cursor.pending_wrap);
t.print('X' as u32).unwrap();
assert_eq!("ABCDX", plain(&t));
}
#[test]
fn terminal_cursor_left_reverse_wrap_extended_with_pending_wrap_state() {
let mut t = init(5, 5);
t.modes.set(Mode::Wraparound, true);
t.modes.set(Mode::ReverseWrapExtended, true);
chars(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.cursor_left(1);
assert!(!t.screen().cursor.pending_wrap);
t.print('X' as u32).unwrap();
assert_eq!("ABCDX", plain(&t));
}
#[test]
fn terminal_cursor_left_reverse_wrap() {
let mut t = init(5, 5);
t.modes.set(Mode::Wraparound, true);
t.modes.set(Mode::ReverseWrap, true);
chars(&mut t, "ABCDE1");
t.cursor_left(2);
t.print('X' as u32).unwrap();
assert!(t.screen().cursor.pending_wrap);
assert_eq!("ABCDX\n1", plain(&t));
}
#[test]
fn terminal_cursor_left_reverse_wrap_with_no_soft_wrap() {
let mut t = init(5, 5);
t.modes.set(Mode::Wraparound, true);
t.modes.set(Mode::ReverseWrap, true);
chars(&mut t, "ABCDE");
t.carriage_return();
t.linefeed().unwrap();
t.print('1' as u32).unwrap();
t.cursor_left(2);
t.print('X' as u32).unwrap();
assert_eq!("ABCDE\nX", plain(&t));
}
#[test]
fn terminal_cursor_left_reverse_wrap_before_left_margin() {
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.cursor_left(1);
t.print('X' as u32).unwrap();
assert_eq!("\n\nX", plain(&t));
}
@@ -0,0 +1,649 @@
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());
}
@@ -0,0 +1,709 @@
use super::*;
use crate::page::{ContentTag, Wide};
use ghostty_vt::charsets::Slots;
use ghostty_vt::color::Rgb;
use ghostty_vt::sgr::Attribute;
/// Zig `for ("...") |c| try t.print(c)`.
fn pr(t: &mut Terminal, s: &str) {
for c in s.chars() {
t.print(u32::from(c)).unwrap();
}
}
fn pt(t: &mut Terminal, c: u32) {
t.print(c).unwrap();
}
fn node_page(t: &Terminal) -> &crate::page::Page {
t.screen().cursor_page()
}
#[test]
fn terminal_insert_blanks_shift_off_screen() {
let mut t = init(5, 10);
pr(&mut t, " ABC");
t.set_cursor_pos(1, 3);
t.clear_dirty();
t.insert_blanks(2);
assert!(t.is_dirty(pt_active(0, 0)));
pt(&mut t, 'X' as u32);
assert_eq!(" X A", plain(&t));
}
#[test]
fn terminal_insert_blanks_split_multi_cell_character() {
let mut t = init(5, 10);
pr(&mut t, "123");
pt(&mut t, '橋' as u32);
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.insert_blanks(1);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!(" 123", plain(&t));
}
#[test]
fn terminal_insert_blanks_inside_left_right_scroll_region() {
let mut t = init(10, 10);
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
t.set_cursor_pos(1, 3);
pr(&mut t, "ABC");
t.set_cursor_pos(1, 3);
t.clear_dirty();
t.insert_blanks(2);
assert!(t.is_dirty(pt_active(0, 0)));
pt(&mut t, 'X' as u32);
assert_eq!(" X A", plain(&t));
}
#[test]
fn terminal_insert_blanks_outside_left_right_scroll_region() {
let mut t = init(6, 10);
t.set_cursor_pos(1, 4);
pr(&mut t, "ABC");
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
assert!(t.screen().cursor.pending_wrap);
t.clear_dirty();
t.insert_blanks(2);
assert!(!t.is_dirty(pt_active(0, 0)));
assert!(!t.screen().cursor.pending_wrap);
pt(&mut t, 'X' as u32);
assert_eq!(" ABX", plain(&t));
}
#[test]
fn terminal_insert_blanks_left_right_scroll_region_large_count() {
let mut t = init(10, 10);
t.modes.set(Mode::Origin, true);
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(3, 5);
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.insert_blanks(140);
assert!(t.is_dirty(pt_active(0, 0)));
pt(&mut t, 'X' as u32);
assert_eq!(" X", plain(&t));
}
#[test]
fn terminal_insert_blanks_deleting_graphemes() {
let mut t = init(5, 5);
t.modes.set(Mode::GraphemeCluster, true);
t.print_string("ABC").unwrap();
for cp in [0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F467] {
pt(&mut t, cp);
}
assert_eq!(1, node_page(&t).grapheme_count());
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.insert_blanks(4);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!(" A", plain(&t));
assert_eq!(0, node_page(&t).grapheme_count());
}
#[test]
fn terminal_insert_blanks_shift_graphemes() {
let mut t = init(5, 5);
t.modes.set(Mode::GraphemeCluster, true);
t.print_string("A").unwrap();
for cp in [0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F467] {
pt(&mut t, cp);
}
assert_eq!(1, node_page(&t).grapheme_count());
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.insert_blanks(1);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!(" A👨‍👩‍👧", plain(&t));
assert_eq!(1, node_page(&t).grapheme_count());
}
#[test]
fn terminal_insert_blanks_split_multi_cell_character_from_tail() {
let mut t = init(5, 10);
t.print_string("橋123").unwrap();
t.set_cursor_pos(1, 2);
t.insert_blanks(1);
assert_eq!(" 12", plain(&t));
}
#[test]
fn terminal_insert_blanks_shifts_hyperlinks() {
let mut t = init(10, 2);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
t.print_string("ABC").unwrap();
t.set_cursor_pos(1, 1);
t.insert_blanks(2);
assert_eq!(" ABC", plain(&t));
// Verify all our cells have a hyperlink
for x in 2..5u16 {
let p = pin_at(&t, pt_screen(x, 0));
let page = t.screen().page(p.node);
assert!(page.rows[usize::from(p.y)].hyperlink());
let i = page.cell_index(usize::from(p.x), usize::from(p.y)).unwrap();
assert!(page.cells[i].hyperlink());
assert_eq!(Some(1), page.lookup_hyperlink(i));
}
for x in 0..2u16 {
let p = pin_at(&t, pt_screen(x, 0));
let page = t.screen().page(p.node);
let i = page.cell_index(usize::from(p.x), usize::from(p.y)).unwrap();
assert!(!page.cells[i].hyperlink());
assert_eq!(None, page.lookup_hyperlink(i));
}
}
#[test]
fn terminal_insert_blanks_pushes_hyperlink_off_end_completely() {
let mut t = init(3, 2);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
t.print_string("ABC").unwrap();
t.set_cursor_pos(1, 1);
t.insert_blanks(3);
assert_eq!("", plain(&t));
for x in 0..3u16 {
let p = pin_at(&t, pt_screen(x, 0));
let page = t.screen().page(p.node);
assert!(!page.rows[usize::from(p.y)].hyperlink());
let i = page.cell_index(usize::from(p.x), usize::from(p.y)).unwrap();
assert!(!page.cells[i].hyperlink());
assert_eq!(None, page.lookup_hyperlink(i));
}
}
#[test]
fn terminal_insert_blanks_wide_char_straddling_right_margin() {
let mut t = init(10, 5);
t.set_cursor_pos(1, 1);
pr(&mut t, "ABCD");
pt(&mut t, '橋' as u32);
t.scrolling_region.right = 4;
t.set_cursor_pos(1, 3);
t.insert_blanks(1);
assert_eq!("AB CD", plain(&t));
}
#[test]
fn terminal_insert_blanks_wide_char_spacer_tail_orphaned_beyond_right_margin() {
let mut t = init(10, 5);
for _ in 0..5 {
pt(&mut t, 0x4E2D);
}
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(1, 9);
pt(&mut t, 'a' as u32);
t.insert_blanks(8);
assert_eq!("a", plain(&t));
}
#[test]
fn terminal_insert_mode_with_space() {
let mut t = init(10, 2);
pr(&mut t, "hello");
t.set_cursor_pos(1, 2);
t.modes.set(Mode::Insert, true);
pt(&mut t, 'X' as u32);
assert_eq!("hXello", plain(&t));
}
#[test]
fn terminal_insert_mode_doesnt_wrap_pushed_characters() {
let mut t = init(5, 2);
pr(&mut t, "hello");
t.set_cursor_pos(1, 2);
t.modes.set(Mode::Insert, true);
pt(&mut t, 'X' as u32);
assert_eq!("hXell", plain(&t));
}
#[test]
fn terminal_insert_mode_does_nothing_at_the_end_of_the_line() {
let mut t = init(5, 2);
pr(&mut t, "hello");
t.modes.set(Mode::Insert, true);
pt(&mut t, 'X' as u32);
assert_eq!("hello\nX", plain(&t));
}
#[test]
fn terminal_insert_mode_with_wide_characters() {
let mut t = init(5, 2);
pr(&mut t, "hello");
t.set_cursor_pos(1, 2);
t.modes.set(Mode::Insert, true);
pt(&mut t, 0x1F600);
assert_eq!("h😀el", plain(&t));
}
#[test]
fn terminal_insert_mode_with_wide_characters_at_end() {
let mut t = init(5, 2);
pr(&mut t, "well");
t.modes.set(Mode::Insert, true);
pt(&mut t, 0x1F600);
assert_eq!("well\n😀", plain(&t));
}
#[test]
fn terminal_insert_mode_pushing_off_wide_character() {
let mut t = init(5, 2);
pr(&mut t, "123");
pt(&mut t, 0x1F600);
t.modes.set(Mode::Insert, true);
t.set_cursor_pos(1, 1);
pt(&mut t, 'X' as u32);
assert_eq!("X123", plain(&t));
}
#[test]
fn terminal_delete_chars() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE");
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.delete_chars(2);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("ADE", plain(&t));
}
#[test]
fn terminal_delete_chars_zero_count() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE");
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.delete_chars(0);
assert!(!t.is_dirty(pt_active(0, 0)));
assert_eq!("ABCDE", plain(&t));
}
#[test]
fn terminal_delete_chars_more_than_half() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE");
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.delete_chars(3);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("AE", plain(&t));
}
#[test]
fn terminal_delete_chars_more_than_line_width() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE");
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.delete_chars(10);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("A", plain(&t));
}
#[test]
fn terminal_delete_chars_should_shift_left() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE");
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.delete_chars(1);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("ACDE", plain(&t));
}
#[test]
fn terminal_delete_chars_resets_pending_wrap() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.delete_chars(1);
assert!(!t.screen().cursor.pending_wrap);
pt(&mut t, 'X' as u32);
assert_eq!("ABCDX", plain(&t));
}
#[test]
fn terminal_delete_chars_resets_wrap() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE123");
assert!(row_at(&t, pt_active(0, 0)).wrap());
t.set_cursor_pos(1, 1);
t.delete_chars(1);
assert!(!row_at(&t, pt_active(0, 0)).wrap());
pt(&mut t, 'X' as u32);
assert_eq!("XCDE\n123", plain(&t));
}
#[test]
fn terminal_delete_chars_simple_operation() {
let mut t = init(10, 10);
t.print_string("ABC123").unwrap();
t.set_cursor_pos(1, 3);
t.clear_dirty();
t.delete_chars(2);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("AB23", plain(&t));
}
#[test]
fn terminal_delete_chars_preserves_background_sgr() {
let mut t = init(10, 10);
pr(&mut t, "ABC123");
t.set_cursor_pos(1, 3);
t.set_attribute(Attribute::DirectColorBg(Rgb {
r: 0xFF,
g: 0,
b: 0,
}))
.unwrap();
t.delete_chars(2);
assert_eq!("AB23", plain(&t));
for x in t.cols - 2..t.cols {
let c = cell_at(&t, pt_active(x, 0));
assert!(c.content_tag() == ContentTag::BgColorRgb);
assert_eq!(
Rgb {
r: 0xFF,
g: 0,
b: 0
},
c.color_rgb()
);
}
}
#[test]
fn terminal_delete_chars_outside_scroll_region() {
let mut t = init(6, 10);
t.print_string("ABC123").unwrap();
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
assert!(t.screen().cursor.pending_wrap);
t.clear_dirty();
t.delete_chars(2);
assert!(!t.is_dirty(pt_active(0, 0)));
assert!(t.screen().cursor.pending_wrap);
assert_eq!("ABC123", plain(&t));
}
#[test]
fn terminal_delete_chars_inside_scroll_region() {
let mut t = init(6, 10);
t.print_string("ABC123").unwrap();
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
t.set_cursor_pos(1, 4);
t.clear_dirty();
t.delete_chars(1);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("ABC2 3", plain(&t));
}
#[test]
fn terminal_delete_chars_split_wide_character_from_spacer_tail() {
let mut t = init(6, 10);
t.print_string("A橋123").unwrap();
t.set_cursor_pos(1, 3);
t.delete_chars(1);
assert_eq!("A 123", plain(&t));
}
#[test]
fn terminal_delete_chars_split_wide_character_from_wide() {
let mut t = init(6, 10);
t.print_string("橋123").unwrap();
t.set_cursor_pos(1, 1);
t.delete_chars(1);
let c = cell_at(&t, pt_screen(0, 0));
assert_eq!(0, c.codepoint());
assert_eq!(Wide::Narrow, c.wide());
let c = cell_at(&t, pt_screen(1, 0));
assert_eq!('1' as u32, c.codepoint());
assert_eq!(Wide::Narrow, c.wide());
}
#[test]
fn terminal_delete_chars_split_wide_character_from_end() {
let mut t = init(6, 10);
t.print_string("A橋123").unwrap();
t.set_cursor_pos(1, 1);
t.delete_chars(1);
let c = cell_at(&t, pt_screen(0, 0));
assert_eq!(0x6A4B, c.codepoint());
assert_eq!(Wide::Wide, c.wide());
let c = cell_at(&t, pt_screen(1, 0));
assert_eq!(0, c.codepoint());
assert_eq!(Wide::SpacerTail, c.wide());
}
#[test]
fn terminal_delete_chars_with_a_spacer_head_at_the_end() {
let mut t = init(5, 10);
t.print_string("0123橋123").unwrap();
{
let c = cell_at(&t, pt_screen(4, 0));
assert_eq!(Wide::SpacerHead, c.wide());
assert!(row_at(&t, pt_screen(4, 0)).wrap());
}
t.set_cursor_pos(1, 1);
t.delete_chars(1);
let c = cell_at(&t, pt_screen(3, 0));
assert_eq!(0, c.codepoint());
assert_eq!(Wide::Narrow, c.wide());
}
#[test]
fn terminal_delete_chars_split_wide_character_tail() {
let mut t = init(5, 5);
let cols = usize::from(t.cols);
t.set_cursor_pos(1, cols - 1);
pt(&mut t, 0x6A4B);
t.carriage_return();
t.delete_chars(cols - 1);
pt(&mut t, '0' as u32);
assert_eq!("0", plain(&t));
}
#[test]
fn terminal_delete_chars_wide_char_boundary_conditions() {
let mut t = init(8, 1);
t.print_string("😀a😀b😀").unwrap();
assert_eq!("😀a😀b😀", plain(&t));
t.set_cursor_pos(1, 2);
t.delete_chars(3);
node_page(&t).assert_integrity();
assert_eq!(" b😀", plain(&t));
}
#[test]
fn terminal_delete_chars_wide_char_wrap_boundary_conditions() {
let mut t = init(8, 3);
t.print_string(".......😀abcde😀......").unwrap();
assert_eq!(".......\n😀abcde\n😀......", plain(&t));
assert_eq!(".......😀abcde😀......", plain_unwrapped(&t));
t.set_cursor_pos(2, 2);
t.delete_chars(3);
node_page(&t).assert_integrity();
assert_eq!(".......\n cde\n😀......", plain(&t));
assert_eq!("....... cde\n😀......", plain_unwrapped(&t));
}
#[test]
fn terminal_delete_chars_wide_char_across_right_margin() {
let mut t = init(8, 3);
t.print_string("123456橋").unwrap();
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(2, 7);
assert_eq!("123456橋", plain(&t));
t.set_cursor_pos(1, 2);
t.delete_chars(1);
node_page(&t).assert_integrity();
assert_eq!("13456", plain(&t));
}
#[test]
fn terminal_save_cursor() {
let mut t = init(3, 3);
t.set_attribute(Attribute::Bold).unwrap();
t.screen_mut().charset.gr = Slots::G3;
t.modes.set(Mode::Origin, true);
t.save_cursor();
t.screen_mut().charset.gr = Slots::G0;
t.set_attribute(Attribute::Unset).unwrap();
t.modes.set(Mode::Origin, false);
t.restore_cursor();
assert!(t.screen().cursor.style.flags.bold);
assert!(t.screen().charset.gr == Slots::G3);
assert!(t.modes.get(Mode::Origin));
}
#[test]
fn terminal_save_cursor_position() {
let mut t = init(10, 5);
t.set_cursor_pos(1, 5);
pt(&mut t, 'A' as u32);
t.save_cursor();
t.set_cursor_pos(1, 1);
pt(&mut t, 'B' as u32);
t.restore_cursor();
pt(&mut t, 'X' as u32);
assert_eq!("B AX", plain(&t));
}
#[test]
fn terminal_save_cursor_pending_wrap_state() {
let mut t = init(5, 5);
t.set_cursor_pos(1, 5);
pt(&mut t, 'A' as u32);
t.save_cursor();
t.set_cursor_pos(1, 1);
pt(&mut t, 'B' as u32);
t.restore_cursor();
pt(&mut t, 'X' as u32);
assert_eq!("B A\nX", plain(&t));
}
#[test]
fn terminal_save_cursor_origin_mode() {
let mut t = init(10, 5);
t.modes.set(Mode::Origin, true);
t.save_cursor();
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(3, 5);
t.set_top_and_bottom_margin(2, 4);
t.restore_cursor();
pt(&mut t, 'X' as u32);
assert_eq!("X", plain(&t));
}
#[test]
fn terminal_save_cursor_resize() {
let mut t = init(10, 5);
t.set_cursor_pos(1, 10);
t.save_cursor();
t.resize(5, 5).unwrap();
t.restore_cursor();
pt(&mut t, 'X' as u32);
assert_eq!(" X", plain(&t));
}
#[test]
fn terminal_save_cursor_protected_pen() {
let mut t = init(10, 5);
t.set_protected_mode(ProtectedMode::Iso);
assert!(t.screen().cursor.protected);
t.set_cursor_pos(1, 10);
t.save_cursor();
t.set_protected_mode(ProtectedMode::Off);
assert!(!t.screen().cursor.protected);
t.restore_cursor();
assert!(t.screen().cursor.protected);
}
#[test]
fn terminal_save_cursor_doesnt_modify_hyperlink_state() {
let mut t = init(3, 3);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
let id = t.screen().cursor.hyperlink_id;
t.save_cursor();
assert_eq!(id, t.screen().cursor.hyperlink_id);
t.restore_cursor();
assert_eq!(id, t.screen().cursor.hyperlink_id);
}
#[test]
fn terminal_restore_cursor_uses_default_style_on_out_of_space() {
// Use a single row so the page can't be split
let mut t = init(10, 1);
// Set a style and save the cursor
t.set_attribute(Attribute::Bold).unwrap();
t.save_cursor();
// Clear the style
t.set_attribute(Attribute::Unset).unwrap();
assert!(!t.screen().cursor.style.flags.bold);
// Fill the style map to max capacity
let max_styles = size::CellCountInt::MAX;
while t.screen().cursor_page().capacity.styles < max_styles {
let node = t.screen().cursor_pin().node;
if t.screen_mut()
.increase_capacity(node, Some(IncreaseCapacity::Styles))
.is_err()
{
break;
}
}
assert_eq!(max_styles, t.screen().cursor_page().capacity.styles);
// Fill all style slots using the StyleSet's capacity.
{
let page = t.screen_mut().cursor_page_mut();
page.pause_integrity_checks(true);
// Zig `layout.cap` counts the reserved zero slot.
let max_items = page.styles.capacity() + 1;
let mut n: usize = 1;
while n < max_items {
let bg = Rgb {
r: ((n >> 16) & 0xFF) as u8,
g: ((n >> 8) & 0xFF) as u8,
b: (n & 0xFF) as u8,
};
let st = Style {
bg_color: StyleColor::Rgb(bg),
..Style::default()
};
if page.styles.add(st).is_err() {
break;
}
n += 1;
}
page.pause_integrity_checks(false);
page.assert_integrity();
}
// Restore cursor - should fall back to default style since page
// can't be split (1 row) and styles are at max capacity
t.restore_cursor();
// The style should be reset to default because OutOfSpace occurred
assert!(!t.screen().cursor.style.flags.bold);
assert_eq!(style::DEFAULT_ID, t.screen().cursor.style_id);
}
#[test]
fn terminal_set_protected_mode() {
let mut t = init(3, 3);
assert!(!t.screen().cursor.protected);
t.set_protected_mode(ProtectedMode::Off);
assert!(!t.screen().cursor.protected);
t.set_protected_mode(ProtectedMode::Iso);
assert!(t.screen().cursor.protected);
t.set_protected_mode(ProtectedMode::Dec);
assert!(t.screen().cursor.protected);
t.set_protected_mode(ProtectedMode::Off);
assert!(!t.screen().cursor.protected);
}
#[test]
fn terminal_erase_line_simple_erase_right() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE");
t.set_cursor_pos(1, 3);
t.clear_dirty();
t.erase_line(csi::EraseLine::Right, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("AB", plain(&t));
}
@@ -0,0 +1,672 @@
use super::*;
use crate::page::{ContentTag, SemanticContent, SemanticPrompt as RowPrompt};
use ghostty_vt::{
ansi::ProtectedMode,
color::Rgb,
csi::{EraseDisplay, EraseLine, TabClear},
osc::semantic_prompt::{Action as PA, Command},
sgr::{Attribute as A, Underline},
};
fn pr(t: &mut Terminal, s: &str) {
for c in s.chars() {
t.print(u32::from(c)).unwrap();
}
}
fn red() -> Rgb {
Rgb {
r: 0xFF,
g: 0,
b: 0,
}
}
fn assert_bg_red(t: &Terminal, xs: std::ops::Range<u16>, y: u32) {
for x in xs {
let c = cell_at(t, pt_active(x, y));
assert_eq!(c.content_tag(), ContentTag::BgColorRgb);
assert_eq!(c.color_rgb(), red());
}
}
fn abc_def_ghi(t: &mut Terminal) {
pr(t, "ABC");
t.carriage_return();
t.linefeed().unwrap();
pr(t, "DEF");
t.carriage_return();
t.linefeed().unwrap();
pr(t, "GHI");
}
#[test]
fn terminal_erase_line_resets_pending_wrap() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.erase_line(EraseLine::Right, false);
assert!(!t.screen().cursor.pending_wrap);
t.print(u32::from('B')).unwrap();
assert_eq!("ABCDB", plain(&t));
}
#[test]
fn terminal_erase_line_resets_wrap() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE123");
assert!(row_at(&t, pt_active(0, 0)).wrap());
t.set_cursor_pos(1, 1);
t.erase_line(EraseLine::Right, false);
assert!(!row_at(&t, pt_active(0, 0)).wrap());
t.print(u32::from('X')).unwrap();
assert_eq!("X\n123", plain(&t));
}
#[test]
fn terminal_erase_line_right_preserves_background_sgr() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE");
t.set_cursor_pos(1, 2);
t.set_attribute(A::DirectColorBg(red())).unwrap();
t.erase_line(EraseLine::Right, false);
assert_eq!("A", plain(&t));
assert_bg_red(&t, 1..5, 0);
}
#[test]
fn terminal_erase_line_right_wide_character() {
let mut t = init(10, 5);
pr(&mut t, "AB橋DE");
t.set_cursor_pos(1, 4);
t.clear_dirty();
t.erase_line(EraseLine::Right, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("AB", plain(&t));
}
#[test]
fn terminal_erase_line_right_protected_attributes_respected_with_iso() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Iso);
pr(&mut t, "ABC");
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.erase_line(EraseLine::Right, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("ABC", plain(&t));
}
#[test]
fn terminal_erase_line_right_protected_attributes_ignored_with_dec_most_recent() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Iso);
pr(&mut t, "ABC");
t.set_protected_mode(ProtectedMode::Dec);
t.set_protected_mode(ProtectedMode::Off);
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.erase_line(EraseLine::Right, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("A", plain(&t));
}
#[test]
fn terminal_erase_line_right_protected_attributes_ignored_with_dec_set() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Dec);
pr(&mut t, "ABC");
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.erase_line(EraseLine::Right, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("A", plain(&t));
}
#[test]
fn terminal_erase_line_right_protected_requested() {
let mut t = init(10, 5);
pr(&mut t, "12345678");
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y + 1, 6);
t.set_protected_mode(ProtectedMode::Dec);
t.print(u32::from('X')).unwrap();
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y + 1, 4);
t.clear_dirty();
t.erase_line(EraseLine::Right, true);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("123 X", plain(&t));
}
#[test]
fn terminal_erase_line_simple_erase_left() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE");
t.set_cursor_pos(1, 3);
t.clear_dirty();
t.erase_line(EraseLine::Left, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!(" DE", plain(&t));
}
#[test]
fn terminal_erase_line_left_resets_wrap() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.clear_dirty();
t.erase_line(EraseLine::Left, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert!(!t.screen().cursor.pending_wrap);
t.print(u32::from('B')).unwrap();
assert_eq!(" B", plain(&t));
}
#[test]
fn terminal_erase_line_left_preserves_background_sgr() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE");
t.set_cursor_pos(1, 2);
t.set_attribute(A::DirectColorBg(red())).unwrap();
t.erase_line(EraseLine::Left, false);
assert_eq!(" CDE", plain(&t));
assert_bg_red(&t, 0..2, 0);
}
#[test]
fn terminal_erase_line_left_wide_character() {
let mut t = init(10, 5);
pr(&mut t, "AB橋DE");
t.set_cursor_pos(1, 3);
t.clear_dirty();
t.erase_line(EraseLine::Left, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!(" DE", plain(&t));
}
#[test]
fn terminal_erase_line_left_protected_attributes_respected_with_iso() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Iso);
pr(&mut t, "ABC");
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.erase_line(EraseLine::Left, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("ABC", plain(&t));
}
#[test]
fn terminal_erase_line_left_protected_attributes_ignored_with_dec_most_recent() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Iso);
pr(&mut t, "ABC");
t.set_protected_mode(ProtectedMode::Dec);
t.set_protected_mode(ProtectedMode::Off);
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.erase_line(EraseLine::Left, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!(" C", plain(&t));
}
#[test]
fn terminal_erase_line_left_protected_attributes_ignored_with_dec_set() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Dec);
pr(&mut t, "ABC");
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.erase_line(EraseLine::Left, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!(" C", plain(&t));
}
#[test]
fn terminal_erase_line_left_protected_requested() {
let mut t = init(10, 5);
pr(&mut t, "123456789");
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y + 1, 6);
t.set_protected_mode(ProtectedMode::Dec);
t.print(u32::from('X')).unwrap();
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y + 1, 8);
t.clear_dirty();
t.erase_line(EraseLine::Left, true);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!(" X 9", plain(&t));
}
#[test]
fn terminal_erase_line_complete_preserves_background_sgr() {
let mut t = init(5, 5);
pr(&mut t, "ABCDE");
t.set_cursor_pos(1, 2);
t.set_attribute(A::DirectColorBg(red())).unwrap();
t.erase_line(EraseLine::Complete, false);
assert_eq!("", plain(&t));
assert_bg_red(&t, 0..5, 0);
}
#[test]
fn terminal_erase_line_complete_protected_attributes_respected_with_iso() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Iso);
pr(&mut t, "ABC");
t.set_cursor_pos(1, 1);
t.clear_dirty();
t.erase_line(EraseLine::Complete, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("ABC", plain(&t));
}
#[test]
fn terminal_erase_line_complete_protected_attributes_ignored_with_dec_most_recent() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Iso);
pr(&mut t, "ABC");
t.set_protected_mode(ProtectedMode::Dec);
t.set_protected_mode(ProtectedMode::Off);
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.erase_line(EraseLine::Complete, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("", plain(&t));
}
#[test]
fn terminal_erase_line_complete_protected_attributes_ignored_with_dec_set() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Dec);
pr(&mut t, "ABC");
t.set_cursor_pos(1, 2);
t.clear_dirty();
t.erase_line(EraseLine::Complete, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("", plain(&t));
}
#[test]
fn terminal_erase_line_complete_protected_requested() {
let mut t = init(10, 5);
pr(&mut t, "123456789");
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y + 1, 6);
t.set_protected_mode(ProtectedMode::Dec);
t.print(u32::from('X')).unwrap();
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y + 1, 8);
t.clear_dirty();
t.erase_line(EraseLine::Complete, true);
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!(" X", plain(&t));
}
#[test]
fn terminal_tab_clear_single() {
let mut t = init(30, 5);
t.horizontal_tab();
t.tab_clear(TabClear::Current);
assert!(!t.is_dirty(pt_active(0, 0)));
t.set_cursor_pos(1, 1);
t.horizontal_tab();
assert_eq!(16, t.screen().cursor.x);
}
#[test]
fn terminal_tab_clear_all() {
let mut t = init(30, 5);
t.tab_clear(TabClear::All);
assert!(!t.is_dirty(pt_active(0, 0)));
t.set_cursor_pos(1, 1);
t.horizontal_tab();
assert_eq!(29, t.screen().cursor.x);
}
#[test]
fn terminal_print_repeat_simple() {
let mut t = init(5, 5);
t.print_string("A").unwrap();
t.print_repeat(1).unwrap();
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!("AA", plain(&t));
}
#[test]
fn terminal_print_repeat_wrap() {
let mut t = init(5, 5);
t.print_string(" A").unwrap();
t.print_repeat(1).unwrap();
assert!(t.is_dirty(pt_active(0, 0)));
assert_eq!(" A\nA", plain(&t));
}
#[test]
fn terminal_print_repeat_no_previous_character() {
let mut t = init(5, 5);
t.print_repeat(1).unwrap();
assert!(!t.is_dirty(pt_active(0, 0)));
assert_eq!("", plain(&t));
}
#[test]
fn terminal_print_attributes() {
let mut t = init(5, 5);
let rgb = |r, g, b| Rgb { r, g, b };
{
t.set_attribute(A::DirectColorFg(rgb(1, 2, 3))).unwrap();
assert_eq!("0;38:2::1:2:3", t.print_attributes());
t.set_attribute(A::Unset).unwrap();
}
{
t.set_attribute(A::Bold).unwrap();
t.set_attribute(A::DirectColorBg(rgb(1, 2, 3))).unwrap();
assert_eq!("0;1;48:2::1:2:3", t.print_attributes());
t.set_attribute(A::Unset).unwrap();
}
{
t.set_attribute(A::Bold).unwrap();
t.set_attribute(A::Faint).unwrap();
t.set_attribute(A::Italic).unwrap();
t.set_attribute(A::Underline(Underline::Single)).unwrap();
t.set_attribute(A::Blink).unwrap();
t.set_attribute(A::Inverse).unwrap();
t.set_attribute(A::Invisible).unwrap();
t.set_attribute(A::Strikethrough).unwrap();
t.set_attribute(A::DirectColorFg(rgb(100, 200, 255)))
.unwrap();
t.set_attribute(A::DirectColorBg(rgb(101, 102, 103)))
.unwrap();
assert_eq!(
"0;1;2;3;4;5;7;8;9;38:2::100:200:255;48:2::101:102:103",
t.print_attributes()
);
t.set_attribute(A::Unset).unwrap();
}
{
t.set_attribute(A::Underline(Underline::Single)).unwrap();
assert_eq!("0;4", t.print_attributes());
t.set_attribute(A::Unset).unwrap();
}
assert_eq!("0", t.print_attributes());
}
#[test]
fn terminal_erase_display_simple_erase_below() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
t.clear_dirty();
t.erase_display(EraseDisplay::Below, false);
assert!(!t.is_dirty(pt_active(0, 0)));
assert!(t.is_dirty(pt_active(0, 1)));
assert!(t.is_dirty(pt_active(0, 2)));
assert_eq!("ABC\nD", plain(&t));
}
#[test]
fn terminal_erase_display_erase_below_preserves_sgr_bg() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
t.set_attribute(A::DirectColorBg(red())).unwrap();
t.erase_display(EraseDisplay::Below, false);
assert_eq!("ABC\nD", plain(&t));
assert_bg_red(&t, 1..5, 1);
}
#[test]
fn terminal_erase_display_below_split_multi_cell() {
let mut t = init(5, 5);
t.print_string("AB橋C").unwrap();
t.carriage_return();
t.linefeed().unwrap();
t.print_string("DE橋F").unwrap();
t.carriage_return();
t.linefeed().unwrap();
t.print_string("GH橋I").unwrap();
t.set_cursor_pos(2, 4);
t.erase_display(EraseDisplay::Below, false);
assert_eq!("AB橋C\nDE", plain(&t));
}
#[test]
fn terminal_erase_display_below_protected_attributes_respected_with_iso() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Iso);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
t.erase_display(EraseDisplay::Below, false);
assert_eq!("ABC\nDEF\nGHI", plain(&t));
}
#[test]
fn terminal_erase_display_below_protected_attributes_ignored_with_dec_most_recent() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Iso);
abc_def_ghi(&mut t);
t.set_protected_mode(ProtectedMode::Dec);
t.set_protected_mode(ProtectedMode::Off);
t.set_cursor_pos(2, 2);
t.erase_display(EraseDisplay::Below, false);
assert_eq!("ABC\nD", plain(&t));
}
#[test]
fn terminal_erase_display_below_protected_attributes_ignored_with_dec_set() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Dec);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
t.erase_display(EraseDisplay::Below, false);
assert_eq!("ABC\nD", plain(&t));
}
#[test]
fn terminal_erase_display_below_protected_attributes_respected_with_force() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Dec);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
t.erase_display(EraseDisplay::Below, true);
assert_eq!("ABC\nDEF\nGHI", plain(&t));
}
#[test]
fn terminal_erase_display_simple_erase_above() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
t.clear_dirty();
t.erase_display(EraseDisplay::Above, false);
assert!(t.is_dirty(pt_active(0, 0)));
assert!(t.is_dirty(pt_active(0, 1)));
assert!(!t.is_dirty(pt_active(0, 2)));
assert_eq!("\n F\nGHI", plain(&t));
}
#[test]
fn terminal_erase_display_erase_above_preserves_sgr_bg() {
let mut t = init(5, 5);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
t.set_attribute(A::DirectColorBg(red())).unwrap();
t.erase_display(EraseDisplay::Above, false);
assert_eq!("\n F\nGHI", plain(&t));
assert_bg_red(&t, 0..2, 1);
}
#[test]
fn terminal_erase_display_above_split_multi_cell() {
let mut t = init(5, 5);
t.print_string("AB橋C").unwrap();
t.carriage_return();
t.linefeed().unwrap();
t.print_string("DE橋F").unwrap();
t.carriage_return();
t.linefeed().unwrap();
t.print_string("GH橋I").unwrap();
t.set_cursor_pos(2, 3);
t.erase_display(EraseDisplay::Above, false);
assert_eq!("\n F\nGH橋I", plain(&t));
}
#[test]
fn terminal_erase_display_above_protected_attributes_respected_with_iso() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Iso);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
t.erase_display(EraseDisplay::Above, false);
assert_eq!("ABC\nDEF\nGHI", plain(&t));
}
#[test]
fn terminal_erase_display_above_protected_attributes_ignored_with_dec_most_recent() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Iso);
abc_def_ghi(&mut t);
t.set_protected_mode(ProtectedMode::Dec);
t.set_protected_mode(ProtectedMode::Off);
t.set_cursor_pos(2, 2);
t.erase_display(EraseDisplay::Above, false);
assert_eq!("\n F\nGHI", plain(&t));
}
#[test]
fn terminal_erase_display_above_protected_attributes_ignored_with_dec_set() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Dec);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
t.erase_display(EraseDisplay::Above, false);
assert_eq!("\n F\nGHI", plain(&t));
}
#[test]
fn terminal_erase_display_above_protected_attributes_respected_with_force() {
let mut t = init(5, 5);
t.set_protected_mode(ProtectedMode::Dec);
abc_def_ghi(&mut t);
t.set_cursor_pos(2, 2);
t.erase_display(EraseDisplay::Above, true);
assert_eq!("ABC\nDEF\nGHI", plain(&t));
}
#[test]
fn terminal_erase_display_protected_complete() {
let mut t = init(10, 5);
t.print(u32::from('A')).unwrap();
t.carriage_return();
t.linefeed().unwrap();
pr(&mut t, "123456789");
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y + 1, 6);
t.set_protected_mode(ProtectedMode::Dec);
t.print(u32::from('X')).unwrap();
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y + 1, 4);
t.clear_dirty();
t.erase_display(EraseDisplay::Complete, true);
for y in 0..u32::from(t.rows) {
assert!(t.is_dirty(pt_active(0, y)));
}
assert_eq!("\n X", plain(&t));
}
#[test]
fn terminal_erase_display_protected_below() {
let mut t = init(10, 5);
t.print(u32::from('A')).unwrap();
t.carriage_return();
t.linefeed().unwrap();
pr(&mut t, "123456789");
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y + 1, 6);
t.set_protected_mode(ProtectedMode::Dec);
t.print(u32::from('X')).unwrap();
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y + 1, 4);
t.erase_display(EraseDisplay::Below, true);
assert_eq!("A\n123 X", plain(&t));
}
#[test]
fn terminal_erase_display_scroll_complete() {
let mut t = init(10, 5);
t.print(u32::from('A')).unwrap();
t.carriage_return();
t.linefeed().unwrap();
t.erase_display(EraseDisplay::ScrollComplete, false);
assert_eq!("", plain(&t));
}
#[test]
fn terminal_erase_display_protected_above() {
let mut t = init(10, 3);
t.print(u32::from('A')).unwrap();
t.carriage_return();
t.linefeed().unwrap();
pr(&mut t, "123456789");
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y + 1, 6);
t.set_protected_mode(ProtectedMode::Dec);
t.print(u32::from('X')).unwrap();
let y = usize::from(t.screen().cursor.y);
t.set_cursor_pos(y + 1, 8);
t.erase_display(EraseDisplay::Above, true);
assert_eq!("\n X 9", plain(&t));
}
#[test]
fn terminal_erase_display_complete_preserves_cursor() {
let mut t = init(5, 5);
// Set our cursor
t.set_attribute(A::Bold).unwrap();
t.print_string("AAAA").unwrap();
assert_ne!(style::DEFAULT_ID, t.screen().cursor.style_id);
// Erasing the display may detect that our style is no longer in use
// and prune our style, which we don't want because its still our
// active cursor.
t.erase_display(EraseDisplay::Complete, false);
assert_ne!(style::DEFAULT_ID, t.screen().cursor.style_id);
}
#[test]
fn terminal_semantic_prompt() {
let mut t = init(10, 5);
// Prompt
t.semantic_prompt(&Command::init(PA::FreshLineNewPrompt))
.unwrap();
pr(&mut t, "hello");
assert_eq!(0, t.screen().cursor.y);
assert_eq!(5, t.screen().cursor.x);
{
let pt = pt_active(t.screen().cursor.x - 1, u32::from(t.screen().cursor.y));
assert_eq!(SemanticContent::Prompt, cell_at(&t, pt).semantic_content());
assert_eq!(RowPrompt::Prompt, row_at(&t, pt).semantic_prompt());
}
// Start input but end it on EOL
t.semantic_prompt(&Command::init(PA::EndPromptStartInputTerminateEol))
.unwrap();
t.carriage_return();
t.linefeed().unwrap();
// Write some output
assert_eq!(1, t.screen().cursor.y);
assert_eq!(0, t.screen().cursor.x);
pr(&mut t, "world");
{
let pt = pt_active(t.screen().cursor.x - 1, u32::from(t.screen().cursor.y));
assert_eq!(SemanticContent::Output, cell_at(&t, pt).semantic_content());
assert_eq!(RowPrompt::None, row_at(&t, pt).semantic_prompt());
}
}
@@ -0,0 +1,746 @@
use super::*;
use crate::page::{ContentTag, SemanticContent, SemanticPrompt as RowPrompt};
use crate::screen::SemanticClick;
use ghostty_vt::color::Rgb;
use ghostty_vt::osc::{
SemanticPrompt as Cmd,
semantic_prompt::{Action as A, Click},
};
use ghostty_vt::sgr::Attribute;
fn sp(a: A) -> Cmd {
Cmd::init(a)
}
fn sp_opts(a: A, o: &str) -> Cmd {
Cmd {
action: a,
options_unvalidated: o.as_bytes().to_vec(),
}
}
fn print_chars(t: &mut Terminal, s: &str) {
for c in s.chars() {
t.print(u32::from(c)).unwrap();
}
}
fn row_prompt(t: &Terminal, x: u16, y: u32) -> RowPrompt {
row_at(t, pt_active(x, y)).semantic_prompt()
}
fn at_cursor(t: &Terminal) -> crate::page::Cell {
let c = &t.screen().cursor;
cell_at(t, pt_active(c.x, u32::from(c.y)))
}
#[test]
fn terminal_semantic_prompt_continuations() {
let mut t = init(10, 5);
// Prompt
t.semantic_prompt(&sp(A::FreshLineNewPrompt)).unwrap();
print_chars(&mut t, "hello");
assert_eq!(0, t.screen().cursor.y);
assert_eq!(5, t.screen().cursor.x);
{
let cell = cell_at(&t, pt_active(t.screen().cursor.x - 1, 0));
assert_eq!(SemanticContent::Prompt, cell.semantic_content());
assert_eq!(RowPrompt::Prompt, row_prompt(&t, 0, 0));
}
// Start input but end it on EOL
t.carriage_return();
t.linefeed().unwrap();
t.semantic_prompt(&sp_opts(A::PromptStart, "k=c")).unwrap();
// Write some output
assert_eq!(1, t.screen().cursor.y);
assert_eq!(0, t.screen().cursor.x);
print_chars(&mut t, "world");
{
let cell = cell_at(&t, pt_active(t.screen().cursor.x - 1, 1));
assert_eq!(SemanticContent::Prompt, cell.semantic_content());
assert_eq!(RowPrompt::PromptContinuation, row_prompt(&t, 0, 1));
}
}
#[test]
fn terminal_index_in_prompt_mode_marks_new_row_as_prompt_continuation() {
let mut t = init(10, 5);
t.semantic_prompt(&sp(A::PromptStart)).unwrap();
print_chars(&mut t, "hello");
assert_eq!(RowPrompt::Prompt, row_prompt(&t, 0, 0));
t.carriage_return();
t.linefeed().unwrap();
assert_eq!(RowPrompt::PromptContinuation, row_prompt(&t, 0, 1));
assert_eq!(SemanticContent::Prompt, t.screen().cursor.semantic_content);
}
#[test]
fn terminal_index_in_input_mode_does_not_mark_new_row_as_prompt() {
let mut t = init(10, 5);
t.semantic_prompt(&sp(A::PromptStart)).unwrap();
print_chars(&mut t, "$ ");
t.semantic_prompt(&sp(A::EndPromptStartInput)).unwrap();
print_chars(&mut t, "echo \\");
t.carriage_return();
t.linefeed().unwrap();
assert_eq!(RowPrompt::PromptContinuation, row_prompt(&t, 0, 1));
assert_eq!(SemanticContent::Input, t.screen().cursor.semantic_content);
}
#[test]
fn terminal_index_in_output_mode_does_not_mark_new_row_as_prompt() {
let mut t = init(10, 5);
t.semantic_prompt(&sp(A::PromptStart)).unwrap();
print_chars(&mut t, "$ ");
t.semantic_prompt(&sp(A::EndPromptStartInput)).unwrap();
print_chars(&mut t, "ls");
t.semantic_prompt(&sp(A::EndInputStartOutput)).unwrap();
t.carriage_return();
t.linefeed().unwrap();
assert_eq!(RowPrompt::None, row_prompt(&t, 0, 1));
}
#[test]
fn terminal_osc133c_at_x0_on_prompt_row_clears_prompt_mark() {
let mut t = init(10, 5);
t.semantic_prompt(&sp(A::PromptStart)).unwrap();
print_chars(&mut t, "$ echo \\");
t.carriage_return();
t.linefeed().unwrap();
assert_eq!(RowPrompt::PromptContinuation, row_prompt(&t, 0, 1));
assert_eq!(0, t.screen().cursor.x);
t.semantic_prompt(&sp(A::EndInputStartOutput)).unwrap();
assert_eq!(RowPrompt::None, row_prompt(&t, 0, 1));
}
#[test]
fn terminal_osc133c_at_x_gt0_on_prompt_row_does_not_clear_prompt_mark() {
let mut t = init(10, 5);
t.semantic_prompt(&sp(A::PromptStart)).unwrap();
print_chars(&mut t, "$ ");
t.carriage_return();
t.linefeed().unwrap();
t.semantic_prompt(&sp_opts(A::PromptStart, "k=c")).unwrap();
print_chars(&mut t, "> ");
assert_eq!(RowPrompt::PromptContinuation, row_prompt(&t, 0, 1));
assert!(t.screen().cursor.x > 0);
t.semantic_prompt(&sp(A::EndInputStartOutput)).unwrap();
assert_eq!(RowPrompt::PromptContinuation, row_prompt(&t, 0, 1));
}
#[test]
fn terminal_multiple_newlines_in_prompt_mode_marks_all_rows() {
let mut t = init(10, 5);
t.semantic_prompt(&sp(A::PromptStart)).unwrap();
print_chars(&mut t, "line1");
t.carriage_return();
t.linefeed().unwrap();
print_chars(&mut t, "line2");
t.carriage_return();
t.linefeed().unwrap();
print_chars(&mut t, "line3");
assert_eq!(RowPrompt::Prompt, row_prompt(&t, 0, 0));
assert_eq!(RowPrompt::PromptContinuation, row_prompt(&t, 0, 1));
assert_eq!(RowPrompt::PromptContinuation, row_prompt(&t, 0, 2));
}
#[test]
fn terminal_osc133a_click_events_1_sets_click_to_click_events() {
let mut t = init(10, 5);
assert_eq!(SemanticClick::None, t.screen().semantic_prompt.click);
t.semantic_prompt(&sp_opts(A::FreshLineNewPrompt, "click_events=1"))
.unwrap();
assert_eq!(SemanticClick::ClickEvents, t.screen().semantic_prompt.click);
}
#[test]
fn terminal_osc133a_click_events_0_does_not_set_click_events() {
let mut t = init(10, 5);
t.semantic_prompt(&sp_opts(A::FreshLineNewPrompt, "click_events=0"))
.unwrap();
assert_eq!(SemanticClick::None, t.screen().semantic_prompt.click);
}
#[test]
fn terminal_osc133a_cl_option_sets_click_to_cl_value() {
let mut t = init(10, 5);
t.semantic_prompt(&sp_opts(A::FreshLineNewPrompt, "cl=m"))
.unwrap();
assert_eq!(
SemanticClick::Cl(Click::Multiple),
t.screen().semantic_prompt.click
);
}
#[test]
fn terminal_osc133a_cl_line_sets_click_to_line() {
let mut t = init(10, 5);
t.semantic_prompt(&sp_opts(A::FreshLineNewPrompt, "cl=line"))
.unwrap();
assert_eq!(
SemanticClick::Cl(Click::Line),
t.screen().semantic_prompt.click
);
}
#[test]
fn terminal_osc133a_click_events_1_takes_priority_over_cl() {
let mut t = init(10, 5);
t.semantic_prompt(&sp_opts(A::FreshLineNewPrompt, "click_events=1;cl=m"))
.unwrap();
assert_eq!(SemanticClick::ClickEvents, t.screen().semantic_prompt.click);
}
#[test]
fn terminal_osc133a_click_events_0_falls_back_to_cl() {
let mut t = init(10, 5);
t.semantic_prompt(&sp_opts(A::FreshLineNewPrompt, "click_events=0;cl=v"))
.unwrap();
assert_eq!(
SemanticClick::Cl(Click::ConservativeVertical),
t.screen().semantic_prompt.click
);
}
#[test]
fn terminal_osc133a_no_click_options_leaves_click_as_none() {
let mut t = init(10, 5);
t.semantic_prompt(&sp_opts(A::FreshLineNewPrompt, "aid=123"))
.unwrap();
assert_eq!(SemanticClick::None, t.screen().semantic_prompt.click);
}
#[test]
fn terminal_cursor_is_at_prompt() {
let mut t = init(10, 3);
assert!(!t.cursor_is_at_prompt());
t.semantic_prompt(&sp(A::PromptStart)).unwrap();
assert!(t.cursor_is_at_prompt());
print_chars(&mut t, "$ ");
// Input is also a prompt
t.semantic_prompt(&sp(A::EndPromptStartInput)).unwrap();
assert!(t.cursor_is_at_prompt());
print_chars(&mut t, "ls");
// (cursor is not at x=0, so the Fish heuristic doesn't trigger)
t.semantic_prompt(&sp(A::EndInputStartOutput)).unwrap();
// Still a prompt because this line has a prompt
assert!(t.cursor_is_at_prompt());
t.linefeed().unwrap();
assert!(!t.cursor_is_at_prompt());
// Until we know we're at a prompt again
t.linefeed().unwrap();
t.semantic_prompt(&sp(A::PromptStart)).unwrap();
assert!(t.cursor_is_at_prompt());
}
#[test]
fn terminal_cursor_is_at_prompt_alternate_screen() {
let mut t = init(3, 2);
assert!(!t.cursor_is_at_prompt());
t.semantic_prompt(&sp(A::PromptStart)).unwrap();
assert!(t.cursor_is_at_prompt());
// Secondary screen is never a prompt
t.switch_screen_mode(SwitchScreenMode::M1049, true).unwrap();
assert!(!t.cursor_is_at_prompt());
t.semantic_prompt(&sp(A::PromptStart)).unwrap();
assert!(!t.cursor_is_at_prompt());
}
#[test]
fn terminal_full_reset_with_a_non_empty_pen() {
let mut t = init(80, 80);
t.set_attribute(Attribute::DirectColorFg(Rgb {
r: 0xFF,
g: 0,
b: 0x7F,
}))
.unwrap();
t.set_attribute(Attribute::DirectColorBg(Rgb {
r: 0xFF,
g: 0,
b: 0x7F,
}))
.unwrap();
t.screen_mut().cursor.semantic_content = SemanticContent::Input;
t.full_reset();
assert_eq!(0, at_cursor(&t).style_id());
assert_eq!(0, t.screen().cursor.style_id);
assert_eq!(SemanticContent::Output, t.screen().cursor.semantic_content);
}
#[test]
fn terminal_full_reset_hyperlink() {
let mut t = init(80, 80);
t.screen_mut()
.start_hyperlink(b"http://example.com", None)
.unwrap();
t.full_reset();
assert_eq!(0, t.screen().cursor.hyperlink_id);
}
#[test]
fn terminal_full_reset_with_a_non_empty_saved_cursor() {
let mut t = init(80, 80);
t.set_attribute(Attribute::DirectColorFg(Rgb {
r: 0xFF,
g: 0,
b: 0x7F,
}))
.unwrap();
t.set_attribute(Attribute::DirectColorBg(Rgb {
r: 0xFF,
g: 0,
b: 0x7F,
}))
.unwrap();
t.save_cursor();
t.full_reset();
assert_eq!(0, at_cursor(&t).style_id());
assert_eq!(0, t.screen().cursor.style_id);
}
#[test]
fn terminal_full_reset_origin_mode() {
let mut t = init(10, 10);
t.set_cursor_pos(3, 5);
t.modes.set(Mode::Origin, true);
t.full_reset();
assert_eq!(0, t.screen().cursor.y);
assert_eq!(0, t.screen().cursor.x);
assert!(!t.modes.get(Mode::Origin));
}
#[test]
fn terminal_full_reset_status_display() {
let mut t = init(10, 10);
t.status_display = StatusDisplay::StatusLine;
t.full_reset();
assert!(t.status_display == StatusDisplay::Main);
}
// https://github.com/mitchellh/ghostty/issues/1607
#[test]
fn terminal_full_reset_clears_alt_screen_kitty_keyboard_state() {
let mut t = init(10, 10);
t.switch_screen_mode(SwitchScreenMode::M1049, true).unwrap();
t.screen_mut()
.kitty_keyboard
.push(ghostty_vt::kitty::key::Flags {
disambiguate: true,
report_events: false,
report_alternates: true,
report_all: true,
report_associated: true,
});
t.switch_screen_mode(SwitchScreenMode::M1049, false)
.unwrap();
t.full_reset();
assert!(t.screens.get(ScreenKey::Alternate).is_none());
}
#[test]
fn terminal_full_reset_default_modes() {
let mut o = Options::new(10, 10);
let mut dm = ModePacked::default();
dm.set(Mode::GraphemeCluster, true);
o.default_modes = dm;
let mut t = Terminal::new(o).unwrap();
assert!(t.modes.get(Mode::GraphemeCluster));
t.full_reset();
assert!(t.modes.get(Mode::GraphemeCluster));
}
#[test]
fn terminal_full_reset_tracked_pins() {
let mut t = init(80, 80);
// Create a tracked pin
let pin = t.screen().cursor_pin();
let h = t.screen_mut().pages.track_pin(pin).unwrap();
t.full_reset();
let p = t.screen().pages.tracked_pin(h).unwrap();
assert!(t.screen().pages.pin_is_valid(p));
}
// https://github.com/mitchellh/ghostty/issues/272
#[test]
fn terminal_resize_less_cols_with_wide_char_then_print() {
let mut t = init(3, 3);
t.print(u32::from('x')).unwrap();
t.print(0x1F600).unwrap();
t.resize(2, 3).unwrap();
t.set_cursor_pos(1, 2);
t.print(0x1F600).unwrap();
}
// https://github.com/mitchellh/ghostty/issues/723
#[test]
fn terminal_resize_with_left_and_right_margin_set() {
let cols = 70;
let rows = 23;
let mut t = init(cols, rows);
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.print(u32::from('0')).unwrap();
t.modes.set(Mode::EnableMode3, true);
t.resize(cols, rows).unwrap();
t.set_left_and_right_margin(2, 0);
t.print_repeat(1850).unwrap();
let _ = t.modes.restore(Mode::EnableMode3);
t.resize(cols, rows).unwrap();
}
// https://github.com/mitchellh/ghostty/issues/1343
#[test]
fn terminal_resize_with_wraparound_off() {
let mut t = init(4, 2);
t.modes.set(Mode::Wraparound, false);
print_chars(&mut t, "0123");
t.resize(2, 2).unwrap();
assert_eq!("01", plain(&t));
}
#[test]
#[ignore = "pagelist: reflow less cols leaves the active area one row too low (screen has '01\n23' but active/viewport shows '23'); see pagelist-requests"]
fn terminal_resize_with_wraparound_on() {
let mut t = init(4, 2);
t.modes.set(Mode::Wraparound, true);
print_chars(&mut t, "0123");
t.resize(2, 2).unwrap();
assert_eq!("01\n23", plain(&t));
}
#[test]
fn terminal_resize_with_high_unique_style_per_cell() {
let mut t = init(30, 30);
for y in 0..t.rows {
for x in 0..t.cols {
t.set_cursor_pos(usize::from(y), usize::from(x));
t.set_attribute(Attribute::DirectColorBg(Rgb {
r: x as u8,
g: y as u8,
b: 0,
}))
.unwrap();
t.print(u32::from('x')).unwrap();
}
}
t.resize(60, 30).unwrap();
}
#[test]
fn terminal_resize_with_high_unique_style_per_cell_with_wrapping() {
let mut t = init(30, 30);
let cell_count: u16 = t.rows * t.cols;
for i in 0..cell_count {
let r = (i >> 8) as u8;
let g = (i & 0xFF) as u8;
t.set_attribute(Attribute::DirectColorBg(Rgb { r, g, b: 0 }))
.unwrap();
t.print(u32::from('x')).unwrap();
}
t.resize(60, 30).unwrap();
}
#[test]
fn terminal_resize_with_reflow_and_saved_cursor() {
let mut t = init(2, 3);
print_str(&mut t, "1A2B");
t.set_cursor_pos(2, 2);
assert_eq!(u32::from('B'), at_cursor(&t).codepoint());
assert_eq!("1A\n2B", plain(&t));
t.save_cursor();
t.resize(5, 3).unwrap();
t.restore_cursor();
assert_eq!("1A2B", plain(&t));
// Verify our cursor is still in the same place
assert_eq!(u32::from('B'), at_cursor(&t).codepoint());
}
#[test]
fn terminal_resize_with_reflow_and_saved_cursor_pending_wrap() {
let mut t = init(2, 3);
print_str(&mut t, "1A2B");
assert_eq!(u32::from('B'), at_cursor(&t).codepoint());
assert_eq!("1A\n2B", plain(&t));
t.save_cursor();
t.resize(5, 3).unwrap();
t.restore_cursor();
assert_eq!("1A2B", plain(&t));
// Pending wrap should be reset
t.print(u32::from('X')).unwrap();
assert_eq!("1A2BX", plain(&t));
}
#[test]
fn terminal_deccolm_without_dec_mode_40() {
let mut t = init(5, 5);
t.modes.set(Mode::M132Column, true);
t.deccolm(DeccolmMode::Cols132).unwrap();
assert_eq!(5, t.cols);
assert_eq!(5, t.rows);
assert!(!t.modes.get(Mode::M132Column));
}
#[test]
fn terminal_deccolm_unset() {
let mut t = init(5, 5);
t.modes.set(Mode::EnableMode3, true);
t.deccolm(DeccolmMode::Cols80).unwrap();
assert_eq!(80, t.cols);
assert_eq!(5, t.rows);
}
#[test]
fn terminal_deccolm_resets_pending_wrap() {
let mut t = init(5, 5);
print_chars(&mut t, "ABCDE");
assert!(t.screen().cursor.pending_wrap);
t.modes.set(Mode::EnableMode3, true);
t.deccolm(DeccolmMode::Cols80).unwrap();
assert_eq!(80, t.cols);
assert_eq!(5, t.rows);
assert!(!t.screen().cursor.pending_wrap);
}
#[test]
fn terminal_deccolm_preserves_sgr_bg() {
let mut t = init(5, 5);
t.set_attribute(Attribute::DirectColorBg(Rgb {
r: 0xFF,
g: 0,
b: 0,
}))
.unwrap();
t.modes.set(Mode::EnableMode3, true);
t.deccolm(DeccolmMode::Cols80).unwrap();
let cell = cell_at(&t, pt_active(0, 0));
assert!(cell.content_tag() == ContentTag::BgColorRgb);
assert_eq!(
Rgb {
r: 0xFF,
g: 0,
b: 0
},
cell.color_rgb()
);
}
#[test]
fn terminal_deccolm_resets_scroll_region() {
let mut t = init(5, 5);
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_top_and_bottom_margin(2, 3);
t.set_left_and_right_margin(3, 5);
t.modes.set(Mode::EnableMode3, true);
t.deccolm(DeccolmMode::Cols80).unwrap();
assert!(t.modes.get(Mode::EnableLeftAndRightMargin));
assert_eq!(0, t.scrolling_region.top);
assert_eq!(4, t.scrolling_region.bottom);
assert_eq!(0, t.scrolling_region.left);
assert_eq!(79, t.scrolling_region.right);
}
#[test]
fn terminal_mode_47_alt_screen_plain() {
let mut t = init(5, 5);
print_str(&mut t, "1A");
t.switch_screen_mode(SwitchScreenMode::M47, true).unwrap();
assert_eq!(ScreenKey::Alternate, t.screens.active_key);
assert_eq!("", plain(&t));
// Off center because we copy the cursor over from the primary screen
print_str(&mut t, "2B");
assert_eq!(" 2B", plain(&t));
t.switch_screen_mode(SwitchScreenMode::M47, false).unwrap();
assert_eq!(ScreenKey::Primary, t.screens.active_key);
assert_eq!("1A", plain(&t));
t.switch_screen_mode(SwitchScreenMode::M47, true).unwrap();
assert_eq!(ScreenKey::Alternate, t.screens.active_key);
// Screen should retain content
assert_eq!(" 2B", plain(&t));
}
fn style_checks(t: &Terminal) {
assert!(t.screen().cursor.style_id != style::DEFAULT_ID);
let page = t.screen().cursor_page();
assert_eq!(1, page.styles.count());
assert!(page.styles.ref_count(t.screen().cursor.style_id) > 0);
}
fn copies_cursor_both_directions(mode: SwitchScreenMode) {
let mut t = init(5, 5);
// Color our cursor red
t.set_attribute(Attribute::DirectColorFg(Rgb {
r: 0xFF,
g: 0,
b: 0x7F,
}))
.unwrap();
t.switch_screen_mode(mode, true).unwrap();
assert_eq!(ScreenKey::Alternate, t.screens.active_key);
style_checks(&t);
// Set a new style
t.set_attribute(Attribute::DirectColorFg(Rgb {
r: 0,
g: 0xFF,
b: 0,
}))
.unwrap();
t.switch_screen_mode(mode, false).unwrap();
assert_eq!(ScreenKey::Primary, t.screens.active_key);
style_checks(&t);
}
#[test]
fn terminal_mode_47_copies_cursor_both_directions() {
copies_cursor_both_directions(SwitchScreenMode::M47);
}
#[test]
fn terminal_mode_1047_alt_screen_plain() {
let mut t = init(5, 5);
print_str(&mut t, "1A");
t.switch_screen_mode(SwitchScreenMode::M1047, true).unwrap();
assert_eq!(ScreenKey::Alternate, t.screens.active_key);
assert_eq!("", plain(&t));
print_str(&mut t, "2B");
assert_eq!(" 2B", plain(&t));
t.switch_screen_mode(SwitchScreenMode::M1047, false)
.unwrap();
assert_eq!(ScreenKey::Primary, t.screens.active_key);
assert_eq!("1A", plain(&t));
t.switch_screen_mode(SwitchScreenMode::M1047, true).unwrap();
assert_eq!(ScreenKey::Alternate, t.screens.active_key);
assert_eq!("", plain(&t));
}
#[test]
fn terminal_mode_1047_copies_cursor_both_directions() {
copies_cursor_both_directions(SwitchScreenMode::M1047);
}
#[test]
fn terminal_mode_1049_alt_screen_plain() {
let mut t = init(5, 5);
print_str(&mut t, "1A");
t.switch_screen_mode(SwitchScreenMode::M1049, true).unwrap();
assert_eq!(ScreenKey::Alternate, t.screens.active_key);
assert_eq!("", plain(&t));
print_str(&mut t, "2B");
assert_eq!(" 2B", plain(&t));
t.switch_screen_mode(SwitchScreenMode::M1049, false)
.unwrap();
assert_eq!(ScreenKey::Primary, t.screens.active_key);
assert_eq!("1A", plain(&t));
// Write, our cursor should be restored back.
print_str(&mut t, "C");
assert_eq!("1AC", plain(&t));
t.switch_screen_mode(SwitchScreenMode::M1049, true).unwrap();
assert_eq!(ScreenKey::Alternate, t.screens.active_key);
assert_eq!("", plain(&t));
}
// Reproduces a crash found by AFL++ fuzzer: orphaned spacer_tail when
// deleteLines clears all rows without shifting.
#[test]
fn terminal_delete_lines_wide_char_at_right_margin_with_full_clear() {
let mut t = init(80, 24);
t.set_cursor_pos(10, 39);
t.print(0x4E2D).unwrap();
t.modes.set(Mode::EnableLeftAndRightMargin, true);
t.set_left_and_right_margin(5, 39);
let rows = usize::from(t.rows);
t.scroll_up(rows).unwrap();
}