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:
1 parent
ecf16f4a97
commit
ea6fb5880f
16 files changed
+11178
-8
No files matched your search
@@ -9,6 +9,10 @@ ghostty-datastruct.workspace = true
|
|||||||
ghostty-unicode.workspace = true
|
ghostty-unicode.workspace = true
|
||||||
ghostty-vt.workspace = true
|
ghostty-vt.workspace = true
|
||||||
log = "0.4.34"
|
log = "0.4.34"
|
||||||
|
onig = "6.5.3"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
ghostty-config.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
@@ -11,5 +11,8 @@ pub mod screen;
|
|||||||
pub mod screen_set;
|
pub mod screen_set;
|
||||||
pub mod selection;
|
pub mod selection;
|
||||||
pub mod size;
|
pub mod size;
|
||||||
|
pub mod stream_readonly;
|
||||||
|
pub mod string_map;
|
||||||
pub mod style;
|
pub mod style;
|
||||||
pub mod tabstops;
|
pub mod tabstops;
|
||||||
|
pub mod terminal;
|
||||||
@@ -361,14 +361,7 @@ pub enum SemanticContentSet {
|
|||||||
Input(InputClear),
|
Input(InputClear),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A string and a map from each byte of the string to the pin it came from.
|
pub use crate::string_map::StringMap;
|
||||||
///
|
|
||||||
/// TODO(integration): replace with `string_map::StringMap` once ported.
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
||||||
pub struct StringMap {
|
|
||||||
pub string: String,
|
|
||||||
pub map: Vec<Pin>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct SelectionString {
|
pub struct SelectionString {
|
||||||
|
|||||||
@@ -0,0 +1,1017 @@
|
|||||||
|
//! A stream handler that applies all terminal-state-modifying actions to a
|
||||||
|
//! [`Terminal`] and ignores everything that would require a response.
|
||||||
|
//!
|
||||||
|
//! Port of `ZIG/src/terminal/stream_readonly.zig`.
|
||||||
|
//!
|
||||||
|
//! This is a Stream implementation that processes actions against a Terminal
|
||||||
|
//! and updates the Terminal state. It is called "readonly" because it only
|
||||||
|
//! processes actions that modify terminal state, while ignoring any actions
|
||||||
|
//! that require a response (like queries).
|
||||||
|
//!
|
||||||
|
//! If you're implementing a terminal emulator that only needs to render
|
||||||
|
//! output and doesn't need to respond (since it maybe isn't running the
|
||||||
|
//! actual program), this is the stream type to use. For example, this is
|
||||||
|
//! ideal for replay tooling, CI logs, PaaS builder output, etc.
|
||||||
|
use crate::{
|
||||||
|
cursor::Style as CursorStyle,
|
||||||
|
terminal::{
|
||||||
|
DeccolmMode, Error, MouseEvents, MouseFormat, MouseShiftCapture, SwitchScreenMode, Terminal,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use ghostty_vt::{
|
||||||
|
ansi::{CursorStyle as AnsiCursorStyle, ModifyKeyFormat, ProtectedMode},
|
||||||
|
color::Dynamic as DynamicColor,
|
||||||
|
csi,
|
||||||
|
kitty::{self, key::SetMode as KittySetMode},
|
||||||
|
modes::Mode,
|
||||||
|
osc::color::{self as osc_color, Request, Target},
|
||||||
|
stream::{self, Action},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The stream type wrapping [`Handler`].
|
||||||
|
pub type Stream<'a> = stream::Stream<Handler<'a>>;
|
||||||
|
|
||||||
|
/// See [`Stream`], which is just the stream wrapper around this.
|
||||||
|
///
|
||||||
|
/// This isn't attached directly to Terminal because there is additional
|
||||||
|
/// state and options we plan to add in the future, such as APC/DCS which
|
||||||
|
/// don't make sense to me to add to the Terminal directly. Instead, you
|
||||||
|
/// can call `vt_handler` on Terminal to initialize this handler.
|
||||||
|
pub struct Handler<'a> {
|
||||||
|
/// The terminal state to modify.
|
||||||
|
pub terminal: &'a mut Terminal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Handler<'a> {
|
||||||
|
pub fn new(terminal: &'a mut Terminal) -> Self {
|
||||||
|
Self { terminal }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn horizontal_tab(&mut self, count: u16) {
|
||||||
|
for _ in 0..count {
|
||||||
|
let x = self.terminal.screen().cursor.x;
|
||||||
|
self.terminal.horizontal_tab();
|
||||||
|
if x == self.terminal.screen().cursor.x {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn horizontal_tab_back(&mut self, count: u16) {
|
||||||
|
for _ in 0..count {
|
||||||
|
let x = self.terminal.screen().cursor.x;
|
||||||
|
self.terminal.horizontal_tab_back();
|
||||||
|
if x == self.terminal.screen().cursor.x {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_mode(&mut self, mode: Mode, enabled: bool) -> Result<(), Error> {
|
||||||
|
// Set the mode on the terminal
|
||||||
|
self.terminal.modes.set(mode, enabled);
|
||||||
|
|
||||||
|
// Some modes require additional processing
|
||||||
|
match mode {
|
||||||
|
Mode::Autorepeat | Mode::ReverseColors => {}
|
||||||
|
|
||||||
|
Mode::Origin => self.terminal.set_cursor_pos(1, 1),
|
||||||
|
|
||||||
|
Mode::EnableLeftAndRightMargin => {
|
||||||
|
if !enabled {
|
||||||
|
self.terminal.scrolling_region.left = 0;
|
||||||
|
self.terminal.scrolling_region.right = self.terminal.cols - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mode::AltScreenLegacy => self
|
||||||
|
.terminal
|
||||||
|
.switch_screen_mode(SwitchScreenMode::M47, enabled)?,
|
||||||
|
Mode::AltScreen => self
|
||||||
|
.terminal
|
||||||
|
.switch_screen_mode(SwitchScreenMode::M1047, enabled)?,
|
||||||
|
Mode::AltScreenSaveCursorClearEnter => self
|
||||||
|
.terminal
|
||||||
|
.switch_screen_mode(SwitchScreenMode::M1049, enabled)?,
|
||||||
|
|
||||||
|
Mode::SaveCursor => {
|
||||||
|
if enabled {
|
||||||
|
self.terminal.save_cursor();
|
||||||
|
} else {
|
||||||
|
self.terminal.restore_cursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mode::EnableMode3 => {}
|
||||||
|
|
||||||
|
Mode::M132Column => self.terminal.deccolm(if enabled {
|
||||||
|
DeccolmMode::Cols132
|
||||||
|
} else {
|
||||||
|
DeccolmMode::Cols80
|
||||||
|
})?,
|
||||||
|
|
||||||
|
Mode::SynchronizedOutput
|
||||||
|
| Mode::Linefeed
|
||||||
|
| Mode::InBandSizeReports
|
||||||
|
| Mode::FocusEvent => {}
|
||||||
|
|
||||||
|
Mode::MouseEventX10 => {
|
||||||
|
self.terminal.flags.mouse_event = if enabled {
|
||||||
|
MouseEvents::X10
|
||||||
|
} else {
|
||||||
|
MouseEvents::None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Mode::MouseEventNormal => {
|
||||||
|
self.terminal.flags.mouse_event = if enabled {
|
||||||
|
MouseEvents::Normal
|
||||||
|
} else {
|
||||||
|
MouseEvents::None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Mode::MouseEventButton => {
|
||||||
|
self.terminal.flags.mouse_event = if enabled {
|
||||||
|
MouseEvents::Button
|
||||||
|
} else {
|
||||||
|
MouseEvents::None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Mode::MouseEventAny => {
|
||||||
|
self.terminal.flags.mouse_event = if enabled {
|
||||||
|
MouseEvents::Any
|
||||||
|
} else {
|
||||||
|
MouseEvents::None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Mode::MouseFormatUtf8 => {
|
||||||
|
self.terminal.flags.mouse_format = if enabled {
|
||||||
|
MouseFormat::Utf8
|
||||||
|
} else {
|
||||||
|
MouseFormat::X10
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Mode::MouseFormatSgr => {
|
||||||
|
self.terminal.flags.mouse_format = if enabled {
|
||||||
|
MouseFormat::Sgr
|
||||||
|
} else {
|
||||||
|
MouseFormat::X10
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Mode::MouseFormatUrxvt => {
|
||||||
|
self.terminal.flags.mouse_format = if enabled {
|
||||||
|
MouseFormat::Urxvt
|
||||||
|
} else {
|
||||||
|
MouseFormat::X10
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Mode::MouseFormatSgrPixels => {
|
||||||
|
self.terminal.flags.mouse_format = if enabled {
|
||||||
|
MouseFormat::SgrPixels
|
||||||
|
} else {
|
||||||
|
MouseFormat::X10
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn color_operation(&mut self, requests: &osc_color::List) {
|
||||||
|
for req in requests {
|
||||||
|
match *req {
|
||||||
|
Request::Set(set) => match set.target {
|
||||||
|
Target::Palette(i) => {
|
||||||
|
self.terminal.flags.dirty.palette = true;
|
||||||
|
self.terminal.colors.palette.set(i, set.color);
|
||||||
|
}
|
||||||
|
Target::Dynamic(dynamic) => match dynamic {
|
||||||
|
DynamicColor::Foreground => self.terminal.colors.foreground.set(set.color),
|
||||||
|
DynamicColor::Background => self.terminal.colors.background.set(set.color),
|
||||||
|
DynamicColor::Cursor => self.terminal.colors.cursor.set(set.color),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Target::Special(_) => {}
|
||||||
|
},
|
||||||
|
|
||||||
|
Request::Reset(target) => match target {
|
||||||
|
Target::Palette(i) => {
|
||||||
|
self.terminal.flags.dirty.palette = true;
|
||||||
|
self.terminal.colors.palette.reset(i);
|
||||||
|
}
|
||||||
|
Target::Dynamic(dynamic) => match dynamic {
|
||||||
|
DynamicColor::Foreground => self.terminal.colors.foreground.reset(),
|
||||||
|
DynamicColor::Background => self.terminal.colors.background.reset(),
|
||||||
|
DynamicColor::Cursor => self.terminal.colors.cursor.reset(),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Target::Special(_) => {}
|
||||||
|
},
|
||||||
|
|
||||||
|
Request::ResetPalette => {
|
||||||
|
let indices: Vec<usize> = self.terminal.colors.palette.mask.iter().collect();
|
||||||
|
for i in indices {
|
||||||
|
self.terminal.flags.dirty.palette = true;
|
||||||
|
self.terminal.colors.palette.reset(i as u8);
|
||||||
|
}
|
||||||
|
self.terminal.colors.palette.mask =
|
||||||
|
ghostty_vt::color::PaletteMask::init_empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
Request::Query(_) | Request::ResetSpecial => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kitty_color_operation(&mut self, request: &kitty::color::OSC) {
|
||||||
|
use kitty::color::{Kind, Request as KReq, Special};
|
||||||
|
for item in &request.list {
|
||||||
|
match item {
|
||||||
|
KReq::Set { key, color } => match key {
|
||||||
|
Kind::Palette(palette) => {
|
||||||
|
self.terminal.flags.dirty.palette = true;
|
||||||
|
self.terminal.colors.palette.set(*palette, *color);
|
||||||
|
}
|
||||||
|
Kind::Special(special) => match special {
|
||||||
|
Special::Foreground => self.terminal.colors.foreground.set(*color),
|
||||||
|
Special::Background => self.terminal.colors.background.set(*color),
|
||||||
|
Special::Cursor => self.terminal.colors.cursor.set(*color),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
KReq::Reset(key) => match key {
|
||||||
|
Kind::Palette(palette) => {
|
||||||
|
self.terminal.flags.dirty.palette = true;
|
||||||
|
self.terminal.colors.palette.reset(*palette);
|
||||||
|
}
|
||||||
|
Kind::Special(special) => match special {
|
||||||
|
Special::Foreground => self.terminal.colors.foreground.reset(),
|
||||||
|
Special::Background => self.terminal.colors.background.reset(),
|
||||||
|
Special::Cursor => self.terminal.colors.cursor.reset(),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
KReq::Query(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl stream::Handler for Handler<'_> {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn vt(&mut self, action: Action<'_>) -> Result<(), Error> {
|
||||||
|
let t = &mut *self.terminal;
|
||||||
|
match action {
|
||||||
|
Action::Print(v) => t.print(v.cp)?,
|
||||||
|
Action::PrintRepeat(v) => t.print_repeat(v)?,
|
||||||
|
Action::Backspace => t.backspace(),
|
||||||
|
Action::CarriageReturn => t.carriage_return(),
|
||||||
|
Action::Linefeed => t.linefeed()?,
|
||||||
|
Action::Index => t.index()?,
|
||||||
|
Action::NextLine => {
|
||||||
|
t.index()?;
|
||||||
|
t.carriage_return();
|
||||||
|
}
|
||||||
|
Action::ReverseIndex => t.reverse_index(),
|
||||||
|
Action::CursorUp(v) => t.cursor_up(usize::from(v.value)),
|
||||||
|
Action::CursorDown(v) => t.cursor_down(usize::from(v.value)),
|
||||||
|
Action::CursorLeft(v) => t.cursor_left(usize::from(v.value)),
|
||||||
|
Action::CursorRight(v) => t.cursor_right(usize::from(v.value)),
|
||||||
|
Action::CursorPos(v) => t.set_cursor_pos(usize::from(v.row), usize::from(v.col)),
|
||||||
|
Action::CursorCol(v) => {
|
||||||
|
let row = usize::from(t.screen().cursor.y) + 1;
|
||||||
|
t.set_cursor_pos(row, usize::from(v.value));
|
||||||
|
}
|
||||||
|
Action::CursorRow(v) => {
|
||||||
|
let col = usize::from(t.screen().cursor.x) + 1;
|
||||||
|
t.set_cursor_pos(usize::from(v.value), col);
|
||||||
|
}
|
||||||
|
Action::CursorColRelative(v) => {
|
||||||
|
let row = usize::from(t.screen().cursor.y) + 1;
|
||||||
|
let col =
|
||||||
|
(usize::from(t.screen().cursor.x) + 1).saturating_add(usize::from(v.value));
|
||||||
|
t.set_cursor_pos(row, col);
|
||||||
|
}
|
||||||
|
Action::CursorRowRelative(v) => {
|
||||||
|
let row =
|
||||||
|
(usize::from(t.screen().cursor.y) + 1).saturating_add(usize::from(v.value));
|
||||||
|
let col = usize::from(t.screen().cursor.x) + 1;
|
||||||
|
t.set_cursor_pos(row, col);
|
||||||
|
}
|
||||||
|
Action::CursorStyle(v) => {
|
||||||
|
let blink = match v {
|
||||||
|
AnsiCursorStyle::Default
|
||||||
|
| AnsiCursorStyle::SteadyBlock
|
||||||
|
| AnsiCursorStyle::SteadyBar
|
||||||
|
| AnsiCursorStyle::SteadyUnderline => false,
|
||||||
|
AnsiCursorStyle::BlinkingBlock
|
||||||
|
| AnsiCursorStyle::BlinkingBar
|
||||||
|
| AnsiCursorStyle::BlinkingUnderline => true,
|
||||||
|
};
|
||||||
|
let style = match v {
|
||||||
|
AnsiCursorStyle::Default
|
||||||
|
| AnsiCursorStyle::BlinkingBlock
|
||||||
|
| AnsiCursorStyle::SteadyBlock => CursorStyle::Block,
|
||||||
|
AnsiCursorStyle::BlinkingBar | AnsiCursorStyle::SteadyBar => CursorStyle::Bar,
|
||||||
|
AnsiCursorStyle::BlinkingUnderline | AnsiCursorStyle::SteadyUnderline => {
|
||||||
|
CursorStyle::Underline
|
||||||
|
}
|
||||||
|
};
|
||||||
|
t.modes.set(Mode::CursorBlinking, blink);
|
||||||
|
t.screen_mut().cursor.cursor_style = style;
|
||||||
|
}
|
||||||
|
Action::EraseDisplayBelow(p) => t.erase_display(csi::EraseDisplay::Below, p),
|
||||||
|
Action::EraseDisplayAbove(p) => t.erase_display(csi::EraseDisplay::Above, p),
|
||||||
|
Action::EraseDisplayComplete(p) => t.erase_display(csi::EraseDisplay::Complete, p),
|
||||||
|
Action::EraseDisplayScrollback(p) => t.erase_display(csi::EraseDisplay::Scrollback, p),
|
||||||
|
Action::EraseDisplayScrollComplete(p) => {
|
||||||
|
t.erase_display(csi::EraseDisplay::ScrollComplete, p);
|
||||||
|
}
|
||||||
|
Action::EraseLineRight(p) => t.erase_line(csi::EraseLine::Right, p),
|
||||||
|
Action::EraseLineLeft(p) => t.erase_line(csi::EraseLine::Left, p),
|
||||||
|
Action::EraseLineComplete(p) => t.erase_line(csi::EraseLine::Complete, p),
|
||||||
|
Action::EraseLineRightUnlessPendingWrap(p) => {
|
||||||
|
t.erase_line(csi::EraseLine::RightUnlessPendingWrap, p);
|
||||||
|
}
|
||||||
|
Action::DeleteChars(v) => t.delete_chars(v),
|
||||||
|
Action::EraseChars(v) => t.erase_chars(v),
|
||||||
|
Action::InsertLines(v) => t.insert_lines(v),
|
||||||
|
Action::InsertBlanks(v) => t.insert_blanks(v),
|
||||||
|
Action::DeleteLines(v) => t.delete_lines(v),
|
||||||
|
Action::ScrollUp(v) => t.scroll_up(v)?,
|
||||||
|
Action::ScrollDown(v) => t.scroll_down(v),
|
||||||
|
Action::HorizontalTab(v) => self.horizontal_tab(v),
|
||||||
|
Action::HorizontalTabBack(v) => self.horizontal_tab_back(v),
|
||||||
|
Action::TabClearCurrent => t.tab_clear(csi::TabClear::Current),
|
||||||
|
Action::TabClearAll => t.tab_clear(csi::TabClear::All),
|
||||||
|
Action::TabSet => t.tab_set(),
|
||||||
|
Action::TabReset => t.tab_reset(),
|
||||||
|
Action::SetMode(v) => self.set_mode(v.mode, true)?,
|
||||||
|
Action::ResetMode(v) => self.set_mode(v.mode, false)?,
|
||||||
|
Action::SaveMode(v) => t.modes.save(v.mode),
|
||||||
|
Action::RestoreMode(v) => {
|
||||||
|
let enabled = t.modes.restore(v.mode);
|
||||||
|
self.set_mode(v.mode, enabled)?;
|
||||||
|
}
|
||||||
|
Action::TopAndBottomMargin(v) => {
|
||||||
|
t.set_top_and_bottom_margin(usize::from(v.top_left), usize::from(v.bottom_right));
|
||||||
|
}
|
||||||
|
Action::LeftAndRightMargin(v) => {
|
||||||
|
t.set_left_and_right_margin(usize::from(v.top_left), usize::from(v.bottom_right));
|
||||||
|
}
|
||||||
|
Action::LeftAndRightMarginAmbiguous => {
|
||||||
|
if t.modes.get(Mode::EnableLeftAndRightMargin) {
|
||||||
|
t.set_left_and_right_margin(0, 0);
|
||||||
|
} else {
|
||||||
|
t.save_cursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::SaveCursor => t.save_cursor(),
|
||||||
|
Action::RestoreCursor => t.restore_cursor(),
|
||||||
|
Action::InvokeCharset(v) => t.invoke_charset(v.bank, v.charset, v.locking),
|
||||||
|
Action::ConfigureCharset(v) => t.configure_charset(v.slot, v.charset),
|
||||||
|
Action::SetAttribute(v) => {
|
||||||
|
if !matches!(v, ghostty_vt::sgr::Attribute::Unknown(_)) {
|
||||||
|
let _ = t.set_attribute(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::ProtectedModeOff => t.set_protected_mode(ProtectedMode::Off),
|
||||||
|
Action::ProtectedModeIso => t.set_protected_mode(ProtectedMode::Iso),
|
||||||
|
Action::ProtectedModeDec => t.set_protected_mode(ProtectedMode::Dec),
|
||||||
|
Action::MouseShiftCapture(v) => {
|
||||||
|
t.flags.mouse_shift_capture = if v {
|
||||||
|
MouseShiftCapture::True
|
||||||
|
} else {
|
||||||
|
MouseShiftCapture::False
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Action::KittyKeyboardPush(v) => t.screen_mut().kitty_keyboard.push(v.flags),
|
||||||
|
Action::KittyKeyboardPop(v) => t.screen_mut().kitty_keyboard.pop(usize::from(v)),
|
||||||
|
Action::KittyKeyboardSet(v) => {
|
||||||
|
t.screen_mut()
|
||||||
|
.kitty_keyboard
|
||||||
|
.set(KittySetMode::Set, v.flags);
|
||||||
|
}
|
||||||
|
Action::KittyKeyboardSetOr(v) => {
|
||||||
|
t.screen_mut().kitty_keyboard.set(KittySetMode::Or, v.flags);
|
||||||
|
}
|
||||||
|
Action::KittyKeyboardSetNot(v) => {
|
||||||
|
t.screen_mut()
|
||||||
|
.kitty_keyboard
|
||||||
|
.set(KittySetMode::Not, v.flags);
|
||||||
|
}
|
||||||
|
Action::ModifyKeyFormat(v) => {
|
||||||
|
t.flags.modify_other_keys_2 = false;
|
||||||
|
if v == ModifyKeyFormat::OtherKeysNumeric {
|
||||||
|
t.flags.modify_other_keys_2 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::ActiveStatusDisplay(v) => t.status_display = v,
|
||||||
|
Action::Decaln => t.decaln()?,
|
||||||
|
Action::FullReset => t.full_reset(),
|
||||||
|
Action::StartHyperlink(v) => t.screen_mut().start_hyperlink(v.uri, v.id)?,
|
||||||
|
Action::EndHyperlink => t.screen_mut().end_hyperlink(),
|
||||||
|
Action::SemanticPrompt(v) => t.semantic_prompt(v)?,
|
||||||
|
Action::MouseShape(v) => t.mouse_shape = v,
|
||||||
|
Action::ColorOperation(v) => self.color_operation(v.requests),
|
||||||
|
Action::KittyColorReport(v) => self.kitty_color_operation(v),
|
||||||
|
|
||||||
|
// No supported DCS commands have any terminal-modifying effects,
|
||||||
|
// but they may in the future. For now we just ignore it.
|
||||||
|
Action::DcsHook(_) | Action::DcsPut(_) | Action::DcsUnhook => {}
|
||||||
|
|
||||||
|
// APC can modify terminal state (Kitty graphics) but we don't
|
||||||
|
// currently support it in the readonly stream.
|
||||||
|
Action::ApcStart | Action::ApcEnd | Action::ApcPut(_) => {}
|
||||||
|
|
||||||
|
// Have no terminal-modifying effect
|
||||||
|
Action::Bell
|
||||||
|
| Action::Enquiry
|
||||||
|
| Action::RequestMode(_)
|
||||||
|
| Action::RequestModeUnknown(_)
|
||||||
|
| Action::SizeReport(_)
|
||||||
|
| Action::Xtversion
|
||||||
|
| Action::DeviceAttributes(_)
|
||||||
|
| Action::DeviceStatus(_)
|
||||||
|
| Action::KittyKeyboardQuery
|
||||||
|
| Action::WindowTitle(_)
|
||||||
|
| Action::ReportPwd(_)
|
||||||
|
| Action::ShowDesktopNotification(_)
|
||||||
|
| Action::ProgressReport(_)
|
||||||
|
| Action::ClipboardContents(_)
|
||||||
|
| Action::TitlePush(_)
|
||||||
|
| Action::TitlePop(_) => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{page::SemanticContent, screen_set::Key as ScreenKey, terminal::Options};
|
||||||
|
use ghostty_vt::osc::semantic_prompt::Redraw;
|
||||||
|
|
||||||
|
fn term(cols: u16, rows: u16) -> Terminal {
|
||||||
|
Terminal::new(Options::new(cols, rows)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_print() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
s.next_slice(b"Hello").unwrap();
|
||||||
|
assert_eq!(5, s.handler.terminal.screen().cursor.x);
|
||||||
|
assert_eq!(0, s.handler.terminal.screen().cursor.y);
|
||||||
|
|
||||||
|
assert_eq!("Hello", s.handler.terminal.plain_string().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_movement() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Move cursor using escape sequences
|
||||||
|
s.next_slice(b"Hello\x1B[1;1H").unwrap();
|
||||||
|
assert_eq!(0, s.handler.terminal.screen().cursor.x);
|
||||||
|
assert_eq!(0, s.handler.terminal.screen().cursor.y);
|
||||||
|
|
||||||
|
// Move to position 2,3
|
||||||
|
s.next_slice(b"\x1B[2;3H").unwrap();
|
||||||
|
assert_eq!(2, s.handler.terminal.screen().cursor.x);
|
||||||
|
assert_eq!(1, s.handler.terminal.screen().cursor.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn erase_operations() {
|
||||||
|
let mut t = term(20, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Print some text
|
||||||
|
s.next_slice(b"Hello World").unwrap();
|
||||||
|
assert_eq!(11, s.handler.terminal.screen().cursor.x);
|
||||||
|
assert_eq!(0, s.handler.terminal.screen().cursor.y);
|
||||||
|
|
||||||
|
// Move cursor to position 1,6 and erase from cursor to end of line
|
||||||
|
s.next_slice(b"\x1B[1;6H").unwrap();
|
||||||
|
s.next_slice(b"\x1B[K").unwrap();
|
||||||
|
|
||||||
|
assert_eq!("Hello", s.handler.terminal.plain_string().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tabs() {
|
||||||
|
let mut t = term(80, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
s.next_slice(b"A\tB").unwrap();
|
||||||
|
assert_eq!(9, s.handler.terminal.screen().cursor.x);
|
||||||
|
assert_eq!("A B", s.handler.terminal.plain_string().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modes() {
|
||||||
|
let mut t = term(80, 24);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Test wraparound mode
|
||||||
|
assert!(s.handler.terminal.modes.get(Mode::Wraparound));
|
||||||
|
s.next_slice(b"\x1B[?7l").unwrap(); // Disable wraparound
|
||||||
|
assert!(!s.handler.terminal.modes.get(Mode::Wraparound));
|
||||||
|
s.next_slice(b"\x1B[?7h").unwrap(); // Enable wraparound
|
||||||
|
assert!(s.handler.terminal.modes.get(Mode::Wraparound));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scrolling_regions() {
|
||||||
|
let mut t = term(80, 24);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Set scrolling region from line 5 to 20
|
||||||
|
s.next_slice(b"\x1B[5;20r").unwrap();
|
||||||
|
let sr = s.handler.terminal.scrolling_region;
|
||||||
|
assert_eq!(4, sr.top);
|
||||||
|
assert_eq!(19, sr.bottom);
|
||||||
|
assert_eq!(0, sr.left);
|
||||||
|
assert_eq!(79, sr.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn charsets() {
|
||||||
|
let mut t = term(80, 24);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Configure G0 as DEC special graphics
|
||||||
|
s.next_slice(b"\x1B(0").unwrap();
|
||||||
|
s.next_slice(b"`").unwrap(); // Should print diamond character
|
||||||
|
|
||||||
|
assert_eq!("\u{25c6}", s.handler.terminal.plain_string().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alt_screen() {
|
||||||
|
let mut t = term(10, 5);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Write to primary screen
|
||||||
|
s.next_slice(b"Primary").unwrap();
|
||||||
|
assert_eq!(ScreenKey::Primary, s.handler.terminal.screens.active_key);
|
||||||
|
|
||||||
|
// Switch to alt screen
|
||||||
|
s.next_slice(b"\x1B[?1049h").unwrap();
|
||||||
|
assert_eq!(ScreenKey::Alternate, s.handler.terminal.screens.active_key);
|
||||||
|
|
||||||
|
// Write to alt screen
|
||||||
|
s.next_slice(b"Alt").unwrap();
|
||||||
|
|
||||||
|
// Switch back to primary
|
||||||
|
s.next_slice(b"\x1B[?1049l").unwrap();
|
||||||
|
assert_eq!(ScreenKey::Primary, s.handler.terminal.screens.active_key);
|
||||||
|
|
||||||
|
assert_eq!("Primary", s.handler.terminal.plain_string().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_save_and_restore() {
|
||||||
|
let mut t = term(80, 24);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Move cursor to 10,15
|
||||||
|
s.next_slice(b"\x1B[10;15H").unwrap();
|
||||||
|
assert_eq!(14, s.handler.terminal.screen().cursor.x);
|
||||||
|
assert_eq!(9, s.handler.terminal.screen().cursor.y);
|
||||||
|
|
||||||
|
// Save cursor
|
||||||
|
s.next_slice(b"\x1B7").unwrap();
|
||||||
|
|
||||||
|
// Move cursor elsewhere
|
||||||
|
s.next_slice(b"\x1B[1;1H").unwrap();
|
||||||
|
assert_eq!(0, s.handler.terminal.screen().cursor.x);
|
||||||
|
assert_eq!(0, s.handler.terminal.screen().cursor.y);
|
||||||
|
|
||||||
|
// Restore cursor
|
||||||
|
s.next_slice(b"\x1B8").unwrap();
|
||||||
|
assert_eq!(14, s.handler.terminal.screen().cursor.x);
|
||||||
|
assert_eq!(9, s.handler.terminal.screen().cursor.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attributes() {
|
||||||
|
let mut t = term(80, 24);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Set bold and write text
|
||||||
|
s.next_slice(b"\x1B[1mBold\x1B[0m").unwrap();
|
||||||
|
|
||||||
|
// Verify we can write attributes - just check the string was written
|
||||||
|
assert_eq!("Bold", s.handler.terminal.plain_string().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decaln_screen_alignment() {
|
||||||
|
let mut t = term(10, 3);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Run DECALN
|
||||||
|
s.next_slice(b"\x1B#8").unwrap();
|
||||||
|
|
||||||
|
// Verify entire screen is filled with 'E'
|
||||||
|
assert_eq!(
|
||||||
|
"EEEEEEEEEE\nEEEEEEEEEE\nEEEEEEEEEE",
|
||||||
|
s.handler.terminal.plain_string().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cursor should be at 1,1
|
||||||
|
assert_eq!(0, s.handler.terminal.screen().cursor.x);
|
||||||
|
assert_eq!(0, s.handler.terminal.screen().cursor.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_reset() {
|
||||||
|
let mut t = term(80, 24);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Make some changes
|
||||||
|
s.next_slice(b"Hello").unwrap();
|
||||||
|
s.next_slice(b"\x1B[10;20H").unwrap();
|
||||||
|
s.next_slice(b"\x1B[5;20r").unwrap(); // Set scroll region
|
||||||
|
s.next_slice(b"\x1B[?7l").unwrap(); // Disable wraparound
|
||||||
|
|
||||||
|
// Full reset
|
||||||
|
s.next_slice(b"\x1Bc").unwrap();
|
||||||
|
|
||||||
|
// Verify reset state
|
||||||
|
let t = &*s.handler.terminal;
|
||||||
|
assert_eq!(0, t.screen().cursor.x);
|
||||||
|
assert_eq!(0, t.screen().cursor.y);
|
||||||
|
assert_eq!(0, t.scrolling_region.top);
|
||||||
|
assert_eq!(23, t.scrolling_region.bottom);
|
||||||
|
assert!(t.modes.get(Mode::Wraparound));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_query_actions() {
|
||||||
|
let mut t = term(80, 24);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// These should be ignored without error
|
||||||
|
s.next_slice(b"\x1B[c").unwrap(); // Device attributes
|
||||||
|
s.next_slice(b"\x1B[5n").unwrap(); // Device status report
|
||||||
|
s.next_slice(b"\x1B[6n").unwrap(); // Cursor position report
|
||||||
|
|
||||||
|
// Terminal should still be functional
|
||||||
|
s.next_slice(b"Test").unwrap();
|
||||||
|
assert_eq!("Test", s.handler.terminal.plain_string().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_4_set_and_reset_palette() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Save default color
|
||||||
|
let default_color_0 = s.handler.terminal.colors.palette.original[0];
|
||||||
|
|
||||||
|
// Set color 0 to red
|
||||||
|
s.next_slice(b"\x1b]4;0;rgb:ff/00/00\x1b\\").unwrap();
|
||||||
|
let p = &s.handler.terminal.colors.palette;
|
||||||
|
assert_eq!(0xff, p.current[0].r);
|
||||||
|
assert_eq!(0x00, p.current[0].g);
|
||||||
|
assert_eq!(0x00, p.current[0].b);
|
||||||
|
assert!(p.mask.is_set(0));
|
||||||
|
|
||||||
|
// Reset color 0
|
||||||
|
s.next_slice(b"\x1b]104;0\x1b\\").unwrap();
|
||||||
|
let p = &s.handler.terminal.colors.palette;
|
||||||
|
assert_eq!(default_color_0, p.current[0]);
|
||||||
|
assert!(!p.mask.is_set(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_104_reset_all_palette_colors() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Set multiple colors
|
||||||
|
s.next_slice(b"\x1b]4;0;rgb:ff/00/00\x1b\\").unwrap();
|
||||||
|
s.next_slice(b"\x1b]4;1;rgb:00/ff/00\x1b\\").unwrap();
|
||||||
|
s.next_slice(b"\x1b]4;2;rgb:00/00/ff\x1b\\").unwrap();
|
||||||
|
let p = &s.handler.terminal.colors.palette;
|
||||||
|
assert!(p.mask.is_set(0));
|
||||||
|
assert!(p.mask.is_set(1));
|
||||||
|
assert!(p.mask.is_set(2));
|
||||||
|
|
||||||
|
// Reset all palette colors
|
||||||
|
s.next_slice(b"\x1b]104\x1b\\").unwrap();
|
||||||
|
let p = &s.handler.terminal.colors.palette;
|
||||||
|
assert_eq!(p.original[0], p.current[0]);
|
||||||
|
assert_eq!(p.original[1], p.current[1]);
|
||||||
|
assert_eq!(p.original[2], p.current[2]);
|
||||||
|
assert!(!p.mask.is_set(0));
|
||||||
|
assert!(!p.mask.is_set(1));
|
||||||
|
assert!(!p.mask.is_set(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_10_set_and_reset_foreground_color() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Initially unset
|
||||||
|
assert!(s.handler.terminal.colors.foreground.get().is_none());
|
||||||
|
|
||||||
|
// Set foreground to red
|
||||||
|
s.next_slice(b"\x1b]10;rgb:ff/00/00\x1b\\").unwrap();
|
||||||
|
let fg = s.handler.terminal.colors.foreground.get().unwrap();
|
||||||
|
assert_eq!(0xff, fg.r);
|
||||||
|
assert_eq!(0x00, fg.g);
|
||||||
|
assert_eq!(0x00, fg.b);
|
||||||
|
|
||||||
|
// Reset foreground
|
||||||
|
s.next_slice(b"\x1b]110\x1b\\").unwrap();
|
||||||
|
assert!(s.handler.terminal.colors.foreground.get().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_11_set_and_reset_background_color() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Set background to green
|
||||||
|
s.next_slice(b"\x1b]11;rgb:00/ff/00\x1b\\").unwrap();
|
||||||
|
let bg = s.handler.terminal.colors.background.get().unwrap();
|
||||||
|
assert_eq!(0x00, bg.r);
|
||||||
|
assert_eq!(0xff, bg.g);
|
||||||
|
assert_eq!(0x00, bg.b);
|
||||||
|
|
||||||
|
// Reset background
|
||||||
|
s.next_slice(b"\x1b]111\x1b\\").unwrap();
|
||||||
|
assert!(s.handler.terminal.colors.background.get().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_12_set_and_reset_cursor_color() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Set cursor to blue
|
||||||
|
s.next_slice(b"\x1b]12;rgb:00/00/ff\x1b\\").unwrap();
|
||||||
|
let cursor = s.handler.terminal.colors.cursor.get().unwrap();
|
||||||
|
assert_eq!(0x00, cursor.r);
|
||||||
|
assert_eq!(0x00, cursor.g);
|
||||||
|
assert_eq!(0xff, cursor.b);
|
||||||
|
|
||||||
|
// Reset cursor
|
||||||
|
s.next_slice(b"\x1b]112\x1b\\").unwrap();
|
||||||
|
// After reset, cursor might be null (using default)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kitty_color_protocol_set_palette() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Set palette color 5 to magenta using kitty protocol
|
||||||
|
s.next_slice(b"\x1b]21;5=rgb:ff/00/ff\x1b\\").unwrap();
|
||||||
|
let t = &*s.handler.terminal;
|
||||||
|
assert_eq!(0xff, t.colors.palette.current[5].r);
|
||||||
|
assert_eq!(0x00, t.colors.palette.current[5].g);
|
||||||
|
assert_eq!(0xff, t.colors.palette.current[5].b);
|
||||||
|
assert!(t.colors.palette.mask.is_set(5));
|
||||||
|
assert!(t.flags.dirty.palette);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kitty_color_protocol_reset_palette() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Set and then reset palette color
|
||||||
|
let original = s.handler.terminal.colors.palette.original[7];
|
||||||
|
s.next_slice(b"\x1b]21;7=rgb:aa/bb/cc\x1b\\").unwrap();
|
||||||
|
assert!(s.handler.terminal.colors.palette.mask.is_set(7));
|
||||||
|
|
||||||
|
s.next_slice(b"\x1b]21;7=\x1b\\").unwrap();
|
||||||
|
let p = &s.handler.terminal.colors.palette;
|
||||||
|
assert_eq!(original, p.current[7]);
|
||||||
|
assert!(!p.mask.is_set(7));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kitty_color_protocol_set_foreground() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Set foreground using kitty protocol
|
||||||
|
s.next_slice(b"\x1b]21;foreground=rgb:12/34/56\x1b\\")
|
||||||
|
.unwrap();
|
||||||
|
let fg = s.handler.terminal.colors.foreground.get().unwrap();
|
||||||
|
assert_eq!(0x12, fg.r);
|
||||||
|
assert_eq!(0x34, fg.g);
|
||||||
|
assert_eq!(0x56, fg.b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kitty_color_protocol_set_background() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Set background using kitty protocol
|
||||||
|
s.next_slice(b"\x1b]21;background=rgb:78/9a/bc\x1b\\")
|
||||||
|
.unwrap();
|
||||||
|
let bg = s.handler.terminal.colors.background.get().unwrap();
|
||||||
|
assert_eq!(0x78, bg.r);
|
||||||
|
assert_eq!(0x9a, bg.g);
|
||||||
|
assert_eq!(0xbc, bg.b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kitty_color_protocol_set_cursor() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Set cursor using kitty protocol
|
||||||
|
s.next_slice(b"\x1b]21;cursor=rgb:de/f0/12\x1b\\").unwrap();
|
||||||
|
let cursor = s.handler.terminal.colors.cursor.get().unwrap();
|
||||||
|
assert_eq!(0xde, cursor.r);
|
||||||
|
assert_eq!(0xf0, cursor.g);
|
||||||
|
assert_eq!(0x12, cursor.b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kitty_color_protocol_reset_foreground() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Set and reset foreground
|
||||||
|
s.next_slice(b"\x1b]21;foreground=rgb:11/22/33\x1b\\")
|
||||||
|
.unwrap();
|
||||||
|
assert!(s.handler.terminal.colors.foreground.get().is_some());
|
||||||
|
|
||||||
|
s.next_slice(b"\x1b]21;foreground=\x1b\\").unwrap();
|
||||||
|
// After reset, should be unset
|
||||||
|
assert!(s.handler.terminal.colors.foreground.get().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn palette_dirty_flag_set_on_color_change() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Clear dirty flag
|
||||||
|
s.handler.terminal.flags.dirty.palette = false;
|
||||||
|
|
||||||
|
// Setting palette color should set dirty flag
|
||||||
|
s.next_slice(b"\x1b]4;0;rgb:ff/00/00\x1b\\").unwrap();
|
||||||
|
assert!(s.handler.terminal.flags.dirty.palette);
|
||||||
|
|
||||||
|
// Clear and test reset
|
||||||
|
s.handler.terminal.flags.dirty.palette = false;
|
||||||
|
s.next_slice(b"\x1b]104;0\x1b\\").unwrap();
|
||||||
|
assert!(s.handler.terminal.flags.dirty.palette);
|
||||||
|
|
||||||
|
// Clear and test kitty protocol
|
||||||
|
s.handler.terminal.flags.dirty.palette = false;
|
||||||
|
s.next_slice(b"\x1b]21;1=rgb:00/ff/00\x1b\\").unwrap();
|
||||||
|
assert!(s.handler.terminal.flags.dirty.palette);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn semantic_prompt_fresh_line() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
s.next_slice(b"Hello").unwrap();
|
||||||
|
s.next_slice(b"\x1b]133;L\x07").unwrap();
|
||||||
|
assert_eq!(0, s.handler.terminal.screen().cursor.x);
|
||||||
|
assert_eq!(1, s.handler.terminal.screen().cursor.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn semantic_prompt_fresh_line_new_prompt() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Write some text and then send OSC 133;A (fresh_line_new_prompt)
|
||||||
|
s.next_slice(b"Hello").unwrap();
|
||||||
|
s.next_slice(b"\x1b]133;A\x07").unwrap();
|
||||||
|
|
||||||
|
// Should do a fresh line (carriage return + index)
|
||||||
|
assert_eq!(0, s.handler.terminal.screen().cursor.x);
|
||||||
|
assert_eq!(1, s.handler.terminal.screen().cursor.y);
|
||||||
|
|
||||||
|
// Should set cursor semantic_content to prompt
|
||||||
|
assert_eq!(
|
||||||
|
SemanticContent::Prompt,
|
||||||
|
s.handler.terminal.screen().cursor.semantic_content
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test with redraw option
|
||||||
|
s.next_slice(b"prompt$ ").unwrap();
|
||||||
|
s.next_slice(b"\x1b]133;A;redraw=1\x07").unwrap();
|
||||||
|
assert_eq!(Redraw::True, s.handler.terminal.flags.shell_redraws_prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn semantic_prompt_end_of_input_then_start_output() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Write some text and then send OSC 133;A (fresh_line_new_prompt)
|
||||||
|
s.next_slice(b"Hello").unwrap();
|
||||||
|
s.next_slice(b"\x1b]133;A\x07").unwrap();
|
||||||
|
s.next_slice(b"prompt$ ").unwrap();
|
||||||
|
s.next_slice(b"\x1b]133;B\x07").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
SemanticContent::Input,
|
||||||
|
s.handler.terminal.screen().cursor.semantic_content
|
||||||
|
);
|
||||||
|
s.next_slice(b"\x1b]133;C\x07").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
SemanticContent::Output,
|
||||||
|
s.handler.terminal.screen().cursor.semantic_content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn semantic_prompt_prompt_start() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Write some text
|
||||||
|
s.next_slice(b"Hello").unwrap();
|
||||||
|
|
||||||
|
// OSC 133;P marks the start of a prompt (without fresh line behavior)
|
||||||
|
s.next_slice(b"\x1b]133;P\x07").unwrap();
|
||||||
|
let scr = s.handler.terminal.screen();
|
||||||
|
assert_eq!(SemanticContent::Prompt, scr.cursor.semantic_content);
|
||||||
|
assert_eq!(5, scr.cursor.x);
|
||||||
|
assert_eq!(0, scr.cursor.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn semantic_prompt_new_command() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Write some text
|
||||||
|
s.next_slice(b"Hello").unwrap();
|
||||||
|
s.next_slice(b"\x1b]133;N\x07").unwrap();
|
||||||
|
|
||||||
|
// Should behave like fresh_line_new_prompt - cursor moves to column 0
|
||||||
|
// on next line since we had content
|
||||||
|
let scr = s.handler.terminal.screen();
|
||||||
|
assert_eq!(0, scr.cursor.x);
|
||||||
|
assert_eq!(1, scr.cursor.y);
|
||||||
|
assert_eq!(SemanticContent::Prompt, scr.cursor.semantic_content);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn semantic_prompt_new_command_at_column_zero() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// OSC 133;N when already at column 0 should stay on same line
|
||||||
|
s.next_slice(b"\x1b]133;N\x07").unwrap();
|
||||||
|
let scr = s.handler.terminal.screen();
|
||||||
|
assert_eq!(0, scr.cursor.x);
|
||||||
|
assert_eq!(0, scr.cursor.y);
|
||||||
|
assert_eq!(SemanticContent::Prompt, scr.cursor.semantic_content);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn semantic_prompt_end_prompt_start_input_terminate_eol_clears_on_linefeed() {
|
||||||
|
let mut t = term(10, 10);
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
// Set input terminated by EOL
|
||||||
|
s.next_slice(b"\x1b]133;I\x07").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
SemanticContent::Input,
|
||||||
|
s.handler.terminal.screen().cursor.semantic_content
|
||||||
|
);
|
||||||
|
|
||||||
|
// Linefeed should reset semantic content to output
|
||||||
|
s.next_slice(b"\n").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
SemanticContent::Output,
|
||||||
|
s.handler.terminal.screen().cursor.semantic_content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stream_csi_w_with_intermediate_but_no_params() {
|
||||||
|
// Regression test from AFL++ crash. CSI ? W without
|
||||||
|
// parameters caused an out-of-bounds access on input.params[0].
|
||||||
|
let mut o = Options::new(80, 24);
|
||||||
|
o.max_scrollback = 100;
|
||||||
|
let mut t = Terminal::new(o).unwrap();
|
||||||
|
let mut s = t.vt_stream();
|
||||||
|
|
||||||
|
s.next_slice(b"\x1b[?W").unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
//! A string along with the mapping of each individual byte in the string
|
||||||
|
//! to the point in the screen.
|
||||||
|
//!
|
||||||
|
//! Port of `ZIG/src/terminal/StringMap.zig`. Regex matching uses Oniguruma
|
||||||
|
//! (the `onig` crate), like Ghostty.
|
||||||
|
use crate::{page_list::Pin, selection::Selection};
|
||||||
|
use onig::{MatchParam, Regex, Region, SearchOptions};
|
||||||
|
|
||||||
|
/// Retry budget for StringMap regex searches.
|
||||||
|
///
|
||||||
|
/// Units are Oniguruma retry steps (internal backtracking/retry counter),
|
||||||
|
/// not bytes/characters/time.
|
||||||
|
///
|
||||||
|
/// Deviation: the `onig` crate only exposes the retry limit *in match*
|
||||||
|
/// (Zig also sets the retry limit in search); both abort matching with an
|
||||||
|
/// error which is treated as "no further matches".
|
||||||
|
const ONI_SEARCH_RETRY_LIMIT: u32 = 100_000;
|
||||||
|
|
||||||
|
/// A string and a map from each byte of the string to the pin it came from.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct StringMap {
|
||||||
|
pub string: String,
|
||||||
|
pub map: Vec<Pin>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StringMap {
|
||||||
|
/// Returns an iterator that yields the next match of the given regex.
|
||||||
|
pub fn search_iterator<'a>(&'a self, regex: &'a Regex) -> SearchIterator<'a> {
|
||||||
|
SearchIterator {
|
||||||
|
map: self,
|
||||||
|
regex,
|
||||||
|
offset: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterates over the regular expression matches of the string.
|
||||||
|
pub struct SearchIterator<'a> {
|
||||||
|
map: &'a StringMap,
|
||||||
|
regex: &'a Regex,
|
||||||
|
offset: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SearchIterator<'a> {
|
||||||
|
/// Returns the next regular expression match or None if there are
|
||||||
|
/// no more matches.
|
||||||
|
pub fn next_match(&mut self) -> Option<Match<'a>> {
|
||||||
|
if self.offset >= self.map.string.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use per-search match params so we can bound regex retry steps
|
||||||
|
// (Oniguruma's internal backtracking work counter).
|
||||||
|
let mut match_param = MatchParam::default();
|
||||||
|
match_param.set_retry_limit_in_match(ONI_SEARCH_RETRY_LIMIT);
|
||||||
|
|
||||||
|
let haystack = &self.map.string[self.offset..];
|
||||||
|
let mut region = Region::new();
|
||||||
|
let result = self.regex.search_with_param(
|
||||||
|
haystack,
|
||||||
|
0,
|
||||||
|
haystack.len(),
|
||||||
|
SearchOptions::SEARCH_OPTION_NONE,
|
||||||
|
Some(&mut region),
|
||||||
|
match_param,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mismatch and retry/stack-limit errors mean no further matches.
|
||||||
|
let Ok(Some(_)) = result else {
|
||||||
|
self.offset = self.map.string.len();
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let (start, end) = region.pos(0)?;
|
||||||
|
|
||||||
|
// Increment our offset by the number of bytes in the match.
|
||||||
|
// We do this after building the match so that it is returned
|
||||||
|
// with the old offset.
|
||||||
|
let m = Match {
|
||||||
|
map: self.map,
|
||||||
|
offset: self.offset,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
};
|
||||||
|
self.offset += end;
|
||||||
|
Some(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for SearchIterator<'a> {
|
||||||
|
type Item = Match<'a>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Match<'a>> {
|
||||||
|
self.next_match()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single regular expression match.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Match<'a> {
|
||||||
|
map: &'a StringMap,
|
||||||
|
offset: usize,
|
||||||
|
/// Byte range of the match relative to `offset`.
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Match<'_> {
|
||||||
|
/// Returns the selection containing the full match.
|
||||||
|
pub fn selection(&self) -> Selection {
|
||||||
|
let start_pt = self.map.map[self.offset + self.start];
|
||||||
|
let end_pt = self.map.map[self.offset + self.end - 1];
|
||||||
|
Selection::new(start_pt, end_pt, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{
|
||||||
|
point::{Coordinate, Point, Tag},
|
||||||
|
screen::{Options, Screen, SelectLine, SelectionString},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn active(x: u16, y: u32) -> Point {
|
||||||
|
Point::Active(Coordinate { x, y })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn screen_pt(s: &Screen, pin: Pin) -> Point {
|
||||||
|
s.pages.point_from_pin(Tag::Screen, pin).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_map(s: &Screen, x: u16, y: u32) -> StringMap {
|
||||||
|
let pin = s.pages.pin(active(x, y)).unwrap();
|
||||||
|
let line = s.select_line(SelectLine::new(pin)).unwrap();
|
||||||
|
s.selection_string_map(SelectionString {
|
||||||
|
sel: line,
|
||||||
|
trim: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_map_search_iterator() {
|
||||||
|
// Initialize our regex
|
||||||
|
let re = Regex::new("[A-B]{2}").unwrap();
|
||||||
|
|
||||||
|
// Initialize our screen
|
||||||
|
let mut s = Screen::new(Options::new(5, 5, 0)).unwrap();
|
||||||
|
s.test_write_string("1ABCD2EFGH\n3IJKL").unwrap();
|
||||||
|
let map = line_map(&s, 2, 1);
|
||||||
|
|
||||||
|
// Get our iterator
|
||||||
|
let mut it = map.search_iterator(&re);
|
||||||
|
{
|
||||||
|
let m = it.next_match().unwrap();
|
||||||
|
let sel = m.selection();
|
||||||
|
assert_eq!(
|
||||||
|
Point::Screen(Coordinate { x: 1, y: 0 }),
|
||||||
|
screen_pt(&s, sel.start(&s.pages))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Point::Screen(Coordinate { x: 2, y: 0 }),
|
||||||
|
screen_pt(&s, sel.end(&s.pages))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(it.next_match().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_map_search_iterator_url_detection() {
|
||||||
|
let re = Regex::new(ghostty_config::url::REGEX).unwrap();
|
||||||
|
|
||||||
|
let mut s = Screen::new(Options::new(40, 5, 0)).unwrap();
|
||||||
|
s.test_write_string("hello https://example.com/path world")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let map = line_map(&s, 10, 0);
|
||||||
|
|
||||||
|
let mut it = map.search_iterator(&re);
|
||||||
|
{
|
||||||
|
let m = it.next_match().unwrap();
|
||||||
|
let sel = m.selection();
|
||||||
|
// URL should start at x=6 ("https://example.com/path" starts after "hello ")
|
||||||
|
assert_eq!(
|
||||||
|
Point::Screen(Coordinate { x: 6, y: 0 }),
|
||||||
|
screen_pt(&s, sel.start(&s.pages))
|
||||||
|
);
|
||||||
|
// URL should end at x=29 (end of "/path")
|
||||||
|
assert_eq!(
|
||||||
|
Point::Screen(Coordinate { x: 29, y: 0 }),
|
||||||
|
screen_pt(&s, sel.end(&s.pages))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(it.next_match().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_map_search_iterator_url_with_click_position() {
|
||||||
|
let re = Regex::new(ghostty_config::url::REGEX).unwrap();
|
||||||
|
|
||||||
|
let mut s = Screen::new(Options::new(40, 5, 0)).unwrap();
|
||||||
|
s.test_write_string("hello https://example.com world")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Simulate clicking on "example" (x=14)
|
||||||
|
let click_pin = s.pages.pin(active(14, 0)).unwrap();
|
||||||
|
|
||||||
|
let map = line_map(&s, 14, 0);
|
||||||
|
|
||||||
|
let mut it = map.search_iterator(&re);
|
||||||
|
let mut found_url = false;
|
||||||
|
while let Some(m) = it.next_match() {
|
||||||
|
let sel = m.selection();
|
||||||
|
if sel.contains(&s.pages, click_pin) {
|
||||||
|
found_url = true;
|
||||||
|
// Verify URL bounds
|
||||||
|
assert_eq!(
|
||||||
|
Point::Screen(Coordinate { x: 6, y: 0 }),
|
||||||
|
screen_pt(&s, sel.start(&s.pages))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Point::Screen(Coordinate { x: 24, y: 0 }),
|
||||||
|
screen_pt(&s, sel.end(&s.pages))
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(found_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3271 @@
|
|||||||
|
//! The primary terminal emulation structure. This represents a single
|
||||||
|
//! "terminal" containing a grid of characters and exposes various operations
|
||||||
|
//! on that grid. This also maintains the scrollback buffer.
|
||||||
|
//!
|
||||||
|
//! Port of `ZIG/src/terminal/Terminal.zig`.
|
||||||
|
//!
|
||||||
|
//! # Differences from the Zig API
|
||||||
|
//!
|
||||||
|
//! * `screens.active` is `screens.active()` / `screens.active_mut()`.
|
||||||
|
//! * Allocators are dropped; fallible operations return
|
||||||
|
//! [`Result<_, Error>`](Error) (`page::Error`).
|
||||||
|
//! * `switch_screen` returns the *key* of the previous screen instead of a
|
||||||
|
//! pointer to it (`self.screens.get(old)` gives the screen back).
|
||||||
|
//! * `print_attributes` returns a `String` instead of writing into a buffer.
|
||||||
|
#![allow(
|
||||||
|
clippy::manual_assert_eq,
|
||||||
|
clippy::single_match_else,
|
||||||
|
clippy::items_after_statements,
|
||||||
|
clippy::too_many_lines
|
||||||
|
)]
|
||||||
|
use crate::{
|
||||||
|
page::{Cell, ContentTag, Page, SemanticContent, SemanticPrompt as RowPrompt, Wide},
|
||||||
|
page_list::{Direction, IncreaseCapacity, Pin},
|
||||||
|
pin_iter,
|
||||||
|
point::{Coordinate, Point},
|
||||||
|
screen::{
|
||||||
|
CharsetState, InputClear, Options as ScreenOptions, Resize as ScreenResize, SavedCursor,
|
||||||
|
Screen, Scroll, SemanticClick, SemanticContentSet as SetSemantic,
|
||||||
|
},
|
||||||
|
screen_set::{Key as ScreenKey, ScreenSet},
|
||||||
|
size,
|
||||||
|
style::{self, Color as StyleColor, Style},
|
||||||
|
tabstops::Tabstops,
|
||||||
|
};
|
||||||
|
use ghostty_vt::{
|
||||||
|
ansi::{ProtectedMode, StatusDisplay},
|
||||||
|
charsets::{self, Charset, Slots},
|
||||||
|
color::{DynamicPalette, DynamicRgb},
|
||||||
|
csi,
|
||||||
|
kitty::graphics_command as kitty_graphics_command,
|
||||||
|
modes::{Mode, ModePacked, ModeState},
|
||||||
|
mouse_shape::MouseShape,
|
||||||
|
osc::{
|
||||||
|
self,
|
||||||
|
semantic_prompt::{Action as PromptAction, Redraw},
|
||||||
|
},
|
||||||
|
sgr,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use crate::page::Error;
|
||||||
|
|
||||||
|
/// Default tabstop interval
|
||||||
|
const TABSTOP_INTERVAL: usize = 8;
|
||||||
|
|
||||||
|
/// The unicode codepoint for kitty graphics placeholders.
|
||||||
|
const KITTY_PLACEHOLDER: u32 = 0x10EEEE;
|
||||||
|
|
||||||
|
/// The set of screens behind this terminal (e.g. primary vs alternate).
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Terminal {
|
||||||
|
/// The set of screens behind this terminal (e.g. primary vs alternate).
|
||||||
|
pub screens: ScreenSet,
|
||||||
|
|
||||||
|
/// Whether we're currently writing to the status line (DECSASD and DECSSDT).
|
||||||
|
/// We don't support a status line currently so we just black hole this
|
||||||
|
/// data so that it doesn't mess up our main display.
|
||||||
|
pub status_display: StatusDisplay,
|
||||||
|
|
||||||
|
/// Where the tabstops are.
|
||||||
|
pub tabstops: Tabstops,
|
||||||
|
|
||||||
|
/// The size of the terminal.
|
||||||
|
pub rows: size::CellCountInt,
|
||||||
|
pub cols: size::CellCountInt,
|
||||||
|
|
||||||
|
/// The size of the screen in pixels. This is used for pty events and images
|
||||||
|
pub width_px: u32,
|
||||||
|
pub height_px: u32,
|
||||||
|
|
||||||
|
/// The current scrolling region.
|
||||||
|
pub scrolling_region: ScrollingRegion,
|
||||||
|
|
||||||
|
/// The last reported pwd, if any.
|
||||||
|
pub pwd: Vec<u8>,
|
||||||
|
|
||||||
|
/// The color state for this terminal.
|
||||||
|
pub colors: Colors,
|
||||||
|
|
||||||
|
/// The previous printed character. This is used for the repeat previous
|
||||||
|
/// char CSI (ESC [ <n> b).
|
||||||
|
pub previous_char: Option<u32>,
|
||||||
|
|
||||||
|
/// The modes that this terminal currently has active.
|
||||||
|
pub modes: ModeState,
|
||||||
|
|
||||||
|
/// The most recently set mouse shape for the terminal.
|
||||||
|
pub mouse_shape: MouseShape,
|
||||||
|
|
||||||
|
/// These are just a packed set of flags we may set on the terminal.
|
||||||
|
pub flags: Flags,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `flags` struct of the terminal (a packed struct in Zig).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Flags {
|
||||||
|
/// This supports a Kitty extension where programs using semantic
|
||||||
|
/// prompts (OSC133) can annotate their new prompts with `redraw=0` to
|
||||||
|
/// disable clearing the prompt on resize.
|
||||||
|
pub shell_redraws_prompt: Redraw,
|
||||||
|
|
||||||
|
/// This is set via ESC[4;2m. Any other modify key mode just sets
|
||||||
|
/// this to false and we act in mode 1 by default.
|
||||||
|
pub modify_other_keys_2: bool,
|
||||||
|
|
||||||
|
/// The mouse event mode and format. These are set to the last
|
||||||
|
/// set mode in modes. You can't get the right event/format to use
|
||||||
|
/// based on modes alone because modes don't show you what order
|
||||||
|
/// this was called so we have to track it separately.
|
||||||
|
pub mouse_event: MouseEvents,
|
||||||
|
pub mouse_format: MouseFormat,
|
||||||
|
|
||||||
|
/// Set via the XTSHIFTESCAPE sequence. If true (XTSHIFTESCAPE = 1)
|
||||||
|
/// then we want to capture the shift key for the mouse protocol
|
||||||
|
/// if the configuration allows it.
|
||||||
|
pub mouse_shift_capture: MouseShiftCapture,
|
||||||
|
|
||||||
|
/// True if the window is focused.
|
||||||
|
pub focused: bool,
|
||||||
|
|
||||||
|
/// True if the terminal is in a password entry mode. This is set
|
||||||
|
/// to true based on termios state.
|
||||||
|
pub password_input: bool,
|
||||||
|
|
||||||
|
/// True if the terminal should perform selection scrolling.
|
||||||
|
pub selection_scroll: bool,
|
||||||
|
|
||||||
|
/// Dirty flag used only by the search thread. The renderer is expected
|
||||||
|
/// to set this to true if the viewport was dirty as it was rendering.
|
||||||
|
/// This is used by the search thread to more efficiently re-search the
|
||||||
|
/// viewport and active area.
|
||||||
|
///
|
||||||
|
/// Since the renderer is going to inspect the viewport/active area ANYWAYS,
|
||||||
|
/// this lets our search thread do less work and hold the lock less time,
|
||||||
|
/// resulting in more throughput for everything.
|
||||||
|
pub search_viewport_dirty: bool,
|
||||||
|
|
||||||
|
/// Dirty flags for the renderer.
|
||||||
|
pub dirty: Dirty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Flags {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
shell_redraws_prompt: Redraw::True,
|
||||||
|
modify_other_keys_2: false,
|
||||||
|
mouse_event: MouseEvents::None,
|
||||||
|
mouse_format: MouseFormat::X10,
|
||||||
|
mouse_shift_capture: MouseShiftCapture::Null,
|
||||||
|
focused: true,
|
||||||
|
password_input: false,
|
||||||
|
selection_scroll: false,
|
||||||
|
search_viewport_dirty: false,
|
||||||
|
dirty: Dirty::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `mouse_shift_capture` tri-state (Zig `enum(u2) { null, false, true }`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum MouseShiftCapture {
|
||||||
|
#[default]
|
||||||
|
Null,
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The various color configurations a terminal maintains and that can
|
||||||
|
/// be set dynamically via OSC, with defaults usually coming from a
|
||||||
|
/// configuration.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Colors {
|
||||||
|
pub background: DynamicRgb,
|
||||||
|
pub foreground: DynamicRgb,
|
||||||
|
pub cursor: DynamicRgb,
|
||||||
|
pub palette: DynamicPalette,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Colors {
|
||||||
|
pub const DEFAULT: Colors = Colors {
|
||||||
|
background: DynamicRgb::UNSET,
|
||||||
|
foreground: DynamicRgb::UNSET,
|
||||||
|
cursor: DynamicRgb::UNSET,
|
||||||
|
palette: DynamicPalette::DEFAULT,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Colors {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is a set of dirty flags the renderer can use to determine
|
||||||
|
/// what parts of the screen need to be redrawn. It is up to the renderer
|
||||||
|
/// to clear these flags.
|
||||||
|
///
|
||||||
|
/// This only contains dirty flags for terminal state, not for the screen
|
||||||
|
/// state. The screen state has its own dirty flags.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct Dirty {
|
||||||
|
/// Set when the color palette is modified in any way.
|
||||||
|
pub palette: bool,
|
||||||
|
|
||||||
|
/// Set when the reverse colors mode is modified.
|
||||||
|
pub reverse_colors: bool,
|
||||||
|
|
||||||
|
/// Screen clear of some kind. This can be due to a screen change,
|
||||||
|
/// erase display, etc.
|
||||||
|
pub clear: bool,
|
||||||
|
|
||||||
|
/// Set when the pre-edit is modified.
|
||||||
|
pub preedit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The event types that can be reported for mouse-related activities.
|
||||||
|
/// These are all mutually exclusive (hence in a single enum).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum MouseEvents {
|
||||||
|
#[default]
|
||||||
|
None = 0,
|
||||||
|
X10 = 1, // 9
|
||||||
|
Normal = 2, // 1000
|
||||||
|
Button = 3, // 1002
|
||||||
|
Any = 4, // 1003
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MouseEvents {
|
||||||
|
/// Returns true if this event sends motion events.
|
||||||
|
pub fn motion(self) -> bool {
|
||||||
|
self == MouseEvents::Button || self == MouseEvents::Any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The format of mouse events when enabled.
|
||||||
|
/// These are all mutually exclusive (hence in a single enum).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum MouseFormat {
|
||||||
|
#[default]
|
||||||
|
X10 = 0,
|
||||||
|
Utf8 = 1, // 1005
|
||||||
|
Sgr = 2, // 1006
|
||||||
|
Urxvt = 3, // 1015
|
||||||
|
SgrPixels = 4, // 1016
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrolling region is the area of the screen designated where scrolling
|
||||||
|
/// occurs. When scrolling the screen, only this viewport is scrolled.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ScrollingRegion {
|
||||||
|
// Top and bottom of the scroll region (0-indexed)
|
||||||
|
// Precondition: top < bottom
|
||||||
|
pub top: size::CellCountInt,
|
||||||
|
pub bottom: size::CellCountInt,
|
||||||
|
|
||||||
|
// Left/right scroll regions.
|
||||||
|
// Precondition: right > left
|
||||||
|
// Precondition: right <= cols - 1
|
||||||
|
pub left: size::CellCountInt,
|
||||||
|
pub right: size::CellCountInt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Options {
|
||||||
|
pub cols: size::CellCountInt,
|
||||||
|
pub rows: size::CellCountInt,
|
||||||
|
pub max_scrollback: usize,
|
||||||
|
pub colors: Colors,
|
||||||
|
|
||||||
|
/// The default mode state. When the terminal gets a reset, it
|
||||||
|
/// will revert back to this state.
|
||||||
|
pub default_modes: ModePacked,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Options {
|
||||||
|
pub fn new(cols: size::CellCountInt, rows: size::CellCountInt) -> Self {
|
||||||
|
Self {
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
max_scrollback: 10_000,
|
||||||
|
colors: Colors::DEFAULT,
|
||||||
|
default_modes: ModePacked::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The semantic prompt type. This is used when tracking a line type and
|
||||||
|
/// requires integration with the shell. By default, we mark a line as "none"
|
||||||
|
/// meaning we don't know what type it is.
|
||||||
|
///
|
||||||
|
/// See: <https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md>
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SemanticPrompt {
|
||||||
|
Prompt,
|
||||||
|
PromptContinuation,
|
||||||
|
Input,
|
||||||
|
Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for scrolling the viewport of the terminal grid.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ScrollViewport {
|
||||||
|
/// Scroll to the top of the scrollback
|
||||||
|
Top,
|
||||||
|
|
||||||
|
/// Scroll to the bottom, i.e. the top of the active area
|
||||||
|
Bottom,
|
||||||
|
|
||||||
|
/// Scroll by some delta amount, up is negative.
|
||||||
|
Delta(isize),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The modes for DECCOLM.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum DeccolmMode {
|
||||||
|
Cols80 = 0,
|
||||||
|
Cols132 = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modal screen changes. These map to the literal terminal
|
||||||
|
/// modes to enable or disable alternate screen modes. They each
|
||||||
|
/// have subtle behaviors so we define them as an enum here.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SwitchScreenMode {
|
||||||
|
/// Legacy alternate screen mode. This goes to the alternate
|
||||||
|
/// screen or primary screen and only copies the cursor. The
|
||||||
|
/// screen is not erased.
|
||||||
|
M47,
|
||||||
|
|
||||||
|
/// Alternate screen mode where the alternate screen is cleared
|
||||||
|
/// on exit. The primary screen is never cleared. The cursor is
|
||||||
|
/// copied.
|
||||||
|
M1047,
|
||||||
|
|
||||||
|
/// Save primary screen cursor, switch to alternate screen,
|
||||||
|
/// and clear the alternate screen on entry. On exit,
|
||||||
|
/// do not clear the screen, and restore the cursor on the
|
||||||
|
/// primary screen.
|
||||||
|
M1049,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_tabstops_err(_: std::collections::TryReserveError) -> Error {
|
||||||
|
Error::OutOfMemory
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The adjustment needed to retry a failed partial row clone (insert/delete
|
||||||
|
/// lines cross-page slow path).
|
||||||
|
#[allow(clippy::option_option)]
|
||||||
|
fn capacity_adjustment(err: Error) -> Option<Option<IncreaseCapacity>> {
|
||||||
|
Some(match err {
|
||||||
|
// Rehash the sets
|
||||||
|
Error::StyleSetNeedsRehash | Error::SetNeedsRehash => None,
|
||||||
|
// Increase style memory
|
||||||
|
Error::StyleSetOutOfMemory => Some(IncreaseCapacity::Styles),
|
||||||
|
// Increase string memory
|
||||||
|
Error::StringsOutOfMemory => Some(IncreaseCapacity::StringBytes),
|
||||||
|
// Increase hyperlink memory
|
||||||
|
Error::SetOutOfMemory | Error::HyperlinkMapOutOfMemory => {
|
||||||
|
Some(IncreaseCapacity::HyperlinkBytes)
|
||||||
|
}
|
||||||
|
// Increase grapheme memory
|
||||||
|
Error::GraphemeMapOutOfMemory | Error::GraphemeAllocOutOfMemory => {
|
||||||
|
Some(IncreaseCapacity::GraphemeBytes)
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To be called before shifting a row (as in insert_lines and delete_lines)
|
||||||
|
///
|
||||||
|
/// Takes care of boundary conditions such as potentially split wide chars
|
||||||
|
/// across scrolling region boundaries and orphaned spacer heads at line
|
||||||
|
/// ends.
|
||||||
|
fn row_will_be_shifted(sr: ScrollingRegion, cols: size::CellCountInt, page: &mut Page, y: usize) {
|
||||||
|
let base = page.rows[y].cells();
|
||||||
|
|
||||||
|
// If our scrolling region includes the rightmost column then we
|
||||||
|
// need to turn any spacer heads in to normal empty cells, since
|
||||||
|
// once we move them they no longer correspond with soft-wrapped
|
||||||
|
// wide characters.
|
||||||
|
//
|
||||||
|
// If it contains either of the 2 leftmost columns, then the wide
|
||||||
|
// characters in the first column which may be associated with a
|
||||||
|
// spacer head will be either moved or cleared, so we also need
|
||||||
|
// to turn the spacer heads in to empty cells in that case.
|
||||||
|
if sr.right == cols - 1 || sr.left < 2 {
|
||||||
|
let end = base + usize::from(page.size.cols) - 1;
|
||||||
|
if page.cells[end].wide() == Wide::SpacerHead {
|
||||||
|
page.cells[end].set_wide(Wide::Narrow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the leftmost or rightmost cells of our scrolling region
|
||||||
|
// are parts of wide chars, we need to clear the cells' contents
|
||||||
|
// since they'd be split by the move.
|
||||||
|
let left = base + usize::from(sr.left);
|
||||||
|
let right = base + usize::from(sr.right);
|
||||||
|
|
||||||
|
if page.cells[left].wide() == Wide::SpacerTail {
|
||||||
|
let wide = left - 1;
|
||||||
|
if page.cells[wide].has_grapheme() {
|
||||||
|
page.clear_grapheme(wide);
|
||||||
|
page.update_row_grapheme_flag(y);
|
||||||
|
}
|
||||||
|
page.cells[wide].set_codepoint(0);
|
||||||
|
page.cells[wide].set_wide(Wide::Narrow);
|
||||||
|
page.cells[left].set_wide(Wide::Narrow);
|
||||||
|
}
|
||||||
|
|
||||||
|
if page.cells[right].wide() == Wide::Wide {
|
||||||
|
let tail = right + 1;
|
||||||
|
if page.cells[right].has_grapheme() {
|
||||||
|
page.clear_grapheme(right);
|
||||||
|
page.update_row_grapheme_flag(y);
|
||||||
|
}
|
||||||
|
page.cells[right].set_codepoint(0);
|
||||||
|
page.cells[right].set_wide(Wide::Narrow);
|
||||||
|
page.cells[tail].set_wide(Wide::Narrow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminal {
|
||||||
|
/// Initialize a new terminal.
|
||||||
|
pub fn new(opts: Options) -> Result<Terminal, Error> {
|
||||||
|
let cols = opts.cols;
|
||||||
|
let rows = opts.rows;
|
||||||
|
|
||||||
|
let screens = ScreenSet::new(ScreenOptions::new(cols, rows, opts.max_scrollback))?;
|
||||||
|
|
||||||
|
Ok(Terminal {
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
screens,
|
||||||
|
status_display: StatusDisplay::Main,
|
||||||
|
tabstops: Tabstops::new(usize::from(cols), TABSTOP_INTERVAL)
|
||||||
|
.map_err(map_tabstops_err)?,
|
||||||
|
width_px: 0,
|
||||||
|
height_px: 0,
|
||||||
|
scrolling_region: ScrollingRegion {
|
||||||
|
top: 0,
|
||||||
|
bottom: rows - 1,
|
||||||
|
left: 0,
|
||||||
|
right: cols - 1,
|
||||||
|
},
|
||||||
|
pwd: Vec::new(),
|
||||||
|
colors: opts.colors,
|
||||||
|
previous_char: None,
|
||||||
|
modes: ModeState {
|
||||||
|
values: opts.default_modes,
|
||||||
|
saved: ModePacked::default(),
|
||||||
|
default: opts.default_modes,
|
||||||
|
},
|
||||||
|
mouse_shape: MouseShape::Text,
|
||||||
|
flags: Flags::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a stream that can process VT streams and update this
|
||||||
|
/// terminal state. The streams will only process read-only data that
|
||||||
|
/// modifies terminal state. Sequences that query or otherwise require
|
||||||
|
/// output will be ignored.
|
||||||
|
pub fn vt_stream(&mut self) -> crate::stream_readonly::Stream<'_> {
|
||||||
|
crate::stream_readonly::Stream::new_alloc(self.vt_handler())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is the handler-side only for vt_stream.
|
||||||
|
pub fn vt_handler(&mut self) -> crate::stream_readonly::Handler<'_> {
|
||||||
|
crate::stream_readonly::Handler::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorthand for `self.screens.active()`.
|
||||||
|
#[inline]
|
||||||
|
pub fn screen(&self) -> &Screen {
|
||||||
|
self.screens.active()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorthand for `self.screens.active_mut()`.
|
||||||
|
#[inline]
|
||||||
|
pub fn screen_mut(&mut self) -> &mut Screen {
|
||||||
|
self.screens.active_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print UTF-8 encoded string to the terminal.
|
||||||
|
pub fn print_string(&mut self, s: &str) -> Result<(), Error> {
|
||||||
|
for ch in s.chars() {
|
||||||
|
match ch {
|
||||||
|
'\n' => {
|
||||||
|
self.carriage_return();
|
||||||
|
self.linefeed()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => self.print(u32::from(ch))?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print the previous printed character a repeated amount of times.
|
||||||
|
pub fn print_repeat(&mut self, count_req: usize) -> Result<(), Error> {
|
||||||
|
if let Some(c) = self.previous_char {
|
||||||
|
let count = count_req.max(1);
|
||||||
|
for _ in 0..count {
|
||||||
|
self.print(c)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print(&mut self, c: u32) -> Result<(), Error> {
|
||||||
|
// If we're not on the main display, do nothing for now
|
||||||
|
if self.status_display != StatusDisplay::Main {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// After doing any printing, wrapping, scrolling, etc. we want to ensure
|
||||||
|
// that our screen remains in a consistent state.
|
||||||
|
let result = self.print_inner(c);
|
||||||
|
self.screen().assert_integrity();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_inner(&mut self, c: u32) -> Result<(), Error> {
|
||||||
|
// Our right margin depends where our cursor is now.
|
||||||
|
let right_limit = if self.screen().cursor.x > self.scrolling_region.right {
|
||||||
|
self.cols
|
||||||
|
} else {
|
||||||
|
self.scrolling_region.right + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// Perform grapheme clustering if grapheme support is enabled (mode 2027).
|
||||||
|
// This is MUCH slower than the normal path so the conditional below is
|
||||||
|
// purposely ordered in least-likely to most-likely so we can drop out
|
||||||
|
// as quickly as possible.
|
||||||
|
if c > 255
|
||||||
|
&& self.modes.get(Mode::GraphemeCluster)
|
||||||
|
&& self.screen().cursor.x > 0
|
||||||
|
&& self.print_grapheme(c, right_limit)?
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the width of this character so we can handle
|
||||||
|
// non-single-width characters properly. We have a fast-path for
|
||||||
|
// byte-sized characters since they're so common. We can ignore
|
||||||
|
// control characters because they're always filtered prior.
|
||||||
|
let width: usize = if c <= 0xFF {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
usize::from(ghostty_unicode::table().get(c).width)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Note: it is possible to have a width of "3" and a width of "-1" from
|
||||||
|
// uucode.x's wcwidth. We should look into those cases and handle them
|
||||||
|
// appropriately.
|
||||||
|
assert!(width <= 2);
|
||||||
|
|
||||||
|
// Attach zero-width characters to our cell as grapheme data.
|
||||||
|
if width == 0 {
|
||||||
|
// If we have grapheme clustering enabled, we don't blindly attach
|
||||||
|
// any zero width character to our cells and we instead just ignore
|
||||||
|
// it.
|
||||||
|
if self.modes.get(Mode::GraphemeCluster) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're at cell zero, then this is malformed data and we don't
|
||||||
|
// print anything or even store this. Zero-width characters are ALWAYS
|
||||||
|
// attached to some other non-zero-width character at the time of
|
||||||
|
// writing.
|
||||||
|
if self.screen().cursor.x == 0 {
|
||||||
|
log::warn!("zero-width character with no prior character, ignoring");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find our previous cell
|
||||||
|
let (prev_x, prev) = {
|
||||||
|
let scr = self.screen();
|
||||||
|
let immediate = scr.cursor_cell_left(1);
|
||||||
|
if immediate.wide() == Wide::SpacerTail {
|
||||||
|
(scr.cursor.x - 2, scr.cursor_cell_left(2))
|
||||||
|
} else {
|
||||||
|
(scr.cursor.x - 1, immediate)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If our previous cell has no text, just ignore the zero-width character
|
||||||
|
if !prev.has_text() {
|
||||||
|
log::warn!("zero-width character with no prior character, ignoring");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a emoji variation selector, prev must be an emoji
|
||||||
|
if c == 0xFE0F || c == 0xFE0E {
|
||||||
|
let prev_props = ghostty_unicode::table().get(prev.codepoint());
|
||||||
|
let emoji = prev_props.grapheme_break
|
||||||
|
== ghostty_unicode::GraphemeBreakNoControl::ExtendedPictographic;
|
||||||
|
if !emoji {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zig's appendGrapheme does not touch the row dirty flag, but the
|
||||||
|
// Rust Page::append_grapheme marks the row dirty; preserve the old
|
||||||
|
// flag (see pagelist requests).
|
||||||
|
let was_dirty = self.screen().cursor_row().dirty();
|
||||||
|
let r = self.screen_mut().append_grapheme(prev_x, c);
|
||||||
|
if !was_dirty {
|
||||||
|
self.screen_mut().cursor_row_mut().set_dirty(false);
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a printable character, save it
|
||||||
|
self.previous_char = Some(c);
|
||||||
|
|
||||||
|
// If we're soft-wrapping, then handle that first.
|
||||||
|
if self.screen().cursor.pending_wrap && self.modes.get(Mode::Wraparound) {
|
||||||
|
self.print_wrap()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have insert mode enabled then we need to handle that. We
|
||||||
|
// only do insert mode if we're not at the end of the line.
|
||||||
|
if self.modes.get(Mode::Insert)
|
||||||
|
&& usize::from(self.screen().cursor.x) + width < usize::from(self.cols)
|
||||||
|
{
|
||||||
|
self.insert_blanks(width);
|
||||||
|
}
|
||||||
|
|
||||||
|
match width {
|
||||||
|
// Single cell is very easy: just write in the cell
|
||||||
|
1 => {
|
||||||
|
self.screen_mut().cursor_mark_dirty();
|
||||||
|
self.print_cell(c, Wide::Narrow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wide character requires a spacer. We print this by
|
||||||
|
// using two cells: the first is flagged "wide" and has the
|
||||||
|
// wide char. The second is guaranteed to be a spacer if
|
||||||
|
// we're not at the end of the line.
|
||||||
|
2 => {
|
||||||
|
if (right_limit - self.scrolling_region.left) > 1 {
|
||||||
|
// If we don't have space for the wide char, we need
|
||||||
|
// to insert spacers and wrap. Then we just print the wide
|
||||||
|
// char as normal.
|
||||||
|
if self.screen().cursor.x == right_limit - 1 {
|
||||||
|
// If we don't have wraparound enabled then we don't print
|
||||||
|
// this character at all and don't move the cursor. This is
|
||||||
|
// how xterm behaves.
|
||||||
|
if !self.modes.get(Mode::Wraparound) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only create a spacer head if we're at the real edge
|
||||||
|
// of the screen. Otherwise, we clear the space with a narrow.
|
||||||
|
// This allows soft wrapping to work correctly.
|
||||||
|
if right_limit == self.cols {
|
||||||
|
// Special-case: we need to set wrap to true even
|
||||||
|
// though we call print_wrap below because if there is
|
||||||
|
// a page resize during print_cell then it'll fail
|
||||||
|
// integrity checks.
|
||||||
|
self.screen_mut().cursor_row_mut().set_wrap(true);
|
||||||
|
self.print_cell(0, Wide::SpacerHead);
|
||||||
|
} else {
|
||||||
|
self.print_cell(0, Wide::Narrow);
|
||||||
|
}
|
||||||
|
self.print_wrap()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.screen_mut().cursor_mark_dirty();
|
||||||
|
self.print_cell(c, Wide::Wide);
|
||||||
|
self.screen_mut().cursor_right(1);
|
||||||
|
self.print_cell(0, Wide::SpacerTail);
|
||||||
|
} else {
|
||||||
|
// This is pretty broken, terminals should never be only 1-wide.
|
||||||
|
// We should prevent this downstream.
|
||||||
|
self.screen_mut().cursor_mark_dirty();
|
||||||
|
self.print_cell(0, Wide::Narrow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're at the column limit, then we need to wrap the next time.
|
||||||
|
// In this case, we don't move the cursor.
|
||||||
|
if self.screen().cursor.x == right_limit - 1 {
|
||||||
|
self.screen_mut().cursor.pending_wrap = true;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the cursor
|
||||||
|
self.screen_mut().cursor_right(1);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The grapheme-clustering section of `print`. Returns `true` if the
|
||||||
|
/// character was fully handled (Zig: `return`), `false` if printing should
|
||||||
|
/// continue on the normal path (Zig: `break :grapheme`).
|
||||||
|
fn print_grapheme(&mut self, c: u32, right_limit: size::CellCountInt) -> Result<bool, Error> {
|
||||||
|
// We need the previous cell to determine if we're at a grapheme
|
||||||
|
// break or not. If we are NOT, then we are still combining the
|
||||||
|
// same grapheme, and will be appending to prev.cell. Otherwise, we are
|
||||||
|
// in a new cell.
|
||||||
|
let wraparound = self.modes.get(Mode::Wraparound);
|
||||||
|
let (mut prev_left, mut prev_cell) = {
|
||||||
|
let scr = self.screen();
|
||||||
|
let left: size::CellCountInt = if wraparound {
|
||||||
|
// If we have wraparound, then we use the prev col unless
|
||||||
|
// there's a pending wrap, in which case we use the current.
|
||||||
|
size::CellCountInt::from(!scr.cursor.pending_wrap)
|
||||||
|
} else if scr.cursor.x != right_limit - 1 {
|
||||||
|
// If we do not have wraparound, the logic is trickier. If
|
||||||
|
// we're not on the last column, then we just use the previous
|
||||||
|
// column. Otherwise, we need to check if there is text to
|
||||||
|
// figure out if we're attaching to the prev or current.
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
size::CellCountInt::from(scr.cursor_cell().codepoint() == 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the previous cell is a wide spacer tail, then we actually
|
||||||
|
// want to use the cell before that because that has the actual
|
||||||
|
// content.
|
||||||
|
let immediate = scr.cursor_cell_left(left);
|
||||||
|
if immediate.wide() == Wide::SpacerTail {
|
||||||
|
(left + 1, scr.cursor_cell_left(left + 1))
|
||||||
|
} else {
|
||||||
|
(left, immediate)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If our cell has no content, then this is a new cell and
|
||||||
|
// necessarily a grapheme break.
|
||||||
|
if prev_cell.codepoint() == 0 {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let grapheme_break = {
|
||||||
|
let mut state = ghostty_unicode::BreakState::default();
|
||||||
|
let mut cp1 = prev_cell.codepoint();
|
||||||
|
if prev_cell.has_grapheme() {
|
||||||
|
let scr = self.screen();
|
||||||
|
let pin = scr.cursor_pin();
|
||||||
|
let page = scr.page(pin.node);
|
||||||
|
let idx = page
|
||||||
|
.cell_index(usize::from(scr.cursor.x - prev_left), usize::from(pin.y))
|
||||||
|
.expect("prev cell");
|
||||||
|
let cps = page.lookup_grapheme(idx).expect("grapheme data");
|
||||||
|
for &cp2 in cps {
|
||||||
|
let b = ghostty_unicode::grapheme_break(cp1, cp2, &mut state);
|
||||||
|
debug_assert!(!b);
|
||||||
|
cp1 = cp2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ghostty_unicode::grapheme_break(cp1, c, &mut state)
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we can NOT break, this means that "c" is part of a grapheme
|
||||||
|
// with the previous char.
|
||||||
|
if grapheme_break {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
enum DesiredWide {
|
||||||
|
NoChange,
|
||||||
|
Wide,
|
||||||
|
Narrow,
|
||||||
|
}
|
||||||
|
let mut desired_wide = DesiredWide::NoChange;
|
||||||
|
|
||||||
|
// The column of the previous cell on the cursor row.
|
||||||
|
let mut prev_x = self.screen().cursor.x - prev_left;
|
||||||
|
|
||||||
|
// If this is an emoji variation selector then we need to modify
|
||||||
|
// the cell width accordingly. VS16 makes the character wide and
|
||||||
|
// VS15 makes it narrow.
|
||||||
|
if c == 0xFE0F || c == 0xFE0E {
|
||||||
|
let prev_props = ghostty_unicode::table().get(prev_cell.codepoint());
|
||||||
|
// Check if it is a valid variation sequence in
|
||||||
|
// emoji-variation-sequences.txt, and if not, ignore the char.
|
||||||
|
if !prev_props.emoji_vs_base {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
desired_wide = if c == 0xFE0F {
|
||||||
|
DesiredWide::Wide
|
||||||
|
} else {
|
||||||
|
DesiredWide::Narrow
|
||||||
|
};
|
||||||
|
} else if !ghostty_unicode::table().get(c).width_zero_in_grapheme {
|
||||||
|
// If we have a code point that contributes to the width of a
|
||||||
|
// grapheme, it necessarily means that we're at least at width
|
||||||
|
// 2, since the first code point must be at least width 1 to
|
||||||
|
// start. (Note that Prepend code points could effectively mean
|
||||||
|
// the first code point should be width 0, but we don't handle
|
||||||
|
// that yet.)
|
||||||
|
desired_wide = DesiredWide::Wide;
|
||||||
|
}
|
||||||
|
|
||||||
|
match desired_wide {
|
||||||
|
DesiredWide::Wide => 'wide: {
|
||||||
|
if prev_cell.wide() == Wide::Wide {
|
||||||
|
break 'wide;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move our cursor back to the previous. We'll move
|
||||||
|
// the cursor within this block to the proper location.
|
||||||
|
self.screen_mut().cursor_left(prev_left);
|
||||||
|
|
||||||
|
// If we don't have space for the wide char, we need to
|
||||||
|
// insert spacers and wrap. We need special handling if the
|
||||||
|
// previous cell has grapheme data.
|
||||||
|
if self.screen().cursor.x == right_limit - 1 {
|
||||||
|
if !wraparound {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This path can write a spacer_head before print_wrap
|
||||||
|
// which can trigger integrity violations so mark
|
||||||
|
// the wrap first to keep the intermediary state valid
|
||||||
|
// if we're wrapping.
|
||||||
|
let row_wrap = right_limit == self.cols;
|
||||||
|
if row_wrap {
|
||||||
|
self.screen_mut().cursor_row_mut().set_wrap(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let prev_cp = prev_cell.codepoint();
|
||||||
|
if prev_cell.has_grapheme() {
|
||||||
|
// This is like print_cell but without clearing the
|
||||||
|
// grapheme data from the cell, so we can move it
|
||||||
|
// later.
|
||||||
|
{
|
||||||
|
let cell = self.screen_mut().cursor_cell_mut();
|
||||||
|
cell.set_wide(if row_wrap {
|
||||||
|
Wide::SpacerHead
|
||||||
|
} else {
|
||||||
|
Wide::Narrow
|
||||||
|
});
|
||||||
|
cell.set_codepoint(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.print_wrap()?;
|
||||||
|
self.print_cell(prev_cp, Wide::Wide);
|
||||||
|
|
||||||
|
let new_pin = self.screen().cursor_pin();
|
||||||
|
|
||||||
|
'transfer: {
|
||||||
|
let Some(mut old_pin) = new_pin.up(&self.screen().pages, 1) else {
|
||||||
|
break 'transfer;
|
||||||
|
};
|
||||||
|
old_pin.x = right_limit - 1;
|
||||||
|
let (old_x, old_y) = (usize::from(old_pin.x), usize::from(old_pin.y));
|
||||||
|
let (new_x, new_y) = (usize::from(new_pin.x), usize::from(new_pin.y));
|
||||||
|
|
||||||
|
if new_pin.node == old_pin.node {
|
||||||
|
let page = self.screen_mut().page_mut(new_pin.node);
|
||||||
|
let old_idx = page.cell_index(old_x, old_y).expect("old cell");
|
||||||
|
let new_idx = page.cell_index(new_x, new_y).expect("new cell");
|
||||||
|
page.move_grapheme(old_idx, new_idx)?;
|
||||||
|
page.cells[old_idx].set_content_tag(ContentTag::Codepoint);
|
||||||
|
page.cells[new_idx].set_content_tag(ContentTag::CodepointGrapheme);
|
||||||
|
page.rows[new_y].set_grapheme(true);
|
||||||
|
page.update_row_grapheme_flag(old_y);
|
||||||
|
} else {
|
||||||
|
let cps: Vec<u32> = {
|
||||||
|
let page = self.screen().page(old_pin.node);
|
||||||
|
let old_idx = page.cell_index(old_x, old_y).expect("old cell");
|
||||||
|
page.lookup_grapheme(old_idx).expect("grapheme").to_vec()
|
||||||
|
};
|
||||||
|
for cp in cps {
|
||||||
|
let x = self.screen().cursor.x;
|
||||||
|
self.screen_mut().append_grapheme(x, cp)?;
|
||||||
|
}
|
||||||
|
let page = self.screen_mut().page_mut(old_pin.node);
|
||||||
|
let old_idx = page.cell_index(old_x, old_y).expect("old cell");
|
||||||
|
page.clear_grapheme(old_idx);
|
||||||
|
page.update_row_grapheme_flag(old_y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point prev.cell to our new previous cell that
|
||||||
|
// we'll be appending graphemes to
|
||||||
|
prev_x = self.screen().cursor.x;
|
||||||
|
} else {
|
||||||
|
self.print_cell(
|
||||||
|
0,
|
||||||
|
if row_wrap {
|
||||||
|
Wide::SpacerHead
|
||||||
|
} else {
|
||||||
|
Wide::Narrow
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.print_wrap()?;
|
||||||
|
self.print_cell(prev_cp, Wide::Wide);
|
||||||
|
|
||||||
|
// Point prev.cell to our new previous cell that
|
||||||
|
// we'll be appending graphemes to
|
||||||
|
prev_x = self.screen().cursor.x;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.screen_mut().cursor_cell_mut().set_wide(Wide::Wide);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write our spacer, since prev.cell is now wide
|
||||||
|
self.screen_mut().cursor_right(1);
|
||||||
|
self.print_cell(0, Wide::SpacerTail);
|
||||||
|
|
||||||
|
// Move the cursor again so we're beyond our spacer
|
||||||
|
if self.screen().cursor.x == right_limit - 1 {
|
||||||
|
self.screen_mut().cursor.pending_wrap = true;
|
||||||
|
} else {
|
||||||
|
self.screen_mut().cursor_right(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DesiredWide::Narrow => 'narrow: {
|
||||||
|
// Prev cell is no longer wide
|
||||||
|
if prev_cell.wide() != Wide::Wide {
|
||||||
|
break 'narrow;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
let x = scr.cursor.x;
|
||||||
|
let pin = scr.cursor_pin();
|
||||||
|
let page = scr.page_mut(pin.node);
|
||||||
|
let idx = page
|
||||||
|
.cell_index(usize::from(x - prev_left), usize::from(pin.y))
|
||||||
|
.expect("prev cell");
|
||||||
|
page.cells[idx].set_wide(Wide::Narrow);
|
||||||
|
}
|
||||||
|
prev_cell.set_wide(Wide::Narrow);
|
||||||
|
|
||||||
|
// Remove the wide spacer tail
|
||||||
|
self.screen_mut()
|
||||||
|
.cursor_cell_left_mut(prev_left - 1)
|
||||||
|
.set_wide(Wide::Narrow);
|
||||||
|
|
||||||
|
// Back track the cursor so that we don't end up with
|
||||||
|
// an extra space after the character. Since xterm is
|
||||||
|
// not VS aware, it cannot be used as a reference for
|
||||||
|
// this behavior; but it does follow the principle of
|
||||||
|
// least surprise, and also matches the behavior that
|
||||||
|
// can be observed in Kitty, which is one of the only
|
||||||
|
// other VS aware terminals.
|
||||||
|
if self.screen().cursor.x == right_limit - 1 {
|
||||||
|
// If we're already at the right edge, we stay
|
||||||
|
// here and set the pending wrap to false since
|
||||||
|
// when we pend a wrap, we only move our cursor once
|
||||||
|
// even for wide chars (tests verify).
|
||||||
|
self.screen_mut().cursor.pending_wrap = false;
|
||||||
|
} else {
|
||||||
|
// Otherwise, move back.
|
||||||
|
self.screen_mut().cursor_left(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DesiredWide::NoChange => {}
|
||||||
|
}
|
||||||
|
let _ = &mut prev_left;
|
||||||
|
|
||||||
|
log::debug!(
|
||||||
|
"c={c:X} grapheme attach to left={prev_left} primary_cp={:X}",
|
||||||
|
prev_cell.codepoint()
|
||||||
|
);
|
||||||
|
self.screen_mut().cursor_mark_dirty();
|
||||||
|
self.screen_mut().append_grapheme(prev_x, c)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_cell(&mut self, unmapped_c: u32, wide: Wide) {
|
||||||
|
let cols = self.cols;
|
||||||
|
let scr = self.screens.active_mut();
|
||||||
|
|
||||||
|
// TODO: spacers should use a bgcolor only cell
|
||||||
|
|
||||||
|
let c: u32 = 'c: {
|
||||||
|
// TODO: non-utf8 handling, gr
|
||||||
|
|
||||||
|
// If we're single shifting, then we use the key exactly once.
|
||||||
|
let key = if let Some(key_once) = scr.charset.single_shift {
|
||||||
|
scr.charset.single_shift = None;
|
||||||
|
key_once
|
||||||
|
} else {
|
||||||
|
scr.charset.gl
|
||||||
|
};
|
||||||
|
|
||||||
|
let set = scr.charset.charsets.get(key);
|
||||||
|
|
||||||
|
// UTF-8 or ASCII is used as-is
|
||||||
|
if set == Charset::Utf8 || set == Charset::Ascii {
|
||||||
|
break 'c unmapped_c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're outside of ASCII range this is an invalid value in
|
||||||
|
// this table so we just return space.
|
||||||
|
if unmapped_c > u32::from(u8::MAX) {
|
||||||
|
break 'c u32::from(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get our lookup table and map it
|
||||||
|
let table = charsets::table(set);
|
||||||
|
u32::from(table[unmapped_c as usize])
|
||||||
|
};
|
||||||
|
|
||||||
|
let cell = scr.cursor_cell();
|
||||||
|
|
||||||
|
// If the wide property of this cell is the same, then we don't
|
||||||
|
// need to do the special handling here because the structure will
|
||||||
|
// be the same. If it is NOT the same, then we may need to clear some
|
||||||
|
// cells.
|
||||||
|
if cell.wide() != wide {
|
||||||
|
match cell.wide() {
|
||||||
|
// Previous cell was narrow. Do nothing.
|
||||||
|
Wide::Narrow => {}
|
||||||
|
|
||||||
|
// Previous cell was wide. We need to clear the tail and head.
|
||||||
|
Wide::Wide => 'wide: {
|
||||||
|
if scr.cursor.x >= cols - 1 {
|
||||||
|
break 'wide;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pin = scr.cursor_pin();
|
||||||
|
scr.clear_cells(
|
||||||
|
pin.node,
|
||||||
|
pin.y,
|
||||||
|
usize::from(scr.cursor.x) + 1,
|
||||||
|
usize::from(scr.cursor.x) + 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we're near the left edge, a wide char may have
|
||||||
|
// wrapped from the previous row, leaving a spacer_head
|
||||||
|
// at the end of that row. Clear it so the previous row
|
||||||
|
// doesn't keep a stale spacer_head.
|
||||||
|
if scr.cursor.y > 0 && scr.cursor.x <= 1 {
|
||||||
|
clear_prev_row_spacer_head(scr, cols);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Wide::SpacerTail => {
|
||||||
|
assert!(scr.cursor.x > 0);
|
||||||
|
|
||||||
|
// So integrity checks pass. We fix this up later so we don't
|
||||||
|
// need to do this without safety checks.
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
scr.cursor_cell_mut().set_wide(Wide::Narrow);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pin = scr.cursor_pin();
|
||||||
|
scr.clear_cells(
|
||||||
|
pin.node,
|
||||||
|
pin.y,
|
||||||
|
usize::from(scr.cursor.x) - 1,
|
||||||
|
usize::from(scr.cursor.x),
|
||||||
|
);
|
||||||
|
// If we're near the left edge, a wide char may have
|
||||||
|
// wrapped from the previous row, leaving a spacer_head
|
||||||
|
// at the end of that row. Clear it so the previous row
|
||||||
|
// doesn't keep a stale spacer_head.
|
||||||
|
if scr.cursor.y > 0 && scr.cursor.x <= 1 {
|
||||||
|
clear_prev_row_spacer_head(scr, cols);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this case was not handled in the old terminal implementation
|
||||||
|
// but it feels like we should do something. investigate other
|
||||||
|
// terminals (xterm mainly) and see what's up.
|
||||||
|
Wide::SpacerHead => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pin = scr.cursor_pin();
|
||||||
|
let (x, y) = (usize::from(pin.x), usize::from(pin.y));
|
||||||
|
let cursor_style_id = scr.cursor.style_id;
|
||||||
|
let cursor_protected = scr.cursor.protected;
|
||||||
|
let cursor_semantic = scr.cursor.semantic_content;
|
||||||
|
|
||||||
|
// Re-read the cell (clearing above may have changed it).
|
||||||
|
let page = scr.page_mut(pin.node);
|
||||||
|
let idx = page.cell_index(x, y).expect("cursor cell");
|
||||||
|
let cell = page.cells[idx];
|
||||||
|
|
||||||
|
// If the prior value had graphemes, clear those
|
||||||
|
if cell.has_grapheme() {
|
||||||
|
page.clear_grapheme(idx);
|
||||||
|
page.update_row_grapheme_flag(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't need to update the style refs unless the
|
||||||
|
// cell's new style will be different after writing.
|
||||||
|
let style_changed = cell.style_id() != cursor_style_id;
|
||||||
|
if style_changed {
|
||||||
|
// Release the old style.
|
||||||
|
if cell.style_id() != style::DEFAULT_ID {
|
||||||
|
assert!(page.rows[y].styled());
|
||||||
|
let _ = page.styles.release(cell.style_id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep track if we had a hyperlink so we can unset it.
|
||||||
|
let had_hyperlink = cell.hyperlink();
|
||||||
|
|
||||||
|
// Write
|
||||||
|
let mut new_cell = Cell::init(c);
|
||||||
|
new_cell.set_content_tag(ContentTag::Codepoint);
|
||||||
|
new_cell.set_style_id(cursor_style_id);
|
||||||
|
new_cell.set_wide(wide);
|
||||||
|
new_cell.set_protected(cursor_protected);
|
||||||
|
new_cell.set_semantic_content(cursor_semantic);
|
||||||
|
page.cells[idx] = new_cell;
|
||||||
|
|
||||||
|
if style_changed {
|
||||||
|
// Use the new style.
|
||||||
|
if cursor_style_id != style::DEFAULT_ID {
|
||||||
|
let _ = page.styles.r#use(cursor_style_id);
|
||||||
|
page.rows[y].set_styled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a Kitty unicode placeholder then we need to mark the
|
||||||
|
// row so that the renderer can lookup rows with these much faster.
|
||||||
|
if c == KITTY_PLACEHOLDER {
|
||||||
|
page.rows[y].set_kitty_virtual_placeholder(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We check for an active hyperlink first because setHyperlink
|
||||||
|
// handles clearing the old hyperlink and an optimization if we're
|
||||||
|
// overwriting the same hyperlink.
|
||||||
|
if scr.cursor.hyperlink_id > 0 {
|
||||||
|
if let Err(err) = scr.cursor_set_hyperlink() {
|
||||||
|
log::warn!(
|
||||||
|
"error reallocating for more hyperlink space, ignoring hyperlink err={err:?}"
|
||||||
|
);
|
||||||
|
debug_assert!(!scr.cursor_cell().hyperlink());
|
||||||
|
}
|
||||||
|
} else if had_hyperlink {
|
||||||
|
// If the previous cell had a hyperlink then we need to clear it.
|
||||||
|
let page = scr.page_mut(pin.node);
|
||||||
|
let _ = page.clear_hyperlink(idx);
|
||||||
|
page.update_row_hyperlink_flag(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
scr.assert_integrity();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_wrap(&mut self) -> Result<(), Error> {
|
||||||
|
// We only mark that we soft-wrapped if we're at the edge of our
|
||||||
|
// full screen. We don't mark the row as wrapped if we're in the
|
||||||
|
// middle due to a right margin.
|
||||||
|
let mark_wrap = self.screen().cursor.x == self.cols - 1;
|
||||||
|
if mark_wrap {
|
||||||
|
self.screen_mut().cursor_row_mut().set_wrap(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the old semantic prompt so we can extend it to the next
|
||||||
|
// line. We need to do this before we index() because we may
|
||||||
|
// modify memory.
|
||||||
|
let old_semantic = self.screen().cursor.semantic_content;
|
||||||
|
let old_semantic_clear = self.screen().cursor.semantic_content_clear_eol;
|
||||||
|
|
||||||
|
// Move to the next line
|
||||||
|
self.index()?;
|
||||||
|
let left = self.scrolling_region.left;
|
||||||
|
self.screen_mut().cursor_horizontal_absolute(left);
|
||||||
|
|
||||||
|
// We always reset our semantic prompt state
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
scr.cursor.semantic_content = old_semantic;
|
||||||
|
scr.cursor.semantic_content_clear_eol = old_semantic_clear;
|
||||||
|
match old_semantic {
|
||||||
|
SemanticContent::Output | SemanticContent::Input => {}
|
||||||
|
SemanticContent::Prompt => scr
|
||||||
|
.cursor_row_mut()
|
||||||
|
.set_semantic_prompt(RowPrompt::PromptContinuation),
|
||||||
|
}
|
||||||
|
|
||||||
|
if mark_wrap {
|
||||||
|
// Always mark the row as a continuation
|
||||||
|
scr.cursor_row_mut().set_wrap_continuation(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assure that our screen is consistent
|
||||||
|
scr.assert_integrity();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the charset into the given slot.
|
||||||
|
pub fn configure_charset(&mut self, slot: Slots, set: Charset) {
|
||||||
|
self.screen_mut().charset.charsets.set(slot, set);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invoke the charset in slot into the active slot. If single is true,
|
||||||
|
/// then this will only be invoked for a single character.
|
||||||
|
pub fn invoke_charset(&mut self, active: charsets::ActiveSlot, slot: Slots, single: bool) {
|
||||||
|
if single {
|
||||||
|
assert!(active == charsets::ActiveSlot::GL);
|
||||||
|
self.screen_mut().charset.single_shift = Some(slot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match active {
|
||||||
|
charsets::ActiveSlot::GL => self.screen_mut().charset.gl = slot,
|
||||||
|
charsets::ActiveSlot::GR => self.screen_mut().charset.gr = slot,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carriage return moves the cursor to the first column.
|
||||||
|
pub fn carriage_return(&mut self) {
|
||||||
|
// Always reset pending wrap state
|
||||||
|
self.screen_mut().cursor.pending_wrap = false;
|
||||||
|
|
||||||
|
// In origin mode we always move to the left margin
|
||||||
|
let x = if self.modes.get(Mode::Origin)
|
||||||
|
|| self.screen().cursor.x >= self.scrolling_region.left
|
||||||
|
{
|
||||||
|
self.scrolling_region.left
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
self.screen_mut().cursor_horizontal_absolute(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linefeed moves the cursor to the next line.
|
||||||
|
pub fn linefeed(&mut self) -> Result<(), Error> {
|
||||||
|
self.index()?;
|
||||||
|
if self.modes.get(Mode::Linefeed) {
|
||||||
|
self.carriage_return();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Backspace moves the cursor back a column (but not less than 0).
|
||||||
|
pub fn backspace(&mut self) {
|
||||||
|
self.cursor_left(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor up amount lines. If amount is greater than the maximum
|
||||||
|
/// move distance then it is internally adjusted to the maximum. If amount is
|
||||||
|
/// 0, adjust it to 1.
|
||||||
|
pub fn cursor_up(&mut self, count_req: usize) {
|
||||||
|
// Always resets pending wrap
|
||||||
|
self.screen_mut().cursor.pending_wrap = false;
|
||||||
|
|
||||||
|
// The maximum amount the cursor can move up depends on scrolling regions
|
||||||
|
let y = self.screen().cursor.y;
|
||||||
|
let max = if y >= self.scrolling_region.top {
|
||||||
|
y - self.scrolling_region.top
|
||||||
|
} else {
|
||||||
|
y
|
||||||
|
};
|
||||||
|
let count = usize::from(max).min(count_req.max(1));
|
||||||
|
|
||||||
|
// We can safely cast below because of the min/max clamping we did above.
|
||||||
|
self.screen_mut().cursor_up(count as size::CellCountInt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor down amount lines. If amount is greater than the maximum
|
||||||
|
/// move distance then it is internally adjusted to the maximum. This sequence
|
||||||
|
/// will not scroll the screen or scroll region. If amount is 0, adjust it to 1.
|
||||||
|
pub fn cursor_down(&mut self, count_req: usize) {
|
||||||
|
// Always resets pending wrap
|
||||||
|
self.screen_mut().cursor.pending_wrap = false;
|
||||||
|
|
||||||
|
// The max the cursor can move to depends where the cursor currently is
|
||||||
|
let y = self.screen().cursor.y;
|
||||||
|
let max = if y <= self.scrolling_region.bottom {
|
||||||
|
self.scrolling_region.bottom - y
|
||||||
|
} else {
|
||||||
|
self.rows - y - 1
|
||||||
|
};
|
||||||
|
let count = usize::from(max).min(count_req.max(1));
|
||||||
|
self.screen_mut().cursor_down(count as size::CellCountInt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor right amount columns. If amount is greater than the
|
||||||
|
/// maximum move distance then it is internally adjusted to the maximum.
|
||||||
|
/// This sequence will not scroll the screen or scroll region. If amount is
|
||||||
|
/// 0, adjust it to 1.
|
||||||
|
pub fn cursor_right(&mut self, count_req: usize) {
|
||||||
|
// Always resets pending wrap
|
||||||
|
self.screen_mut().cursor.pending_wrap = false;
|
||||||
|
|
||||||
|
// The max the cursor can move to depends where the cursor currently is
|
||||||
|
let x = self.screen().cursor.x;
|
||||||
|
let max = if x <= self.scrolling_region.right {
|
||||||
|
self.scrolling_region.right - x
|
||||||
|
} else {
|
||||||
|
self.cols - x - 1
|
||||||
|
};
|
||||||
|
let count = usize::from(max).min(count_req.max(1));
|
||||||
|
self.screen_mut().cursor_right(count as size::CellCountInt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the left amount cells. If amount is 0, adjust it to 1.
|
||||||
|
pub fn cursor_left(&mut self, count_req: usize) {
|
||||||
|
// Wrapping behavior depends on various terminal modes
|
||||||
|
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||||
|
enum WrapMode {
|
||||||
|
None,
|
||||||
|
Reverse,
|
||||||
|
ReverseExtended,
|
||||||
|
}
|
||||||
|
let wrap_mode = if !self.modes.get(Mode::Wraparound) {
|
||||||
|
WrapMode::None
|
||||||
|
} else if self.modes.get(Mode::ReverseWrapExtended) {
|
||||||
|
WrapMode::ReverseExtended
|
||||||
|
} else if self.modes.get(Mode::ReverseWrap) {
|
||||||
|
WrapMode::Reverse
|
||||||
|
} else {
|
||||||
|
WrapMode::None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut count = count_req.max(1);
|
||||||
|
|
||||||
|
// If we are in no wrap mode, then we move the cursor left and exit
|
||||||
|
// since this is the fastest and most typical path.
|
||||||
|
if wrap_mode == WrapMode::None {
|
||||||
|
let x = self.screen().cursor.x;
|
||||||
|
self.screen_mut()
|
||||||
|
.cursor_left(count.min(usize::from(x)) as size::CellCountInt);
|
||||||
|
self.screen_mut().cursor.pending_wrap = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a pending wrap state and we are in either reverse wrap
|
||||||
|
// modes then we decrement the amount we move by one to match xterm.
|
||||||
|
if self.screen().cursor.pending_wrap {
|
||||||
|
count -= 1;
|
||||||
|
self.screen_mut().cursor.pending_wrap = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The margins we can move to.
|
||||||
|
let top = self.scrolling_region.top;
|
||||||
|
let bottom = self.scrolling_region.bottom;
|
||||||
|
let right_margin = self.scrolling_region.right;
|
||||||
|
let left_margin = if self.screen().cursor.x < self.scrolling_region.left {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
self.scrolling_region.left
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle some edge cases when our cursor is already on the left margin.
|
||||||
|
if self.screen().cursor.x == left_margin {
|
||||||
|
match wrap_mode {
|
||||||
|
// In reverse mode, if we're already before the top margin
|
||||||
|
// then we just set our cursor to the top-left and we're done.
|
||||||
|
WrapMode::Reverse => {
|
||||||
|
if self.screen().cursor.y <= top {
|
||||||
|
self.screen_mut().cursor_absolute(left_margin, top);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handled in while loop
|
||||||
|
WrapMode::ReverseExtended => {}
|
||||||
|
|
||||||
|
// Handled above
|
||||||
|
WrapMode::None => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// We can move at most to the left margin.
|
||||||
|
let max = usize::from(self.screen().cursor.x - left_margin);
|
||||||
|
|
||||||
|
// We want to move at most the number of columns we have left
|
||||||
|
// or our remaining count. Do the move.
|
||||||
|
let amount = max.min(count);
|
||||||
|
count -= amount;
|
||||||
|
self.screen_mut().cursor_left(amount as size::CellCountInt);
|
||||||
|
|
||||||
|
// If we have no more to move, then we're done.
|
||||||
|
if count == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are at the top, then we are done.
|
||||||
|
if self.screen().cursor.y == top {
|
||||||
|
if wrap_mode != WrapMode::ReverseExtended {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.screen_mut().cursor_absolute(right_margin, bottom);
|
||||||
|
count -= 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UNDEFINED TERMINAL BEHAVIOR. This situation is not handled in xterm
|
||||||
|
// and currently results in a crash in xterm. Given no other known
|
||||||
|
// terminal [to me] implements XTREVWRAP2, I decided to just mimic
|
||||||
|
// the behavior of xterm up and not including the crash by wrapping
|
||||||
|
// up to the (0, 0) and stopping there. My reasoning is that for an
|
||||||
|
// appropriately sized value of "count" this is the behavior that xterm
|
||||||
|
// would have. This is unit tested.
|
||||||
|
if self.screen().cursor.y == 0 {
|
||||||
|
assert!(self.screen().cursor.x == left_margin);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our previous line is not wrapped then we are done.
|
||||||
|
if wrap_mode != WrapMode::ReverseExtended {
|
||||||
|
let prev_row = self.screen().cursor_row_up(1);
|
||||||
|
if !prev_row.wrap() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let y = self.screen().cursor.y - 1;
|
||||||
|
self.screen_mut().cursor_absolute(right_margin, y);
|
||||||
|
count -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save cursor position and further state.
|
||||||
|
///
|
||||||
|
/// The primary and alternate screen have distinct save state. One saved state
|
||||||
|
/// is kept per screen (main / alternative). If for the current screen state
|
||||||
|
/// was already saved it is overwritten.
|
||||||
|
pub fn save_cursor(&mut self) {
|
||||||
|
let origin = self.modes.get(Mode::Origin);
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
scr.saved_cursor = Some(SavedCursor {
|
||||||
|
x: scr.cursor.x,
|
||||||
|
y: scr.cursor.y,
|
||||||
|
style: scr.cursor.style,
|
||||||
|
protected: scr.cursor.protected,
|
||||||
|
pending_wrap: scr.cursor.pending_wrap,
|
||||||
|
origin,
|
||||||
|
charset: scr.charset,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore cursor position and other state.
|
||||||
|
///
|
||||||
|
/// The primary and alternate screen have distinct save state.
|
||||||
|
/// If no save was done before values are reset to their initial values.
|
||||||
|
pub fn restore_cursor(&mut self) {
|
||||||
|
let saved = self.screen().saved_cursor.unwrap_or(SavedCursor {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
style: Style::default(),
|
||||||
|
protected: false,
|
||||||
|
pending_wrap: false,
|
||||||
|
origin: false,
|
||||||
|
charset: CharsetState::default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the style first because it can fail
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
scr.cursor.style = saved.style;
|
||||||
|
if let Err(err) = scr.manual_style_update() {
|
||||||
|
// Regardless of the error here, we revert back to an unstyled
|
||||||
|
// cursor. It is more important that the restore succeeds in
|
||||||
|
// other attributes because terminals have no way to communicate
|
||||||
|
// failure back.
|
||||||
|
log::warn!("restoreCursor error updating style err={err:?}");
|
||||||
|
scr.cursor.style = Style::default();
|
||||||
|
scr.manual_style_update().expect("default style update");
|
||||||
|
}
|
||||||
|
|
||||||
|
scr.charset = saved.charset;
|
||||||
|
scr.cursor.pending_wrap = saved.pending_wrap;
|
||||||
|
scr.cursor.protected = saved.protected;
|
||||||
|
let (cols, rows) = (self.cols, self.rows);
|
||||||
|
self.modes.set(Mode::Origin, saved.origin);
|
||||||
|
self.screen_mut()
|
||||||
|
.cursor_absolute(saved.x.min(cols - 1), saved.y.min(rows - 1));
|
||||||
|
|
||||||
|
// Ensure our screen is consistent
|
||||||
|
self.screen().assert_integrity();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the character protection mode for the terminal.
|
||||||
|
pub fn set_protected_mode(&mut self, mode: ProtectedMode) {
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
match mode {
|
||||||
|
ProtectedMode::Off => {
|
||||||
|
scr.cursor.protected = false;
|
||||||
|
|
||||||
|
// screen.protected_mode is NEVER reset to ".off" because
|
||||||
|
// logic such as eraseChars depends on knowing what the
|
||||||
|
// _most recent_ mode was.
|
||||||
|
}
|
||||||
|
|
||||||
|
ProtectedMode::Iso => {
|
||||||
|
scr.cursor.protected = true;
|
||||||
|
scr.protected_mode = ProtectedMode::Iso;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProtectedMode::Dec => {
|
||||||
|
scr.cursor.protected = true;
|
||||||
|
scr.protected_mode = ProtectedMode::Dec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform a semantic prompt command.
|
||||||
|
///
|
||||||
|
/// If there is an error, we do our best to get the terminal into
|
||||||
|
/// some coherent state, since callers typically can't handle errors
|
||||||
|
/// (since they're sending sequences via the pty).
|
||||||
|
pub fn semantic_prompt(&mut self, cmd: &osc::SemanticPrompt) -> Result<(), Error> {
|
||||||
|
match cmd.action {
|
||||||
|
PromptAction::FreshLine => self.semantic_prompt_fresh_line()?,
|
||||||
|
|
||||||
|
PromptAction::FreshLineNewPrompt => {
|
||||||
|
// "First do a fresh-line."
|
||||||
|
self.semantic_prompt_fresh_line()?;
|
||||||
|
|
||||||
|
// "Subsequent text (until a OSC "133;B" or OSC "133;I" command)
|
||||||
|
// is a prompt string (as if followed by OSC 133;P;k=i\007)."
|
||||||
|
let kind = cmd
|
||||||
|
.prompt_kind()
|
||||||
|
.unwrap_or(osc::semantic_prompt::PromptKind::Initial);
|
||||||
|
self.screen_mut()
|
||||||
|
.cursor_set_semantic_content(SetSemantic::Prompt(kind));
|
||||||
|
|
||||||
|
// This is a kitty-specific flag that notes that the shell
|
||||||
|
// is NOT capable of redraw. Redraw defaults to true so this
|
||||||
|
// usually just disables it, but either is possible.
|
||||||
|
if let Some(v) = cmd.redraw() {
|
||||||
|
self.flags.shell_redraws_prompt = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
'click: {
|
||||||
|
// Handle click_events as a priority over cl. click_events
|
||||||
|
// is another Kitty-specific extension that converts clicks
|
||||||
|
// within a prompt area to SGR mouse events and defers to the
|
||||||
|
// shell to handle them.
|
||||||
|
if let Some(v) = cmd.click_events()
|
||||||
|
&& v
|
||||||
|
{
|
||||||
|
self.screen_mut().semantic_prompt.click = SemanticClick::ClickEvents;
|
||||||
|
break 'click;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If click_events was not set or disabled, fallback to `cl`.
|
||||||
|
if let Some(v) = cmd.cl() {
|
||||||
|
self.screen_mut().semantic_prompt.click = SemanticClick::Cl(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "aid" and "cl" options are also valid for this
|
||||||
|
// command but we don't yet handle these in any meaningful way.
|
||||||
|
}
|
||||||
|
|
||||||
|
PromptAction::NewCommand => {
|
||||||
|
// Spec:
|
||||||
|
// Same as OSC "133;A" but may first implicitly terminate a
|
||||||
|
// previous command: if the options specify an aid and there
|
||||||
|
// is an active (open) command with matching aid, finish the
|
||||||
|
// innermost such command (as well as any other commands
|
||||||
|
// nested more deeply). If no aid is specified, treat as an
|
||||||
|
// aid whose value is the empty string.
|
||||||
|
|
||||||
|
// Ghostty:
|
||||||
|
// We don't currently do explicit command tracking in any way
|
||||||
|
// so there is no need to terminate prior commands. We just
|
||||||
|
// perform the `A` action.
|
||||||
|
self.semantic_prompt(&osc::SemanticPrompt {
|
||||||
|
action: PromptAction::FreshLineNewPrompt,
|
||||||
|
options_unvalidated: cmd.options_unvalidated.clone(),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
PromptAction::PromptStart => {
|
||||||
|
// Explicit start of prompt. Optional after an A or N command.
|
||||||
|
// The k (kind) option specifies the type of prompt:
|
||||||
|
// regular primary prompt (k=i or default),
|
||||||
|
// right-side prompts (k=r), or prompts for continuation lines (k=c or k=s).
|
||||||
|
let kind = cmd
|
||||||
|
.prompt_kind()
|
||||||
|
.unwrap_or(osc::semantic_prompt::PromptKind::Initial);
|
||||||
|
self.screen_mut()
|
||||||
|
.cursor_set_semantic_content(SetSemantic::Prompt(kind));
|
||||||
|
}
|
||||||
|
|
||||||
|
PromptAction::EndPromptStartInput => {
|
||||||
|
// End of prompt and start of user input, terminated by a OSC
|
||||||
|
// "133;C" or another prompt (OSC "133;P").
|
||||||
|
self.screen_mut()
|
||||||
|
.cursor_set_semantic_content(SetSemantic::Input(InputClear::ClearExplicit));
|
||||||
|
}
|
||||||
|
|
||||||
|
PromptAction::EndPromptStartInputTerminateEol => {
|
||||||
|
// End of prompt and start of user input, terminated by end-of-line.
|
||||||
|
self.screen_mut()
|
||||||
|
.cursor_set_semantic_content(SetSemantic::Input(InputClear::ClearEol));
|
||||||
|
}
|
||||||
|
|
||||||
|
PromptAction::EndInputStartOutput => {
|
||||||
|
// "End of input, and start of output."
|
||||||
|
self.screen_mut()
|
||||||
|
.cursor_set_semantic_content(SetSemantic::Output);
|
||||||
|
|
||||||
|
// If our current row is marked as a prompt and we're
|
||||||
|
// at column zero then we assume we're un-prompting. This
|
||||||
|
// is a heuristic to deal with fish, mostly. The issue that
|
||||||
|
// fish brings up is that it has no PS2 equivalent and its
|
||||||
|
// builtin OSC133 marking doesn't output continuation lines
|
||||||
|
// as k=s. So, we assume when we get a newline with a prompt
|
||||||
|
// cursor that the new line is also a prompt. But fish changes
|
||||||
|
// to output on the newline. So if we're at col 0 we just assume
|
||||||
|
// we're overwriting the prompt.
|
||||||
|
if self.screen().cursor_row().semantic_prompt() != RowPrompt::None
|
||||||
|
&& self.screen().cursor.x == 0
|
||||||
|
{
|
||||||
|
self.screen_mut()
|
||||||
|
.cursor_row_mut()
|
||||||
|
.set_semantic_prompt(RowPrompt::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PromptAction::EndCommand => {
|
||||||
|
// From a terminal state perspective, this doesn't really do
|
||||||
|
// anything. Other terminals appear to do nothing here. I think
|
||||||
|
// its reasonable at this point to reset our semantic content
|
||||||
|
// state but the spec doesn't really say what to do.
|
||||||
|
self.screen_mut()
|
||||||
|
.cursor_set_semantic_content(SetSemantic::Output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// OSC 133;L
|
||||||
|
fn semantic_prompt_fresh_line(&mut self) -> Result<(), Error> {
|
||||||
|
let left_margin = if self.screen().cursor.x < self.scrolling_region.left {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
self.scrolling_region.left
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spec: "If the cursor is the initial column (left, assuming
|
||||||
|
// left-to-right writing), do nothing" This specification is very under
|
||||||
|
// specified. We are taking the liberty to assume that in a left/right
|
||||||
|
// margin context, if the cursor is outside of the left margin, we treat
|
||||||
|
// it as being at the left margin for the purposes of this command.
|
||||||
|
// This is arbitrary. If someone has a better reasonable idea we can
|
||||||
|
// apply it.
|
||||||
|
if self.screen().cursor.x == left_margin {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.carriage_return();
|
||||||
|
self.index()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the cursor is currently at a prompt. Another way to look
|
||||||
|
/// at this is it returns false if the shell is currently outputting something.
|
||||||
|
/// This requires shell integration (semantic prompt integration).
|
||||||
|
///
|
||||||
|
/// If the shell integration doesn't exist, this will always return false.
|
||||||
|
pub fn cursor_is_at_prompt(&self) -> bool {
|
||||||
|
// If we're on the secondary screen, we're never at a prompt.
|
||||||
|
if self.screens.active_key == ScreenKey::Alternate {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our page row is a prompt then we're always at a prompt
|
||||||
|
let scr = self.screen();
|
||||||
|
if scr.cursor_row().semantic_prompt() != RowPrompt::None {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, determine our cursor state
|
||||||
|
match scr.cursor.semantic_content {
|
||||||
|
SemanticContent::Input | SemanticContent::Prompt => true,
|
||||||
|
SemanticContent::Output => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Horizontal tab moves the cursor to the next tabstop, clearing
|
||||||
|
/// the screen to the left the tabstop.
|
||||||
|
pub fn horizontal_tab(&mut self) {
|
||||||
|
while self.screen().cursor.x < self.scrolling_region.right {
|
||||||
|
// Move the cursor right
|
||||||
|
self.screen_mut().cursor_right(1);
|
||||||
|
|
||||||
|
// If the last cursor position was a tabstop we return. We do
|
||||||
|
// "last cursor position" because we want a space to be written
|
||||||
|
// at the tabstop unless we're at the end (the while condition).
|
||||||
|
if self.tabstops.get(usize::from(self.screen().cursor.x)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as horizontal_tab but moves to the previous tabstop instead of the next.
|
||||||
|
pub fn horizontal_tab_back(&mut self) {
|
||||||
|
// With origin mode enabled, our leftmost limit is the left margin.
|
||||||
|
let left_limit = if self.modes.get(Mode::Origin) {
|
||||||
|
self.scrolling_region.left
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// If we're already at the edge of the screen, then we're done.
|
||||||
|
if self.screen().cursor.x <= left_limit {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the cursor left
|
||||||
|
self.screen_mut().cursor_left(1);
|
||||||
|
if self.tabstops.get(usize::from(self.screen().cursor.x)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear tab stops.
|
||||||
|
pub fn tab_clear(&mut self, cmd: csi::TabClear) {
|
||||||
|
match cmd {
|
||||||
|
csi::TabClear::Current => self.tabstops.unset(usize::from(self.screen().cursor.x)),
|
||||||
|
csi::TabClear::All => self.tabstops.reset(0),
|
||||||
|
other @ csi::TabClear::Other(_) => {
|
||||||
|
log::warn!("invalid or unknown tab clear setting: {other:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a tab stop on the current cursor.
|
||||||
|
pub fn tab_set(&mut self) {
|
||||||
|
self.tabstops.set(usize::from(self.screen().cursor.x));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tab_reset(&mut self) {
|
||||||
|
self.tabstops.reset(TABSTOP_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the next line in the scrolling region, possibly scrolling.
|
||||||
|
///
|
||||||
|
/// If the cursor is outside of the scrolling region: move the cursor one line
|
||||||
|
/// down if it is not on the bottom-most line of the screen.
|
||||||
|
///
|
||||||
|
/// If the cursor is inside the scrolling region:
|
||||||
|
/// If the cursor is on the bottom-most line of the scrolling region:
|
||||||
|
/// invoke scroll up with amount=1
|
||||||
|
/// If the cursor is not on the bottom-most line of the scrolling region:
|
||||||
|
/// move the cursor one line down
|
||||||
|
///
|
||||||
|
/// This unsets the pending wrap state without wrapping.
|
||||||
|
#[allow(clippy::if_not_else)]
|
||||||
|
pub fn index(&mut self) -> Result<(), Error> {
|
||||||
|
// Unset pending wrap state
|
||||||
|
self.screen_mut().cursor.pending_wrap = false;
|
||||||
|
|
||||||
|
// We handle our cursor semantic prompt state AFTER doing the
|
||||||
|
// scrolling, because we may need to apply to new rows.
|
||||||
|
let result = self.index_inner();
|
||||||
|
|
||||||
|
let screen = self.screen_mut();
|
||||||
|
if screen.cursor.semantic_content != SemanticContent::Output {
|
||||||
|
// Always reset any semantic content clear-eol state.
|
||||||
|
//
|
||||||
|
// The specification is not clear what "end-of-line" means. If we
|
||||||
|
// discover that there are more scenarios we should be unsetting
|
||||||
|
// this we should document and test it.
|
||||||
|
if screen.cursor.semantic_content_clear_eol {
|
||||||
|
screen.cursor.semantic_content = SemanticContent::Output;
|
||||||
|
screen.cursor.semantic_content_clear_eol = false;
|
||||||
|
} else {
|
||||||
|
// If we aren't clearing our state at EOL and we're not output,
|
||||||
|
// then we mark the new row as a prompt continuation. This is
|
||||||
|
// to work around shells that don't send OSC 133 k=s sequences
|
||||||
|
// for continuations.
|
||||||
|
//
|
||||||
|
// This can be a false positive if the shell changes content
|
||||||
|
// type later and outputs something. We handle that in the
|
||||||
|
// semanticPrompt function.
|
||||||
|
screen
|
||||||
|
.cursor_row_mut()
|
||||||
|
.set_semantic_prompt(RowPrompt::PromptContinuation);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This should never be set in the output mode.
|
||||||
|
assert!(!screen.cursor.semantic_content_clear_eol);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index_inner(&mut self) -> Result<(), Error> {
|
||||||
|
let rows = self.rows;
|
||||||
|
let cols = self.cols;
|
||||||
|
let sr = self.scrolling_region;
|
||||||
|
let screen = self.screens.active_mut();
|
||||||
|
|
||||||
|
// Outside of the scroll region we move the cursor one line down.
|
||||||
|
if screen.cursor.y < sr.top || screen.cursor.y > sr.bottom {
|
||||||
|
// We only move down if we're not already at the bottom of
|
||||||
|
// the screen.
|
||||||
|
if screen.cursor.y < rows - 1 {
|
||||||
|
screen.cursor_down(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the cursor is inside the scrolling region and on the bottom-most
|
||||||
|
// line, then we scroll up. If our scrolling region is the full screen
|
||||||
|
// we create scrollback.
|
||||||
|
if screen.cursor.y == sr.bottom && screen.cursor.x >= sr.left && screen.cursor.x <= sr.right
|
||||||
|
{
|
||||||
|
// Scrolling dirties the images because it updates their placements pins.
|
||||||
|
screen.kitty_images.dirty = true;
|
||||||
|
|
||||||
|
// If our scrolling region is at the top, we create scrollback.
|
||||||
|
if sr.top == 0 && sr.left == 0 && sr.right == cols - 1 {
|
||||||
|
screen.cursor_scroll_above()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow path for left and right scrolling region margins.
|
||||||
|
if sr.left != 0
|
||||||
|
|| sr.right != cols - 1
|
||||||
|
// PERF(mitchellh): If we have an SGR background set then
|
||||||
|
// we need to preserve that background in our erased rows.
|
||||||
|
// scrollUp does that but eraseRowBounded below does not.
|
||||||
|
// However, scrollUp is WAY slower. We should optimize this
|
||||||
|
// case to work in the eraseRowBounded codepath and remove
|
||||||
|
// this check.
|
||||||
|
|| !screen.blank_cell().is_zero()
|
||||||
|
{
|
||||||
|
return self.scroll_up(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise use a fast path function from PageList to efficiently
|
||||||
|
// scroll the contents of the scrolling region.
|
||||||
|
|
||||||
|
// Preserve old cursor just for assertions
|
||||||
|
let (old_x, old_y) = (screen.cursor.x, screen.cursor.y);
|
||||||
|
|
||||||
|
screen.pages.erase_row_bounded(
|
||||||
|
Point::Active(Coordinate {
|
||||||
|
x: 0,
|
||||||
|
y: u32::from(sr.top),
|
||||||
|
}),
|
||||||
|
usize::from(sr.bottom - sr.top),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// eraseRow and eraseRowBounded will end up moving the cursor pin
|
||||||
|
// up by 1, so we need to move it back down. A `cursorReload`
|
||||||
|
// would be better option but this is more efficient and this is
|
||||||
|
// a super hot path so we do this instead.
|
||||||
|
assert!(screen.cursor.x == old_x);
|
||||||
|
assert!(screen.cursor.y == old_y);
|
||||||
|
screen.cursor.y -= 1;
|
||||||
|
screen.cursor_down(1);
|
||||||
|
|
||||||
|
// The operations above can prune our cursor style so we need to
|
||||||
|
// update. This should never fail because the above can only FREE
|
||||||
|
// memory.
|
||||||
|
if let Err(err) = screen.manual_style_update() {
|
||||||
|
log::warn!("deleteLines manualStyleUpdate err={err:?}");
|
||||||
|
screen.cursor.style = Style::default();
|
||||||
|
screen.manual_style_update().expect("default style update");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase cursor by 1, maximum to bottom of scroll region
|
||||||
|
if screen.cursor.y < sr.bottom {
|
||||||
|
screen.cursor_down(1);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the previous line in the scrolling region, possibly
|
||||||
|
/// scrolling.
|
||||||
|
///
|
||||||
|
/// If the cursor is outside of the scrolling region, move the cursor one
|
||||||
|
/// line up if it is not on the top-most line of the screen.
|
||||||
|
///
|
||||||
|
/// If the cursor is inside the scrolling region:
|
||||||
|
///
|
||||||
|
/// * If the cursor is on the top-most line of the scrolling region:
|
||||||
|
/// invoke scroll down with amount=1
|
||||||
|
/// * If the cursor is not on the top-most line of the scrolling region:
|
||||||
|
/// move the cursor one line up
|
||||||
|
pub fn reverse_index(&mut self) {
|
||||||
|
let scr = self.screen();
|
||||||
|
if scr.cursor.y != self.scrolling_region.top
|
||||||
|
|| scr.cursor.x < self.scrolling_region.left
|
||||||
|
|| scr.cursor.x > self.scrolling_region.right
|
||||||
|
{
|
||||||
|
self.cursor_up(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scroll_down(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set Cursor Position. Move cursor to the position indicated
|
||||||
|
/// by row and column (1-indexed). If column is 0, it is adjusted to 1.
|
||||||
|
/// If column is greater than the right-most column it is adjusted to
|
||||||
|
/// the right-most column. If row is 0, it is adjusted to 1. If row is
|
||||||
|
/// greater than the bottom-most row it is adjusted to the bottom-most
|
||||||
|
/// row.
|
||||||
|
pub fn set_cursor_pos(&mut self, row_req: usize, col_req: usize) {
|
||||||
|
// If cursor origin mode is set the cursor row will be moved relative to
|
||||||
|
// the top margin row and adjusted to be above or at bottom-most row in
|
||||||
|
// the current scroll region.
|
||||||
|
//
|
||||||
|
// If origin mode is set and left and right margin mode is set the cursor
|
||||||
|
// will be moved relative to the left margin column and adjusted to be on
|
||||||
|
// or left of the right margin column.
|
||||||
|
let (x_offset, y_offset, x_max, y_max): (usize, usize, usize, usize) =
|
||||||
|
if self.modes.get(Mode::Origin) {
|
||||||
|
(
|
||||||
|
usize::from(self.scrolling_region.left),
|
||||||
|
usize::from(self.scrolling_region.top),
|
||||||
|
usize::from(self.scrolling_region.right) + 1, // We need this 1-indexed
|
||||||
|
usize::from(self.scrolling_region.bottom) + 1, // We need this 1-indexed
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(0, 0, usize::from(self.cols), usize::from(self.rows))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unset pending wrap state
|
||||||
|
self.screen_mut().cursor.pending_wrap = false;
|
||||||
|
|
||||||
|
// Calculate our new x/y
|
||||||
|
let row = if row_req == 0 { 1 } else { row_req };
|
||||||
|
let col = if col_req == 0 { 1 } else { col_req };
|
||||||
|
let x = x_max.min(col.saturating_add(x_offset)).saturating_sub(1) as size::CellCountInt;
|
||||||
|
let y = y_max.min(row.saturating_add(y_offset)).saturating_sub(1) as size::CellCountInt;
|
||||||
|
|
||||||
|
// If the y is unchanged then this is fast pointer math
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
if y == scr.cursor.y {
|
||||||
|
if x > scr.cursor.x {
|
||||||
|
scr.cursor_right(x - scr.cursor.x);
|
||||||
|
} else {
|
||||||
|
scr.cursor_left(scr.cursor.x - x);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If everything changed we do an absolute change which is slightly slower
|
||||||
|
scr.cursor_absolute(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set Top and Bottom Margins If bottom is not specified, 0 or bigger than
|
||||||
|
/// the number of the bottom-most row, it is adjusted to the number of the
|
||||||
|
/// bottom most row.
|
||||||
|
///
|
||||||
|
/// If top < bottom set the top and bottom row of the scroll region according
|
||||||
|
/// to top and bottom and move the cursor to the top-left cell of the display
|
||||||
|
/// (when in cursor origin mode is set to the top-left cell of the scroll region).
|
||||||
|
///
|
||||||
|
/// Otherwise: Set the top and bottom row of the scroll region to the top-most
|
||||||
|
/// and bottom-most line of the screen.
|
||||||
|
///
|
||||||
|
/// Top and bottom are 1-indexed.
|
||||||
|
pub fn set_top_and_bottom_margin(&mut self, top_req: usize, bottom_req: usize) {
|
||||||
|
let top = top_req.max(1);
|
||||||
|
let rows = usize::from(self.rows);
|
||||||
|
let bottom = rows.min(if bottom_req == 0 { rows } else { bottom_req });
|
||||||
|
if top >= bottom {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scrolling_region.top = (top - 1) as size::CellCountInt;
|
||||||
|
self.scrolling_region.bottom = (bottom - 1) as size::CellCountInt;
|
||||||
|
self.set_cursor_pos(1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DECSLRM
|
||||||
|
pub fn set_left_and_right_margin(&mut self, left_req: usize, right_req: usize) {
|
||||||
|
// We must have this mode enabled to do anything
|
||||||
|
if !self.modes.get(Mode::EnableLeftAndRightMargin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let left = left_req.max(1);
|
||||||
|
let cols = usize::from(self.cols);
|
||||||
|
let right = cols.min(if right_req == 0 { cols } else { right_req });
|
||||||
|
if left >= right {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scrolling_region.left = (left - 1) as size::CellCountInt;
|
||||||
|
self.scrolling_region.right = (right - 1) as size::CellCountInt;
|
||||||
|
self.set_cursor_pos(1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll the text down by one row.
|
||||||
|
pub fn scroll_down(&mut self, count: usize) {
|
||||||
|
// Preserve our x/y to restore.
|
||||||
|
let (old_x, old_y, old_wrap) = {
|
||||||
|
let c = &self.screen().cursor;
|
||||||
|
(c.x, c.y, c.pending_wrap)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Move to the top of the scroll region
|
||||||
|
let (l, t) = (self.scrolling_region.left, self.scrolling_region.top);
|
||||||
|
self.screen_mut().cursor_absolute(l, t);
|
||||||
|
self.insert_lines(count);
|
||||||
|
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
scr.cursor_absolute(old_x, old_y);
|
||||||
|
scr.cursor.pending_wrap = old_wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes amount lines from the top of the scroll region. The remaining lines
|
||||||
|
/// to the bottom margin are shifted up and space from the bottom margin up
|
||||||
|
/// is filled with empty lines.
|
||||||
|
///
|
||||||
|
/// The new lines are created according to the current SGR state.
|
||||||
|
///
|
||||||
|
/// Does not change the (absolute) cursor position.
|
||||||
|
pub fn scroll_up(&mut self, count: usize) -> Result<(), Error> {
|
||||||
|
// Preserve our x/y to restore.
|
||||||
|
let (old_x, old_y, old_wrap) = {
|
||||||
|
let c = &self.screen().cursor;
|
||||||
|
(c.x, c.y, c.pending_wrap)
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = self.scroll_up_inner(count);
|
||||||
|
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
scr.cursor_absolute(old_x, old_y);
|
||||||
|
scr.cursor.pending_wrap = old_wrap;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_up_inner(&mut self, count: usize) -> Result<(), Error> {
|
||||||
|
let sr = self.scrolling_region;
|
||||||
|
|
||||||
|
// If our scroll region is at the top and we have no left/right
|
||||||
|
// margins then we move the scrolled out text into the scrollback.
|
||||||
|
if sr.top == 0 && sr.left == 0 && sr.right == self.cols - 1 {
|
||||||
|
// Scrolling dirties the images because it updates their placements pins.
|
||||||
|
self.screen_mut().kitty_images.dirty = true;
|
||||||
|
|
||||||
|
// Clamp count to the scroll region height.
|
||||||
|
let region_height = usize::from(sr.bottom) + 1;
|
||||||
|
let adjusted_count = count.min(region_height);
|
||||||
|
|
||||||
|
// TODO: Create an optimized version that can scroll N times
|
||||||
|
// This isn't critical because in most cases, scrollUp is used
|
||||||
|
// with count=1, but it's still a big optimization opportunity.
|
||||||
|
|
||||||
|
// Move our cursor to the bottom of the scroll region so we can
|
||||||
|
// use the cursorScrollAbove function to create scrollback
|
||||||
|
self.screen_mut().cursor_absolute(0, sr.bottom);
|
||||||
|
for _ in 0..adjusted_count {
|
||||||
|
self.screen_mut().cursor_scroll_above()?;
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the top of the scroll region
|
||||||
|
self.screen_mut().cursor_absolute(sr.left, sr.top);
|
||||||
|
self.delete_lines(count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll the viewport of the terminal grid.
|
||||||
|
pub fn scroll_viewport(&mut self, behavior: ScrollViewport) {
|
||||||
|
self.screen_mut().scroll(match behavior {
|
||||||
|
ScrollViewport::Top => Scroll::Top,
|
||||||
|
ScrollViewport::Bottom => Scroll::Active,
|
||||||
|
ScrollViewport::Delta(delta) => Scroll::DeltaRow(delta),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy columns `[left, right)` of a row of one page to a row of another
|
||||||
|
/// page in the same list (Zig: `Page.clonePartialRowFrom` across two page
|
||||||
|
/// nodes).
|
||||||
|
///
|
||||||
|
/// TODO(integration): replace with a `PageList` helper (see
|
||||||
|
/// ghostty-terminal-pagelist-requests.md).
|
||||||
|
fn clone_partial_row_between(
|
||||||
|
&mut self,
|
||||||
|
src: Pin,
|
||||||
|
dst: Pin,
|
||||||
|
left: usize,
|
||||||
|
right: usize,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let len = right - left;
|
||||||
|
let (source_row, cells) = {
|
||||||
|
let sp = self.screen().page(src.node);
|
||||||
|
let source_row = sp.rows[usize::from(src.y)];
|
||||||
|
let cells: Vec<_> = (left..right)
|
||||||
|
.map(|x| {
|
||||||
|
sp.cell_index(x, usize::from(src.y))
|
||||||
|
.map(|i| sp.export_cell(i))
|
||||||
|
})
|
||||||
|
.collect::<Result<_, Error>>()?;
|
||||||
|
(source_row, cells)
|
||||||
|
};
|
||||||
|
let dp = self.screen_mut().page_mut(dst.node);
|
||||||
|
let dy = usize::from(dst.y);
|
||||||
|
let original_row = dp.rows[dy];
|
||||||
|
dp.clear_cells(dy, left, right, false)?;
|
||||||
|
if source_row.managed_memory() {
|
||||||
|
for (n, c) in cells.iter().enumerate() {
|
||||||
|
dp.import_cell(left + n, dy, c)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let to = original_row.cells() + left;
|
||||||
|
for (n, c) in cells.iter().enumerate() {
|
||||||
|
dp.cells[to + n] = c.cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let managed = dp.rows[dy];
|
||||||
|
let mut metadata = source_row;
|
||||||
|
metadata.set_cells(original_row.cells());
|
||||||
|
if len < usize::from(dp.size.cols) {
|
||||||
|
metadata.set_wrap(original_row.wrap());
|
||||||
|
metadata.set_wrap_continuation(original_row.wrap_continuation());
|
||||||
|
metadata.set_grapheme(managed.grapheme());
|
||||||
|
metadata.set_styled(managed.styled());
|
||||||
|
metadata.set_hyperlink(managed.hyperlink());
|
||||||
|
metadata.set_dirty(source_row.dirty() || original_row.dirty());
|
||||||
|
}
|
||||||
|
dp.rows[dy] = metadata;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared implementation of insert_lines / delete_lines. When `insert` is
|
||||||
|
/// true rows are shifted down from the cursor (iterating bottom-up),
|
||||||
|
/// otherwise rows are shifted up (iterating top-down).
|
||||||
|
fn shift_lines(&mut self, count: usize, insert: bool) {
|
||||||
|
// Rare, but happens
|
||||||
|
if count == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sr = self.scrolling_region;
|
||||||
|
|
||||||
|
// If the cursor is outside the scroll region we do nothing.
|
||||||
|
{
|
||||||
|
let c = &self.screen().cursor;
|
||||||
|
if c.y < sr.top || c.y > sr.bottom || c.x < sr.left || c.x > sr.right {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrolling dirties the images because it updates their placements pins.
|
||||||
|
self.screen_mut().kitty_images.dirty = true;
|
||||||
|
|
||||||
|
// At the end we need to return the cursor to the row it started on.
|
||||||
|
let start_y = self.screen().cursor.y;
|
||||||
|
|
||||||
|
// We have a slower path if we have left or right scroll margins.
|
||||||
|
let left_right = sr.left > 0 || sr.right < self.cols - 1;
|
||||||
|
|
||||||
|
// Remaining rows from our cursor to the bottom of the scroll region.
|
||||||
|
let rem = usize::from(sr.bottom - self.screen().cursor.y) + 1;
|
||||||
|
|
||||||
|
// We can only insert lines up to our remaining lines in the scroll
|
||||||
|
// region. So we take whichever is smaller.
|
||||||
|
let adjusted_count = count.min(rem);
|
||||||
|
|
||||||
|
// Create a new tracked pin which we'll use to navigate the page list
|
||||||
|
// so that if we need to adjust capacity it will be properly tracked.
|
||||||
|
let start_pin = if insert {
|
||||||
|
self.screen()
|
||||||
|
.cursor_pin()
|
||||||
|
.down(&self.screen().pages, rem - 1)
|
||||||
|
.expect("scroll region bottom")
|
||||||
|
} else {
|
||||||
|
self.screen().cursor_pin()
|
||||||
|
};
|
||||||
|
let cur_h = match self.screen_mut().pages.track_pin(start_pin) {
|
||||||
|
Ok(h) => h,
|
||||||
|
Err(err) => {
|
||||||
|
// This error scenario means that our GPA is OOM. This is not a
|
||||||
|
// situation we can gracefully handle. We can't just ignore
|
||||||
|
// because it'll result in a corrupted screen.
|
||||||
|
log::error!("shift_lines trackPin error err={err:?}");
|
||||||
|
panic!("shift_lines trackPin OOM");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Our current y position relative to the cursor
|
||||||
|
let mut y: usize = if insert { rem } else { 0 };
|
||||||
|
|
||||||
|
// Traverse from the bottom up (insert) or top down (delete)
|
||||||
|
while if insert { y > 0 } else { y < rem } {
|
||||||
|
let cur_p = self.screen().pages.tracked_pin(cur_h).expect("tracked pin");
|
||||||
|
|
||||||
|
// If this is one of the lines we need to shift, do so
|
||||||
|
let shift = if insert {
|
||||||
|
y > adjusted_count
|
||||||
|
} else {
|
||||||
|
y < rem - adjusted_count
|
||||||
|
};
|
||||||
|
if shift {
|
||||||
|
let off_p = if insert {
|
||||||
|
cur_p.up(&self.screen().pages, adjusted_count)
|
||||||
|
} else {
|
||||||
|
cur_p.down(&self.screen().pages, adjusted_count)
|
||||||
|
}
|
||||||
|
.expect("shift source row");
|
||||||
|
|
||||||
|
let sr = self.scrolling_region;
|
||||||
|
self.row_will_be_shifted_at(cur_p);
|
||||||
|
self.row_will_be_shifted_at(off_p);
|
||||||
|
|
||||||
|
// If our scrolling region is full width, then we unset wrap.
|
||||||
|
if !left_right {
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
for p in [off_p, cur_p] {
|
||||||
|
let row = &mut scr.page_mut(p.node).rows[usize::from(p.y)];
|
||||||
|
row.set_wrap(false);
|
||||||
|
row.set_wrap_continuation(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (src_p, dst_p) = (off_p, cur_p);
|
||||||
|
|
||||||
|
// If our page doesn't match, then we need to do a copy from
|
||||||
|
// one page to another. This is the slow path.
|
||||||
|
if src_p.node != dst_p.node {
|
||||||
|
if let Err(err) = self.clone_partial_row_between(
|
||||||
|
src_p,
|
||||||
|
dst_p,
|
||||||
|
usize::from(sr.left),
|
||||||
|
usize::from(sr.right) + 1,
|
||||||
|
) {
|
||||||
|
// Adjust our page capacity to make
|
||||||
|
// room for we didn't have space for
|
||||||
|
let Some(adj) = capacity_adjustment(err) else {
|
||||||
|
panic!("clone_partial_row_from failed: {err:?}");
|
||||||
|
};
|
||||||
|
if let Err(e) = self.screen_mut().increase_capacity(dst_p.node, adj) {
|
||||||
|
match e {
|
||||||
|
// System OOM. We have no way to recover from this
|
||||||
|
// currently.
|
||||||
|
Error::OutOfMemory => {
|
||||||
|
panic!("increaseCapacity system allocator OOM")
|
||||||
|
}
|
||||||
|
// The page can't accommodate the managed memory required
|
||||||
|
// for this operation.
|
||||||
|
_ => panic!("increaseCapacity OutOfSpace"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue the loop to try handling this row again.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if !left_right {
|
||||||
|
// Swap the src/dst cells. This ensures that our dst gets the
|
||||||
|
// proper shifted rows and src gets non-garbage cell data that
|
||||||
|
// we can clear.
|
||||||
|
let page = self.screen_mut().page_mut(cur_p.node);
|
||||||
|
page.rows.swap(usize::from(dst_p.y), usize::from(src_p.y));
|
||||||
|
|
||||||
|
// Ensure what we did didn't corrupt the page
|
||||||
|
page.assert_integrity();
|
||||||
|
} else {
|
||||||
|
// Left/right scroll margins we have to
|
||||||
|
// copy cells, which is much slower...
|
||||||
|
let page = self.screen_mut().page_mut(cur_p.node);
|
||||||
|
page.move_cells(
|
||||||
|
usize::from(src_p.y),
|
||||||
|
usize::from(sr.left),
|
||||||
|
usize::from(dst_p.y),
|
||||||
|
usize::from(sr.left),
|
||||||
|
usize::from(sr.right - sr.left) + 1,
|
||||||
|
)
|
||||||
|
.expect("move_cells");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Clear the cells for this row, it has been shifted (insert)
|
||||||
|
// or it's from out of bounds (delete).
|
||||||
|
self.row_will_be_shifted_at(cur_p);
|
||||||
|
let (l, r) = (usize::from(sr.left), usize::from(sr.right) + 1);
|
||||||
|
self.screen_mut().clear_cells(cur_p.node, cur_p.y, l, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the row as dirty
|
||||||
|
cur_p.mark_dirty(&mut self.screen_mut().pages);
|
||||||
|
|
||||||
|
// We have successfully processed a line.
|
||||||
|
if insert {
|
||||||
|
y -= 1;
|
||||||
|
// Move our pin up to the next row.
|
||||||
|
if let Some(p) = cur_p.up(&self.screen().pages, 1) {
|
||||||
|
let _ = self.screen_mut().pages.set_tracked_pin(cur_h, p);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
y += 1;
|
||||||
|
// Move our pin down to the next row.
|
||||||
|
if let Some(p) = cur_p.down(&self.screen().pages, 1) {
|
||||||
|
let _ = self.screen_mut().pages.set_tracked_pin(cur_h, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
scr.pages.untrack_pin(cur_h);
|
||||||
|
scr.cursor_absolute(sr.left, start_y);
|
||||||
|
|
||||||
|
// Always unset pending wrap
|
||||||
|
scr.cursor.pending_wrap = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row_will_be_shifted_at(&mut self, p: Pin) {
|
||||||
|
let (sr, cols) = (self.scrolling_region, self.cols);
|
||||||
|
let page = self.screens.active_mut().page_mut(p.node);
|
||||||
|
row_will_be_shifted(sr, cols, page, usize::from(p.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert amount lines at the current cursor row. The contents of the line
|
||||||
|
/// at the current cursor row and below (to the bottom-most line in the
|
||||||
|
/// scrolling region) are shifted down by amount lines. The contents of the
|
||||||
|
/// amount bottom-most lines in the scroll region are lost.
|
||||||
|
///
|
||||||
|
/// This unsets the pending wrap state without wrapping. If the current cursor
|
||||||
|
/// position is outside of the current scroll region it does nothing.
|
||||||
|
///
|
||||||
|
/// If amount is greater than the remaining number of lines in the scrolling
|
||||||
|
/// region it is adjusted down (still allowing for scrolling out every remaining
|
||||||
|
/// line in the scrolling region)
|
||||||
|
///
|
||||||
|
/// In left and right margin mode the margins are respected; lines are only
|
||||||
|
/// scrolled in the scroll region.
|
||||||
|
///
|
||||||
|
/// All cleared space is colored according to the current SGR state.
|
||||||
|
///
|
||||||
|
/// Moves the cursor to the left margin.
|
||||||
|
pub fn insert_lines(&mut self, count: usize) {
|
||||||
|
self.shift_lines(count, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes amount lines from the current cursor row down. The remaining lines
|
||||||
|
/// to the bottom margin are shifted up and space from the bottom margin up is
|
||||||
|
/// filled with empty lines.
|
||||||
|
///
|
||||||
|
/// If the current cursor position is outside of the current scroll region it
|
||||||
|
/// does nothing. If amount is greater than the remaining number of lines in the
|
||||||
|
/// scrolling region it is adjusted down.
|
||||||
|
///
|
||||||
|
/// In left and right margin mode the margins are respected; lines are only
|
||||||
|
/// scrolled in the scroll region.
|
||||||
|
///
|
||||||
|
/// If the cell movement splits a multi cell character that character cleared,
|
||||||
|
/// by replacing it by spaces, keeping its current attributes. All other
|
||||||
|
/// cleared space is colored according to the current SGR state.
|
||||||
|
///
|
||||||
|
/// Moves the cursor to the left margin.
|
||||||
|
pub fn delete_lines(&mut self, count: usize) {
|
||||||
|
self.shift_lines(count, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear a stale `spacer_head` in the last cell of the row above the cursor.
|
||||||
|
fn clear_prev_row_spacer_head(scr: &mut Screen, cols: size::CellCountInt) {
|
||||||
|
let Some(mut p) = scr.cursor_pin().up(&scr.pages, 1) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
p.x = cols - 1;
|
||||||
|
let page = scr.page_mut(p.node);
|
||||||
|
if let Ok(i) = page.cell_index(usize::from(p.x), usize::from(p.y))
|
||||||
|
&& page.cells[i].wide() == Wide::SpacerHead
|
||||||
|
{
|
||||||
|
page.cells[i].set_wide(Wide::Narrow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminal {
|
||||||
|
/// Inserts spaces at current cursor position moving existing cell contents
|
||||||
|
/// to the right. The contents of the count right-most columns in the scroll
|
||||||
|
/// region are lost. The cursor position is not changed.
|
||||||
|
///
|
||||||
|
/// This unsets the pending wrap state without wrapping.
|
||||||
|
///
|
||||||
|
/// The inserted cells are colored according to the current SGR state.
|
||||||
|
pub fn insert_blanks(&mut self, count: usize) {
|
||||||
|
// Unset pending wrap state without wrapping. Note: this purposely
|
||||||
|
// happens BEFORE the scroll region check below, because that's what
|
||||||
|
// xterm does.
|
||||||
|
self.screen_mut().cursor.pending_wrap = false;
|
||||||
|
|
||||||
|
// If we're given a zero then we do nothing. The rest of this function
|
||||||
|
// assumes count > 0 and will crash if zero so return early. Note that
|
||||||
|
// this shouldn't be possible with real CSI sequences because the value
|
||||||
|
// is clamped to 1 min.
|
||||||
|
if count == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sr = self.scrolling_region;
|
||||||
|
let cx = usize::from(self.screen().cursor.x);
|
||||||
|
|
||||||
|
// If our cursor is outside the margins then do nothing. We DO reset
|
||||||
|
// wrap state still so this must remain below the above logic.
|
||||||
|
if self.screen().cursor.x < sr.left || self.screen().cursor.x > sr.right {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pin = self.screen().cursor_pin();
|
||||||
|
let (node, y) = (pin.node, usize::from(pin.y));
|
||||||
|
|
||||||
|
// If our X is a wide spacer tail then we need to erase the
|
||||||
|
// previous cell too so we don't split a multi-cell character.
|
||||||
|
if self.screen().cursor_cell().wide() == Wide::SpacerTail {
|
||||||
|
assert!(cx > 0);
|
||||||
|
self.screen_mut().clear_cells(node, pin.y, cx - 1, cx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remaining cols from our cursor to the right margin.
|
||||||
|
let rem = usize::from(sr.right) - cx + 1;
|
||||||
|
|
||||||
|
// If the cell at the right margin is wide, its spacer tail is
|
||||||
|
// outside the scroll region and would be orphaned by either the
|
||||||
|
// shift or the clear. Clean up both halves up front.
|
||||||
|
{
|
||||||
|
let right = cx + rem - 1;
|
||||||
|
if self.screen().cursor_row_cells()[right].wide() == Wide::Wide {
|
||||||
|
self.screen_mut().clear_cells(node, pin.y, right, right + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can only insert blanks up to our remaining cols
|
||||||
|
let adjusted_count = count.min(rem);
|
||||||
|
|
||||||
|
// This is the amount of space at the right of the scroll region
|
||||||
|
// that will NOT be blank, so we need to shift the correct cols right.
|
||||||
|
// "scroll_amount" is the number of such cols.
|
||||||
|
let scroll_amount = rem - adjusted_count;
|
||||||
|
if scroll_amount > 0 {
|
||||||
|
self.screen_mut()
|
||||||
|
.page_mut(node)
|
||||||
|
.pause_integrity_checks(true);
|
||||||
|
|
||||||
|
let end = cx + scroll_amount - 1;
|
||||||
|
|
||||||
|
// If our last cell we're shifting is wide, then we need to clear
|
||||||
|
// it to be empty so we don't split the multi-cell char.
|
||||||
|
if self.screen().cursor_row_cells()[end].wide() == Wide::Wide {
|
||||||
|
debug_assert!(self.screen().cursor_row_cells()[end + 1].wide() == Wide::SpacerTail);
|
||||||
|
self.screen_mut().clear_cells(node, pin.y, end, end + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We work backwards so we don't overwrite data.
|
||||||
|
let page = self.screen_mut().page_mut(node);
|
||||||
|
for x in (cx..=end).rev() {
|
||||||
|
page.swap_cells(x, y, x + adjusted_count, y)
|
||||||
|
.expect("swap_cells");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.screen_mut()
|
||||||
|
.page_mut(node)
|
||||||
|
.pause_integrity_checks(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert blanks. The blanks preserve the background color.
|
||||||
|
self.screen_mut()
|
||||||
|
.clear_cells(node, pin.y, cx, cx + adjusted_count);
|
||||||
|
|
||||||
|
// Our row is always dirty
|
||||||
|
self.screen_mut().cursor_mark_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes amount characters from the current cursor position to the right.
|
||||||
|
/// The remaining characters are shifted to the left and space from the right
|
||||||
|
/// margin is filled with spaces.
|
||||||
|
///
|
||||||
|
/// If amount is greater than the remaining number of characters in the
|
||||||
|
/// scrolling region, it is adjusted down.
|
||||||
|
///
|
||||||
|
/// Does not change the cursor position.
|
||||||
|
pub fn delete_chars(&mut self, count_req: usize) {
|
||||||
|
if count_req == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sr = self.scrolling_region;
|
||||||
|
let cx = usize::from(self.screen().cursor.x);
|
||||||
|
|
||||||
|
// If our cursor is outside the margins then do nothing. We DO reset
|
||||||
|
// wrap state still so this must remain below the above logic.
|
||||||
|
if self.screen().cursor.x < sr.left || self.screen().cursor.x > sr.right {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pin = self.screen().cursor_pin();
|
||||||
|
let (node, y) = (pin.node, usize::from(pin.y));
|
||||||
|
|
||||||
|
// Remaining cols from our cursor to the right margin.
|
||||||
|
let rem = usize::from(sr.right) - cx + 1;
|
||||||
|
|
||||||
|
// We can only insert blanks up to our remaining cols
|
||||||
|
let count = count_req.min(rem);
|
||||||
|
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
scr.split_cell_boundary(cx as size::CellCountInt);
|
||||||
|
scr.split_cell_boundary((cx + count) as size::CellCountInt);
|
||||||
|
scr.split_cell_boundary(sr.right + 1);
|
||||||
|
|
||||||
|
// This is the amount of space at the right of the scroll region
|
||||||
|
// that will NOT be blank, so we need to shift the correct cols right.
|
||||||
|
// "scroll_amount" is the number of such cols.
|
||||||
|
let scroll_amount = rem - count;
|
||||||
|
if scroll_amount > 0 {
|
||||||
|
let page = scr.page_mut(node);
|
||||||
|
page.pause_integrity_checks(true);
|
||||||
|
for x in cx..cx + scroll_amount {
|
||||||
|
page.swap_cells(x + count, y, x, y).expect("swap_cells");
|
||||||
|
}
|
||||||
|
page.pause_integrity_checks(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert blanks. The blanks preserve the background color.
|
||||||
|
scr.clear_cells(node, pin.y, cx + scroll_amount, cx + rem);
|
||||||
|
|
||||||
|
// Our row's soft-wrap is always reset.
|
||||||
|
scr.cursor_reset_wrap();
|
||||||
|
|
||||||
|
// Our row is always dirty
|
||||||
|
scr.cursor_mark_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn erase_chars(&mut self, count_req: usize) {
|
||||||
|
let cx = usize::from(self.screen().cursor.x);
|
||||||
|
let count = {
|
||||||
|
let remaining = usize::from(self.cols) - cx;
|
||||||
|
let mut end = remaining.min(count_req.max(1));
|
||||||
|
|
||||||
|
// If our last cell is a wide char then we need to also clear the
|
||||||
|
// cell beyond it since we can't just split a wide char.
|
||||||
|
if end != remaining {
|
||||||
|
let last = self
|
||||||
|
.screen()
|
||||||
|
.cursor_cell_right((end - 1) as size::CellCountInt);
|
||||||
|
if last.wide() == Wide::Wide {
|
||||||
|
end += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle any boundary conditions on the edges of the erased area.
|
||||||
|
//
|
||||||
|
// TODO(qwerasd): This isn't actually correct if you take in to account
|
||||||
|
// protected modes. We need to figure out how to make `clearCells` or at
|
||||||
|
// least `clearUnprotectedCells` handle boundary conditions...
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
scr.split_cell_boundary(cx as size::CellCountInt);
|
||||||
|
scr.split_cell_boundary((cx + count) as size::CellCountInt);
|
||||||
|
|
||||||
|
// Reset our row's soft-wrap.
|
||||||
|
scr.cursor_reset_wrap();
|
||||||
|
|
||||||
|
// Mark our cursor row as dirty
|
||||||
|
scr.cursor_mark_dirty();
|
||||||
|
|
||||||
|
// Clear the cells
|
||||||
|
let pin = scr.cursor_pin();
|
||||||
|
|
||||||
|
// If we never had a protection mode, then we can assume no cells
|
||||||
|
// are protected and go with the fast path. If the last protection
|
||||||
|
// mode was not ISO we also always ignore protection attributes.
|
||||||
|
if scr.protected_mode != ProtectedMode::Iso {
|
||||||
|
scr.clear_cells(pin.node, pin.y, cx, cx + count);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scr.clear_unprotected_cells(pin.node, pin.y, cx, cx + count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Erase the line.
|
||||||
|
pub fn erase_line(&mut self, mode: csi::EraseLine, protected_req: bool) {
|
||||||
|
// Get our start/end positions depending on mode.
|
||||||
|
let (start, end) = match mode {
|
||||||
|
csi::EraseLine::Right => {
|
||||||
|
let mut x = usize::from(self.screen().cursor.x);
|
||||||
|
|
||||||
|
// If our X is a wide spacer tail then we need to erase the
|
||||||
|
// previous cell too so we don't split a multi-cell character.
|
||||||
|
if x > 0 && self.screen().cursor_cell().wide() == Wide::SpacerTail {
|
||||||
|
x -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset our row's soft-wrap.
|
||||||
|
self.screen_mut().cursor_reset_wrap();
|
||||||
|
|
||||||
|
(x, usize::from(self.cols))
|
||||||
|
}
|
||||||
|
|
||||||
|
csi::EraseLine::Left => {
|
||||||
|
let mut x = usize::from(self.screen().cursor.x);
|
||||||
|
|
||||||
|
// If our x is a wide char we need to delete the tail too.
|
||||||
|
if self.screen().cursor_cell().wide() == Wide::Wide {
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
(0, x + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that it seems like complete should reset the soft-wrap
|
||||||
|
// state of the line but in xterm it does not.
|
||||||
|
csi::EraseLine::Complete => (0, usize::from(self.cols)),
|
||||||
|
|
||||||
|
other => {
|
||||||
|
log::error!("unimplemented erase line mode: {other:?}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// All modes will clear the pending wrap state and we know we have
|
||||||
|
// a valid mode at this point.
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
scr.cursor.pending_wrap = false;
|
||||||
|
|
||||||
|
// We always mark our row as dirty
|
||||||
|
scr.cursor_mark_dirty();
|
||||||
|
|
||||||
|
// We respect protected attributes if explicitly requested (probably
|
||||||
|
// a DECSEL sequence) or if our last protected mode was ISO even if its
|
||||||
|
// not currently set.
|
||||||
|
let protected = scr.protected_mode == ProtectedMode::Iso || protected_req;
|
||||||
|
|
||||||
|
let pin = scr.cursor_pin();
|
||||||
|
|
||||||
|
// If we're not respecting protected attributes, we can use a fast-path
|
||||||
|
// to fill the entire line.
|
||||||
|
if !protected {
|
||||||
|
scr.clear_cells(pin.node, pin.y, start, end);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scr.clear_unprotected_cells(pin.node, pin.y, start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Erase the display.
|
||||||
|
pub fn erase_display(&mut self, mode: csi::EraseDisplay, protected_req: bool) {
|
||||||
|
// We respect protected attributes if explicitly requested (probably
|
||||||
|
// a DECSEL sequence) or if our last protected mode was ISO even if its
|
||||||
|
// not currently set.
|
||||||
|
let protected = self.screen().protected_mode == ProtectedMode::Iso || protected_req;
|
||||||
|
|
||||||
|
match mode {
|
||||||
|
csi::EraseDisplay::ScrollComplete => {
|
||||||
|
if let Err(err) = self.screen_mut().scroll_clear() {
|
||||||
|
log::warn!("scroll clear failed, doing a normal clear err={err:?}");
|
||||||
|
self.erase_display(csi::EraseDisplay::Complete, protected_req);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsets pending wrap state
|
||||||
|
self.screen_mut().cursor.pending_wrap = false;
|
||||||
|
|
||||||
|
// Clear all Kitty graphics state for this screen
|
||||||
|
self.kitty_images_delete_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
csi::EraseDisplay::Complete => {
|
||||||
|
// If we're on the primary screen and our last non-empty row is
|
||||||
|
// a prompt, then we do a scroll_complete instead. This is a
|
||||||
|
// heuristic to get the generally desirable behavior that ^L
|
||||||
|
// at a prompt scrolls the screen contents prior to clearing.
|
||||||
|
// Most shells send `ESC [ H ESC [ 2 J` so we can't just check
|
||||||
|
// our current cursor position. See #905
|
||||||
|
if self.screens.active_key == ScreenKey::Primary && self.last_row_is_prompt() {
|
||||||
|
// If we fail, we just fall back to doing a normal clear
|
||||||
|
// so we don't worry about the error.
|
||||||
|
let _ = self.screen_mut().scroll_clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// All active area
|
||||||
|
self.screen_mut()
|
||||||
|
.clear_rows(Point::Active(Coordinate::default()), None, protected);
|
||||||
|
|
||||||
|
// Unsets pending wrap state
|
||||||
|
self.screen_mut().cursor.pending_wrap = false;
|
||||||
|
|
||||||
|
// Clear all Kitty graphics state for this screen
|
||||||
|
self.kitty_images_delete_all();
|
||||||
|
|
||||||
|
// Cleared screen dirty bit
|
||||||
|
self.flags.dirty.clear = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
csi::EraseDisplay::Below => {
|
||||||
|
// All lines to the right (including the cursor)
|
||||||
|
self.erase_line(csi::EraseLine::Right, protected_req);
|
||||||
|
|
||||||
|
// All lines below
|
||||||
|
if self.screen().cursor.y + 1 < self.rows {
|
||||||
|
let y = u32::from(self.screen().cursor.y) + 1;
|
||||||
|
self.screen_mut().clear_rows(
|
||||||
|
Point::Active(Coordinate { x: 0, y }),
|
||||||
|
None,
|
||||||
|
protected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsets pending wrap state. Should be done by erase_line.
|
||||||
|
assert!(!self.screen().cursor.pending_wrap);
|
||||||
|
}
|
||||||
|
|
||||||
|
csi::EraseDisplay::Above => {
|
||||||
|
// Erase to the left (including the cursor)
|
||||||
|
self.erase_line(csi::EraseLine::Left, protected_req);
|
||||||
|
|
||||||
|
// All lines above
|
||||||
|
if self.screen().cursor.y > 0 {
|
||||||
|
let y = u32::from(self.screen().cursor.y) - 1;
|
||||||
|
self.screen_mut().clear_rows(
|
||||||
|
Point::Active(Coordinate { x: 0, y: 0 }),
|
||||||
|
Some(Point::Active(Coordinate { x: 0, y })),
|
||||||
|
protected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsets pending wrap state
|
||||||
|
assert!(!self.screen().cursor.pending_wrap);
|
||||||
|
}
|
||||||
|
|
||||||
|
csi::EraseDisplay::Scrollback => self
|
||||||
|
.screen_mut()
|
||||||
|
.erase_rows(Point::History(Coordinate::default()), None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The "are we at a prompt" heuristic of `erase_display(.complete)`.
|
||||||
|
fn last_row_is_prompt(&self) -> bool {
|
||||||
|
let scr = self.screen();
|
||||||
|
let Some(active_br) = scr.pages.get_bottom_right(crate::point::Tag::Active) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let top = scr.pages.get_top_left(crate::point::Tag::Active);
|
||||||
|
// Go from the bottom of the active up and see if we're
|
||||||
|
// at a prompt.
|
||||||
|
// (The Zig loop decides on the first row it sees.)
|
||||||
|
let Some(p) = pin_iter::row_iterator(&scr.pages, active_br, Direction::LeftUp, top).next()
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let row = p.row_and_cell(&scr.pages).expect("row").0;
|
||||||
|
match row.semantic_prompt() {
|
||||||
|
// If we're at a prompt or input area, then we are at a prompt.
|
||||||
|
RowPrompt::Prompt | RowPrompt::PromptContinuation => true,
|
||||||
|
|
||||||
|
// If we have command output, then we're most certainly not
|
||||||
|
// at a prompt.
|
||||||
|
RowPrompt::None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete all kitty graphics state for the active screen.
|
||||||
|
///
|
||||||
|
/// TODO(integration): call `kitty_images.delete(.{ .all = true })` once
|
||||||
|
/// kitty graphics storage is ported; for now only the dirty flag exists.
|
||||||
|
fn kitty_images_delete_all(&mut self) {
|
||||||
|
self.screen_mut().kitty_images.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets all margins and fills the whole screen with the character 'E'
|
||||||
|
///
|
||||||
|
/// Sets the cursor to the top left corner.
|
||||||
|
pub fn decaln(&mut self) -> Result<(), Error> {
|
||||||
|
// Clear our stylistic attributes. This is the only thing that can
|
||||||
|
// fail so we do it first so we can undo it.
|
||||||
|
let old_style = self.screen().cursor.style;
|
||||||
|
{
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
scr.cursor.style = Style {
|
||||||
|
bg_color: scr.cursor.style.bg_color,
|
||||||
|
fg_color: scr.cursor.style.fg_color,
|
||||||
|
..Style::default()
|
||||||
|
};
|
||||||
|
if let Err(e) = scr.manual_style_update() {
|
||||||
|
scr.cursor.style = old_style;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset margins, also sets cursor to top-left
|
||||||
|
self.scrolling_region = ScrollingRegion {
|
||||||
|
top: 0,
|
||||||
|
bottom: self.rows - 1,
|
||||||
|
left: 0,
|
||||||
|
right: self.cols - 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Origin mode is disabled
|
||||||
|
self.modes.set(Mode::Origin, false);
|
||||||
|
|
||||||
|
// Move our cursor to the top-left
|
||||||
|
self.set_cursor_pos(1, 1);
|
||||||
|
|
||||||
|
// Use clearRows instead of eraseDisplay because we must NOT respect
|
||||||
|
// protected attributes here.
|
||||||
|
self.screen_mut()
|
||||||
|
.clear_rows(Point::Active(Coordinate::default()), None, false);
|
||||||
|
|
||||||
|
// Fill with Es by moving the cursor but reset it after.
|
||||||
|
let rows = self.rows;
|
||||||
|
loop {
|
||||||
|
let scr = self.screen_mut();
|
||||||
|
let pin = scr.cursor_pin();
|
||||||
|
let style_id = scr.cursor.style_id;
|
||||||
|
let page = scr.page_mut(pin.node);
|
||||||
|
let y = usize::from(pin.y);
|
||||||
|
let base = page.rows[y].cells();
|
||||||
|
let cols = usize::from(page.size.cols);
|
||||||
|
let mut e = Cell::init(u32::from('E'));
|
||||||
|
e.set_style_id(style_id);
|
||||||
|
// DECALN does not respect protected state. Verified with xterm.
|
||||||
|
e.set_protected(false);
|
||||||
|
page.cells[base..base + cols].fill(e);
|
||||||
|
|
||||||
|
// If we have a ref-counted style, increase
|
||||||
|
if style_id != style::DEFAULT_ID {
|
||||||
|
let _ = page.styles.use_multiple(style_id, cols as u16);
|
||||||
|
page.rows[y].set_styled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We messed with the page so assert its integrity here.
|
||||||
|
page.assert_integrity();
|
||||||
|
|
||||||
|
scr.cursor_mark_dirty();
|
||||||
|
if scr.cursor.y == rows - 1 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
scr.cursor_down(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the cursor to the top-left
|
||||||
|
self.set_cursor_pos(1, 1);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a kitty graphics command. The response (if any) is a full,
|
||||||
|
/// valid APC sequence payload to send to the pty.
|
||||||
|
///
|
||||||
|
/// If an error occurs, the caller should response to the pty that a
|
||||||
|
/// an error occurred otherwise the behavior of the graphics protocol is
|
||||||
|
/// undefined.
|
||||||
|
///
|
||||||
|
/// TODO(integration): call `kitty::graphics::execute(self, cmd)` once
|
||||||
|
/// kitty graphics_exec is ported. Until then no command has any effect and
|
||||||
|
/// no response is produced.
|
||||||
|
pub fn kitty_graphics(
|
||||||
|
&mut self,
|
||||||
|
cmd: &mut kitty_graphics_command::Command,
|
||||||
|
) -> Option<kitty_graphics_command::Response> {
|
||||||
|
let _ = cmd;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a style attribute.
|
||||||
|
pub fn set_attribute(&mut self, attr: sgr::Attribute<'_>) -> Result<(), Error> {
|
||||||
|
self.screen_mut().set_attribute(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print the active attributes as a string. This is used to respond to DECRQSS
|
||||||
|
/// requests.
|
||||||
|
///
|
||||||
|
/// Boolean attributes are printed first, followed by foreground color, then
|
||||||
|
/// background color. Each attribute is separated by a semicolon.
|
||||||
|
pub fn print_attributes(&self) -> String {
|
||||||
|
use std::fmt::Write;
|
||||||
|
// The SGR response always starts with a 0. See https://vt100.net/docs/vt510-rm/DECRPSS
|
||||||
|
let mut out = String::from("0");
|
||||||
|
|
||||||
|
let pen = self.screen().cursor.style;
|
||||||
|
let mut attrs = Vec::with_capacity(8);
|
||||||
|
|
||||||
|
if pen.flags.bold {
|
||||||
|
attrs.push('1');
|
||||||
|
}
|
||||||
|
if pen.flags.faint {
|
||||||
|
attrs.push('2');
|
||||||
|
}
|
||||||
|
if pen.flags.italic {
|
||||||
|
attrs.push('3');
|
||||||
|
}
|
||||||
|
if pen.flags.underline != ghostty_vt::sgr::Underline::None {
|
||||||
|
attrs.push('4');
|
||||||
|
}
|
||||||
|
if pen.flags.blink {
|
||||||
|
attrs.push('5');
|
||||||
|
}
|
||||||
|
if pen.flags.inverse {
|
||||||
|
attrs.push('7');
|
||||||
|
}
|
||||||
|
if pen.flags.invisible {
|
||||||
|
attrs.push('8');
|
||||||
|
}
|
||||||
|
if pen.flags.strikethrough {
|
||||||
|
attrs.push('9');
|
||||||
|
}
|
||||||
|
|
||||||
|
for c in attrs {
|
||||||
|
let _ = write!(out, ";{c}");
|
||||||
|
}
|
||||||
|
|
||||||
|
match pen.fg_color {
|
||||||
|
StyleColor::None => {}
|
||||||
|
StyleColor::Palette(idx) => {
|
||||||
|
if idx >= 16 {
|
||||||
|
let _ = write!(out, ";38:5:{idx}");
|
||||||
|
} else if idx >= 8 {
|
||||||
|
let _ = write!(out, ";9{}", idx - 8);
|
||||||
|
} else {
|
||||||
|
let _ = write!(out, ";3{idx}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StyleColor::Rgb(rgb) => {
|
||||||
|
let _ = write!(out, ";38:2::{}:{}:{}", rgb.r, rgb.g, rgb.b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match pen.bg_color {
|
||||||
|
StyleColor::None => {}
|
||||||
|
StyleColor::Palette(idx) => {
|
||||||
|
if idx >= 16 {
|
||||||
|
let _ = write!(out, ";48:5:{idx}");
|
||||||
|
} else if idx >= 8 {
|
||||||
|
let _ = write!(out, ";10{}", idx - 8);
|
||||||
|
} else {
|
||||||
|
let _ = write!(out, ";4{idx}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StyleColor::Rgb(rgb) => {
|
||||||
|
let _ = write!(out, ";48:2::{}:{}:{}", rgb.r, rgb.g, rgb.b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DECCOLM changes the terminal width between 80 and 132 columns. This
|
||||||
|
/// function call will do NOTHING unless `setDeccolmSupported` has been
|
||||||
|
/// called with "true".
|
||||||
|
///
|
||||||
|
/// This breaks the expectation around modern terminals that they resize
|
||||||
|
/// with the window. This will fix the grid at either 80 or 132 columns.
|
||||||
|
/// The rows will continue to be variable.
|
||||||
|
pub fn deccolm(&mut self, mode: DeccolmMode) -> Result<(), Error> {
|
||||||
|
// If DEC mode 40 isn't enabled, then this is ignored. We also make
|
||||||
|
// sure that we don't have deccolm set because we want to fully ignore
|
||||||
|
// set mode.
|
||||||
|
if !self.modes.get(Mode::EnableMode3) {
|
||||||
|
self.modes.set(Mode::M132Column, false);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable it
|
||||||
|
self.modes
|
||||||
|
.set(Mode::M132Column, mode == DeccolmMode::Cols132);
|
||||||
|
|
||||||
|
// Resize to the requested size
|
||||||
|
let rows = self.rows;
|
||||||
|
self.resize(
|
||||||
|
match mode {
|
||||||
|
DeccolmMode::Cols132 => 132,
|
||||||
|
DeccolmMode::Cols80 => 80,
|
||||||
|
},
|
||||||
|
rows,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Erase our display and move our cursor.
|
||||||
|
self.erase_display(csi::EraseDisplay::Complete, false);
|
||||||
|
self.set_cursor_pos(1, 1);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resize the underlying terminal.
|
||||||
|
pub fn resize(
|
||||||
|
&mut self,
|
||||||
|
cols: size::CellCountInt,
|
||||||
|
rows: size::CellCountInt,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
// If our cols/rows didn't change then we're done
|
||||||
|
if self.cols == cols && self.rows == rows {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize our tabstops
|
||||||
|
if self.cols != cols {
|
||||||
|
self.tabstops = Tabstops::new(usize::from(cols), 8).map_err(map_tabstops_err)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize primary screen, which supports reflow
|
||||||
|
let reflow = self.modes.get(Mode::Wraparound);
|
||||||
|
let prompt_redraw = self.flags.shell_redraws_prompt;
|
||||||
|
let primary = self.screens.get_mut(ScreenKey::Primary).expect("primary");
|
||||||
|
primary.resize(ScreenResize {
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
reflow,
|
||||||
|
prompt_redraw,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Alternate screen, if it exists, doesn't reflow
|
||||||
|
if let Some(alt) = self.screens.get_mut(ScreenKey::Alternate) {
|
||||||
|
alt.resize(ScreenResize {
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
reflow: false,
|
||||||
|
prompt_redraw: Redraw::False,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whenever we resize we just mark it as a screen clear
|
||||||
|
self.flags.dirty.clear = true;
|
||||||
|
|
||||||
|
// Set our size
|
||||||
|
self.cols = cols;
|
||||||
|
self.rows = rows;
|
||||||
|
|
||||||
|
// Reset the scrolling region
|
||||||
|
self.scrolling_region = ScrollingRegion {
|
||||||
|
top: 0,
|
||||||
|
bottom: rows - 1,
|
||||||
|
left: 0,
|
||||||
|
right: cols - 1,
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the pwd for the terminal.
|
||||||
|
pub fn set_pwd(&mut self, pwd: &[u8]) -> Result<(), Error> {
|
||||||
|
self.pwd.clear();
|
||||||
|
self.pwd.extend_from_slice(pwd);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the pwd for the terminal, if any. The memory is owned by the
|
||||||
|
/// Terminal and is not copied. It is safe until a reset or set_pwd.
|
||||||
|
pub fn get_pwd(&self) -> Option<&[u8]> {
|
||||||
|
if self.pwd.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(&self.pwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch to the given screen type (alternate or primary).
|
||||||
|
///
|
||||||
|
/// This does NOT handle behaviors such as clearing the screen,
|
||||||
|
/// copying the cursor, etc. This should be handled by downstream
|
||||||
|
/// callers.
|
||||||
|
///
|
||||||
|
/// After calling this function, the active screen is `key`, and the
|
||||||
|
/// returned value will be the key of the previous screen. If the return
|
||||||
|
/// value is `None`, then the screen was not switched because it was
|
||||||
|
/// already the active screen.
|
||||||
|
///
|
||||||
|
/// Note: This is written in a generic way so that we can support
|
||||||
|
/// more than two screens in the future if needed. There isn't
|
||||||
|
/// currently a spec for this, but it is something I think might
|
||||||
|
/// be useful in the future.
|
||||||
|
pub fn switch_screen(&mut self, key: ScreenKey) -> Result<Option<ScreenKey>, Error> {
|
||||||
|
// If we're already on the requested screen we do nothing.
|
||||||
|
if self.screens.active_key == key {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let old_key = self.screens.active_key;
|
||||||
|
|
||||||
|
// We always end hyperlink state when switching screens.
|
||||||
|
// We need to do this on the original screen.
|
||||||
|
self.screens.active_mut().end_hyperlink();
|
||||||
|
|
||||||
|
// Switch the screens/
|
||||||
|
if self.screens.get(key).is_none() {
|
||||||
|
let primary = self.screens.get(ScreenKey::Primary).expect("primary");
|
||||||
|
let opts = ScreenOptions {
|
||||||
|
cols: self.cols,
|
||||||
|
rows: self.rows,
|
||||||
|
max_scrollback: match key {
|
||||||
|
ScreenKey::Primary => primary.pages.explicit_max_size,
|
||||||
|
ScreenKey::Alternate => 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Inherit our Kitty image storage limit from the primary
|
||||||
|
// screen if we have to initialize.
|
||||||
|
// TODO(integration): `primary.kitty_images.total_limit`.
|
||||||
|
kitty_image_storage_limit: ScreenOptions::DEFAULT.kitty_image_storage_limit,
|
||||||
|
};
|
||||||
|
self.screens.get_init(key, opts)?;
|
||||||
|
}
|
||||||
|
let old_charset = self.screens.active().charset;
|
||||||
|
let new = self.screens.get_mut(key).expect("screen exists");
|
||||||
|
|
||||||
|
// The new screen should not have any hyperlinks set
|
||||||
|
assert!(new.cursor.hyperlink_id == 0);
|
||||||
|
|
||||||
|
// Bring our charset state with us
|
||||||
|
new.charset = old_charset;
|
||||||
|
|
||||||
|
// Clear our selection
|
||||||
|
new.clear_selection();
|
||||||
|
|
||||||
|
// Mark kitty images as dirty so they redraw. Without this set
|
||||||
|
// the images will remain where they were (the dirty bit on
|
||||||
|
// the screen only tracks the terminal grid, not the images).
|
||||||
|
new.kitty_images.dirty = true;
|
||||||
|
|
||||||
|
// Mark our terminal as dirty to redraw the grid.
|
||||||
|
self.flags.dirty.clear = true;
|
||||||
|
|
||||||
|
// Finalize the switch
|
||||||
|
self.screens.switch_to(key);
|
||||||
|
|
||||||
|
Ok(Some(old_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch screen via a mode switch (e.g. mode 47, 1047, 1049).
|
||||||
|
/// This is a much more opinionated operation than `switch_screen`
|
||||||
|
/// since it also handles the behaviors of the specific mode,
|
||||||
|
/// such as clearing the screen, saving/restoring the cursor,
|
||||||
|
/// etc.
|
||||||
|
///
|
||||||
|
/// This should be used for legacy compatibility with VT protocols,
|
||||||
|
/// but more modern usage should use `switch_screen` instead and handle
|
||||||
|
/// details like clearing the screen, cursor saving, etc. manually.
|
||||||
|
pub fn switch_screen_mode(
|
||||||
|
&mut self,
|
||||||
|
mode: SwitchScreenMode,
|
||||||
|
enabled: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
// The behavior in this function is completely based on reading
|
||||||
|
// the xterm source, specifically "charproc.c" for
|
||||||
|
// `srm_ALTBUF`, `srm_OPT_ALTBUF`, and `srm_OPT_ALTBUF_CURSOR`.
|
||||||
|
// We shouldn't touch anything in here without adding a unit
|
||||||
|
// test AND verifying the behavior with xterm.
|
||||||
|
|
||||||
|
match mode {
|
||||||
|
SwitchScreenMode::M47 => {}
|
||||||
|
|
||||||
|
// If we're disabling 1047 and we're on alt screen then
|
||||||
|
// we clear the screen.
|
||||||
|
SwitchScreenMode::M1047 => {
|
||||||
|
if !enabled && self.screens.active_key == ScreenKey::Alternate {
|
||||||
|
self.erase_display(csi::EraseDisplay::Complete, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1049 unconditionally saves the cursor on enabling, even
|
||||||
|
// if we're already on the alternate screen.
|
||||||
|
SwitchScreenMode::M1049 => {
|
||||||
|
if enabled {
|
||||||
|
self.save_cursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch screens first to whatever we're going to.
|
||||||
|
let to = if enabled {
|
||||||
|
ScreenKey::Alternate
|
||||||
|
} else {
|
||||||
|
ScreenKey::Primary
|
||||||
|
};
|
||||||
|
let old = self.switch_screen(to)?;
|
||||||
|
|
||||||
|
match mode {
|
||||||
|
// For these modes, we need to copy the cursor. We only copy
|
||||||
|
// the cursor if the screen actually changed, otherwise the
|
||||||
|
// cursor is already copied. The cursor is copied regardless
|
||||||
|
// of destination screen.
|
||||||
|
SwitchScreenMode::M47 | SwitchScreenMode::M1047 => {
|
||||||
|
if let Some(old) = old {
|
||||||
|
self.copy_cursor_from(old);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode 1049 restores cursor on the primary screen when
|
||||||
|
// we disable it.
|
||||||
|
SwitchScreenMode::M1049 => {
|
||||||
|
if enabled {
|
||||||
|
assert!(self.screens.active_key == ScreenKey::Alternate);
|
||||||
|
self.erase_display(csi::EraseDisplay::Complete, false);
|
||||||
|
|
||||||
|
// When we enter alt screen with 1049, we always copy the
|
||||||
|
// cursor from the primary screen (if we weren't already
|
||||||
|
// on it).
|
||||||
|
if let Some(old) = old {
|
||||||
|
self.copy_cursor_from(old);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert!(self.screens.active_key == ScreenKey::Primary);
|
||||||
|
self.restore_cursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_cursor_from(&mut self, old: ScreenKey) {
|
||||||
|
let other = self.screens.get(old).expect("old screen").cursor.clone();
|
||||||
|
if let Err(err) = self.screen_mut().cursor_copy(&other, false) {
|
||||||
|
log::warn!("cursor copy failed entering alt screen err={err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the current string value of the terminal. Newlines are
|
||||||
|
/// encoded as "\n". This omits any formatting such as fg/bg.
|
||||||
|
pub fn plain_string(&self) -> Result<String, Error> {
|
||||||
|
self.screen()
|
||||||
|
.dump_string_alloc(Point::Viewport(Coordinate::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as plain_string, but respects row wrap state when building the string.
|
||||||
|
pub fn plain_string_unwrapped(&self) -> Result<String, Error> {
|
||||||
|
self.screen()
|
||||||
|
.dump_string_alloc_unwrapped(Point::Viewport(Coordinate::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full reset.
|
||||||
|
///
|
||||||
|
/// This will attempt to free the existing screen memory but if that fails
|
||||||
|
/// this will reuse the existing memory. In the latter case, memory may
|
||||||
|
/// be wasted (since its unused) but it isn't leaked.
|
||||||
|
pub fn full_reset(&mut self) {
|
||||||
|
// Ensure we're back on primary screen
|
||||||
|
self.screens.switch_to(ScreenKey::Primary);
|
||||||
|
self.screens.remove(ScreenKey::Alternate);
|
||||||
|
|
||||||
|
// Reset our screens
|
||||||
|
if let Err(err) = self.screen_mut().reset() {
|
||||||
|
log::warn!("screen reset failed err={err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rest our basic state
|
||||||
|
self.modes.reset();
|
||||||
|
self.flags = Flags::default();
|
||||||
|
self.tabstops.reset(TABSTOP_INTERVAL);
|
||||||
|
self.previous_char = None;
|
||||||
|
self.pwd.clear();
|
||||||
|
self.status_display = StatusDisplay::Main;
|
||||||
|
self.scrolling_region = ScrollingRegion {
|
||||||
|
top: 0,
|
||||||
|
bottom: self.rows - 1,
|
||||||
|
left: 0,
|
||||||
|
right: self.cols - 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Always mark dirty so we redraw everything
|
||||||
|
self.flags.dirty.clear = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the point is dirty, used for testing.
|
||||||
|
pub fn is_dirty(&self, pt: Point) -> bool {
|
||||||
|
let scr = self.screen();
|
||||||
|
let pin = scr.pages.pin(pt).expect("valid point");
|
||||||
|
pin.is_dirty(&scr.pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all dirty bits. Testing only.
|
||||||
|
pub fn clear_dirty(&mut self) {
|
||||||
|
self.screen_mut().pages.clear_dirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
Reference in new issue
Block a user