use std::collections::VecDeque;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use vte::{Params, Perform};
use crate::cell::{Cell, CellFlags};
use crate::color::Color;
use crate::cursor::{Cursor, CursorShape, Pen};
use crate::damage::{LineBounds, LineDamage, ScrollOp, TermDamage};
use crate::event::{ClipboardTarget, TermEvent};
use crate::grid::{ExtAttrs, Grid, Row};
use crate::input::{
KeyEvent, MouseEncoding, MouseEvent, MouseProtocol, encode_focus, encode_key, encode_mouse,
encode_paste,
};
use crate::search::Match;
use crate::selection::{BufferPoint, Selection};
use crate::serialize::{Frame, FrameKind, MAX_SCROLL_COUNT, MarkerId, MarkerKind, Overlay, Span};
mod walk;
mod search;
mod selection;
mod markers;
mod logical;
mod tracked;
pub struct Term {
grid: Grid,
alt_grid: Grid,
cursor: Cursor,
saved_cursor: Cursor,
on_alt: bool,
tabs: Vec<bool>,
word_separators: String,
window_title: String,
icon_name: String,
window_title_stack: Vec<String>,
icon_name_stack: Vec<String>,
origin_mode: bool,
autowrap: bool,
insert_mode: bool,
newline_mode: bool,
reverse_wraparound: bool,
bracketed_paste: bool,
synchronized_output: bool,
color_scheme_updates: bool,
grapheme_clustering: bool,
win32_input_mode: bool,
app_cursor_keys: bool,
application_keypad: bool,
vt52_mode: bool,
vt52_y_pending: u8,
vt52_y_row: usize,
mouse_protocol: MouseProtocol,
mouse_encoding: MouseEncoding,
focus_events: bool,
kitty_flags: u8,
kitty_stack: Vec<u8>,
events: Vec<TermEvent>,
replies: Vec<u8>,
current_link: Option<std::sync::Arc<str>>,
link_ids: std::collections::HashMap<String, std::sync::Weak<str>>,
link_ids_sweep_at: usize,
scroll_top: usize,
scroll_bottom: usize,
scrollback: VecDeque<Row>,
display_offset: usize,
scrollback_limit: usize,
recycled_row: Option<Row>,
line_damage: Vec<LineBounds>,
scroll: Option<ScrollOp>,
full_damage: bool,
prev_cursor: (usize, usize),
selection: Option<Selection>,
search_highlights: Vec<Match>,
active_search_highlight: Option<Match>,
normal_markers: VecDeque<Marker>,
alt_markers: VecDeque<Marker>,
next_marker_id: u32,
evicted_total: u64,
marker_epoch: u32,
normal_tracked: Vec<TrackedPoint>,
alt_tracked: Vec<TrackedPoint>,
next_tracked_id: u32,
decsc: SavedCursor,
charsets: [Charset; 4],
gl: usize,
}
#[derive(Clone, Copy, PartialEq, Eq, Default)]
enum Charset {
#[default]
Ascii,
DecSpecialGraphics,
Uk,
}
impl Charset {
fn map(self, c: char) -> char {
match self {
Charset::Ascii => c,
Charset::Uk if c == '#' => '£',
Charset::Uk => c,
Charset::DecSpecialGraphics => dec_special_graphics(c),
}
}
}
fn dec_special_graphics(c: char) -> char {
match c {
'`' => '◆',
'a' => '▒',
'b' => '␉',
'c' => '␌',
'd' => '␍',
'e' => '␊',
'f' => '°',
'g' => '±',
'h' => '',
'i' => '␋',
'j' => '┘',
'k' => '┐',
'l' => '┌',
'm' => '└',
'n' => '┼',
'o' => '⎺',
'p' => '⎻',
'q' => '─',
'r' => '⎼',
's' => '⎽',
't' => '├',
'u' => '┤',
'v' => '┴',
'w' => '┬',
'x' => '│',
'y' => '≤',
'z' => '≥',
'{' => 'π',
'|' => '≠',
'}' => '£',
'~' => '·',
other => other,
}
}
const DEFAULT_SCROLLBACK: usize = 10_000;
pub const MIN_COLUMNS: usize = 2;
pub const DEFAULT_WORD_SEPARATORS: &str = ",│`|:\"' ()[]{}<>\t\u{3000}";
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Hyperlink {
uri: std::sync::Arc<str>,
}
impl Hyperlink {
pub(crate) fn new(uri: std::sync::Arc<str>) -> Self {
Hyperlink { uri }
}
pub fn uri(&self) -> &str {
&self.uri
}
}
const LINK_IDS_FIRST_SWEEP: usize = 16;
const TITLE_STACK_DEPTH: usize = 10;
fn osc8_link_id(params: &[u8]) -> Option<&[u8]> {
params
.split(|b| *b == b':')
.find_map(|kv| kv.strip_prefix(b"id="))
.filter(|value| !value.is_empty())
}
pub const MAX_COLUMNS: usize = u16::MAX as usize;
pub const MAX_ROWS: usize = u16::MAX as usize;
pub const MAX_MARKERS: usize = u16::MAX as usize;
pub const MAX_COMMAND_TEXT: usize = 4096;
pub const MAX_CLIPBOARD_BASE64: usize = 16 * 1024 * 1024;
#[derive(Clone, Copy, Default)]
struct SavedCursor {
row: usize,
col: usize,
pen: Pen,
pending_wrap: bool,
origin_mode: bool,
charsets: [Charset; 4],
gl: usize,
}
struct Marker {
id: MarkerId,
line: usize,
col: usize,
kind: MarkerKind,
command: Option<Box<CommandRecord>>,
}
struct CommandRecord {
text: Box<str>,
exit: Option<i32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TrackedId(pub(crate) u32);
struct TrackedPoint {
id: TrackedId,
line: usize,
col: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MarkerEntry {
pub id: MarkerId,
pub line: u32,
pub kind: MarkerKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MarkerIndex {
pub markers: Vec<MarkerEntry>,
pub evicted_total: u64,
pub epoch: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandLine {
pub line: usize,
pub command: String,
pub exit: Option<i32>,
}
fn bounds_to_lines(bounds: &[LineBounds]) -> Vec<LineDamage> {
bounds
.iter()
.enumerate()
.filter(|(_, b)| b.is_damaged())
.map(|(line, b)| {
let (left, right) = b.span();
LineDamage { line, left, right }
})
.collect()
}
impl Term {
pub fn new(cols: usize, rows: usize) -> Self {
Self::with_scrollback(cols, rows, DEFAULT_SCROLLBACK)
}
pub fn with_scrollback(cols: usize, rows: usize, scrollback_limit: usize) -> Self {
let cols = cols.clamp(MIN_COLUMNS, MAX_COLUMNS);
let rows = rows.clamp(1, MAX_ROWS);
Term {
grid: Grid::new(cols, rows),
alt_grid: Grid::new(cols, rows),
cursor: Cursor::default(),
saved_cursor: Cursor::default(),
on_alt: false,
origin_mode: false,
autowrap: true,
insert_mode: false,
newline_mode: false,
reverse_wraparound: false,
bracketed_paste: false,
synchronized_output: false,
color_scheme_updates: false,
grapheme_clustering: false,
win32_input_mode: false,
app_cursor_keys: false,
application_keypad: false,
vt52_mode: false,
vt52_y_pending: 0,
vt52_y_row: 0,
mouse_protocol: MouseProtocol::Off,
mouse_encoding: MouseEncoding::Default,
focus_events: false,
kitty_flags: 0,
kitty_stack: Vec::new(),
events: Vec::new(),
replies: Vec::new(),
current_link: None,
link_ids: std::collections::HashMap::new(),
link_ids_sweep_at: LINK_IDS_FIRST_SWEEP,
tabs: default_tabs(cols),
word_separators: DEFAULT_WORD_SEPARATORS.to_owned(),
window_title: String::new(),
icon_name: String::new(),
window_title_stack: Vec::new(),
icon_name_stack: Vec::new(),
scroll_top: 0,
scroll_bottom: rows - 1,
scrollback: VecDeque::new(),
display_offset: 0,
scrollback_limit,
recycled_row: None,
line_damage: vec![LineBounds::undamaged(cols); rows],
scroll: None,
full_damage: false,
prev_cursor: (0, 0), selection: None,
search_highlights: Vec::new(),
active_search_highlight: None,
normal_markers: VecDeque::new(),
alt_markers: VecDeque::new(),
next_marker_id: 0,
evicted_total: 0,
marker_epoch: 0,
normal_tracked: Vec::new(),
alt_tracked: Vec::new(),
next_tracked_id: 0,
decsc: SavedCursor::default(),
charsets: [Charset::Ascii; 4],
gl: 0,
}
}
pub fn damage(&self) -> TermDamage {
if self.full_damage {
return TermDamage::Full;
}
if self.display_offset > 0 {
return TermDamage::Partial(Vec::new());
}
TermDamage::Partial(bounds_to_lines(&self.line_damage))
}
fn frame_damage(&self) -> TermDamage {
if self.full_damage {
return TermDamage::Full;
}
if self.display_offset > 0 {
return TermDamage::Partial(Vec::new());
}
let cur = self.cursor.point();
if cur == self.prev_cursor {
return TermDamage::Partial(bounds_to_lines(&self.line_damage));
}
let mut bounds = self.line_damage.clone();
bounds[cur.0].expand(cur.1, cur.1);
let pr = self.prev_cursor.0.min(self.grid.rows() - 1);
let pc = self.prev_cursor.1.min(self.grid.cols() - 1);
bounds[pr].expand(pc, pc);
TermDamage::Partial(bounds_to_lines(&bounds))
}
pub fn reset_damage(&mut self) {
for b in &mut self.line_damage {
b.reset();
}
self.scroll = None;
self.full_damage = false;
self.prev_cursor = self.cursor.point();
}
pub fn mark_fully_damaged(&mut self) {
self.full_damage = true;
}
fn damage_span(&mut self, row: usize, left: usize, right: usize) {
let last = self.grid.cols().saturating_sub(1);
debug_assert!(
left <= right && right <= last,
"damage_span({row}, {left}, {right}) is not a span inside [0, {last}]"
);
self.line_damage[row].expand(left.min(last), right.min(last));
}
pub fn scroll_delta(&self) -> Option<ScrollOp> {
if self.display_offset > 0 {
return None;
}
self.scroll.map(cap_scroll)
}
pub fn frame(&self) -> Frame {
let cols = self.grid.cols();
let rows = self.grid.rows();
let (kind, line_spans): (FrameKind, Vec<(usize, usize, usize)>) = match self.frame_damage()
{
TermDamage::Full => (
FrameKind::Full,
(0..rows).map(|l| (l, 0, cols - 1)).collect(),
),
TermDamage::Partial(lines) => (
FrameKind::Partial,
lines
.into_iter()
.map(|d| (d.line, d.left, d.right))
.collect(),
),
};
let mut link_table: Vec<String> = Vec::new();
let mut link_remap: std::collections::HashMap<*const u8, u32> =
std::collections::HashMap::new();
let top = self.scrollback.len() - self.display_offset;
let mut spans = Vec::with_capacity(line_spans.len());
for (line, left, right) in line_spans {
let mut cells = Vec::with_capacity(right - left + 1);
let mut combining = std::collections::BTreeMap::new();
let mut links = std::collections::BTreeMap::new();
let mut ucolors = std::collections::BTreeMap::new();
let row = self.abs_row(top + line);
let last_col = row.len().saturating_sub(1);
for col in left..=right {
let mut cell = row[col];
if col == last_col && row.is_wrapped() {
cell.insert_flags(CellFlags::WRAPLINE);
}
if let Some(marks) = row.combining_at(col) {
combining.insert(col - left, marks.to_vec());
}
if let Some(uri) = row.link_at(col) {
let key = std::sync::Arc::as_ptr(uri) as *const u8;
let next = link_table.len() as u32 + 1;
let fidx = *link_remap.entry(key).or_insert_with(|| {
link_table.push(uri.to_string());
next
});
let fidx = core::num::NonZeroU32::new(fidx)
.expect("frame-local link indices are 1-based");
links.insert(col - left, fidx);
}
if let Some(color) = row.ucolor_at(col) {
ucolors.insert(col - left, color);
}
cells.push(cell);
}
spans.push(Span {
line: line as u16,
left: left as u16,
right: right as u16,
cells,
combining,
links,
ucolors,
});
}
Frame {
cols: cols as u16,
rows: rows as u16,
kind,
cursor_row: self.cursor.row as u16,
cursor_col: self.cursor.col as u16,
cursor_visible: self.cursor.visible && self.display_offset == 0,
cursor_shape: self.cursor.shape,
cursor_blink: self.cursor.blink,
display_offset: self.display_offset as u32,
scrollback_len: self.scrollback.len() as u32,
evicted_total: self.evicted_total,
marker_epoch: self.marker_epoch,
marker_count: self.markers().len() as u32,
mouse_events: self.mouse_protocol.wanted_events(),
alt_screen: self.on_alt,
scroll: self.scroll_delta(),
spans,
link_table,
overlay: Overlay {
selection: self.selection_range(),
matches: self
.search_highlights
.iter()
.flat_map(|m| self.match_spans(m))
.collect(),
active_match: self
.active_search_highlight
.as_ref()
.map(|m| self.match_spans(m))
.unwrap_or_default(),
markers: self.marker_positions(),
},
}
}
fn record_scroll(&mut self, top: usize, bottom: usize, count: isize) {
let cols = self.grid.cols();
match count {
1 => {
self.line_damage[top..=bottom].rotate_left(1);
self.line_damage[bottom] = LineBounds::fully_damaged(cols);
}
-1 => {
self.line_damage[top..=bottom].rotate_right(1);
self.line_damage[top] = LineBounds::fully_damaged(cols);
}
_ => {}
}
match self.scroll {
Some(op) if op.top == top && op.bottom == bottom => {
self.scroll = Some(ScrollOp {
top,
bottom,
count: op.count + count,
});
}
None => self.scroll = Some(ScrollOp { top, bottom, count }),
Some(_) => {
self.scroll = None;
self.mark_fully_damaged();
}
}
}
pub fn set_word_separators(&mut self, separators: &str) {
let mut set: String = separators.to_owned();
if !set.contains(' ') {
set.push(' ');
}
self.word_separators = set;
}
pub fn word_separators(&self) -> &str {
&self.word_separators
}
pub fn scrollback_len(&self) -> usize {
self.scrollback.len()
}
pub fn synchronized_output(&self) -> bool {
self.synchronized_output
}
pub fn color_scheme_updates(&self) -> bool {
self.color_scheme_updates
}
pub fn grapheme_clustering(&self) -> bool {
self.grapheme_clustering
}
pub fn win32_input_mode(&self) -> bool {
self.win32_input_mode
}
pub fn report_color_scheme(&mut self, dark: bool) {
let ps = if dark { 1 } else { 2 };
self.replies
.extend_from_slice(format!("\x1b[?997;{ps}n").as_bytes());
}
fn special_color(&mut self, params: &[&[u8]], start: usize) {
for (i, &spec) in params[1..].iter().enumerate() {
if spec.is_empty() {
continue; }
let event = match start + i {
0 if spec == b"?" => TermEvent::QueryForeground,
0 => TermEvent::SetForeground(String::from_utf8_lossy(spec).into_owned()),
1 if spec == b"?" => TermEvent::QueryBackground,
1 => TermEvent::SetBackground(String::from_utf8_lossy(spec).into_owned()),
2 if spec == b"?" => TermEvent::QueryCursorColor,
2 => TermEvent::SetCursorColor(String::from_utf8_lossy(spec).into_owned()),
_ => break, };
self.events.push(event);
}
}
fn clipboard(&mut self, params: &[&[u8]]) {
let Some(&field) = params.get(1) else {
return;
};
let target = match field {
b"" | b"c" => ClipboardTarget::Clipboard,
b"p" => ClipboardTarget::Primary,
b"s" => ClipboardTarget::Selection,
_ => return,
};
if params.len() < 3 {
return;
}
let fields = ¶ms[2..];
if fields.iter().map(|f| f.len()).sum::<usize>() + fields.len() - 1 > MAX_CLIPBOARD_BASE64 {
return;
}
let payload: Vec<u8> = fields.join(&b';');
if payload == b"?" {
self.events.push(TermEvent::QueryClipboard { target });
return;
}
if let Some(bytes) = crate::base64::decode(&payload)
&& let Ok(text) = String::from_utf8(bytes)
{
self.events.push(TermEvent::ClipboardStore { target, text });
}
}
pub fn report_clipboard(&mut self, target: ClipboardTarget, text: &str) {
let field = match target {
ClipboardTarget::Clipboard => 'c',
ClipboardTarget::Primary => 'p',
ClipboardTarget::Selection => 's',
};
let data = crate::base64::encode(text.as_bytes());
self.replies
.extend_from_slice(format!("\x1b]52;{field};{data}\x1b\\").as_bytes());
}
pub fn report_palette_color(&mut self, index: u8, spec: &str) {
self.replies
.extend_from_slice(format!("\x1b]4;{index};{spec}\x1b\\").as_bytes());
}
pub fn report_foreground(&mut self, spec: &str) {
self.replies
.extend_from_slice(format!("\x1b]10;{spec}\x1b\\").as_bytes());
}
pub fn report_background(&mut self, spec: &str) {
self.replies
.extend_from_slice(format!("\x1b]11;{spec}\x1b\\").as_bytes());
}
pub fn report_cursor_color(&mut self, spec: &str) {
self.replies
.extend_from_slice(format!("\x1b]12;{spec}\x1b\\").as_bytes());
}
pub fn viewport_line(&self, i: usize) -> &[Cell] {
let top = self.scrollback.len() - self.display_offset;
let idx = top + i;
if idx < self.scrollback.len() {
&self.scrollback[idx]
} else {
self.grid.row(idx - self.scrollback.len())
}
}
pub fn scroll_up(&mut self, n: usize) {
let target = (self.display_offset + n).min(self.scrollback.len());
self.set_display_offset(target);
}
pub fn scroll_down(&mut self, n: usize) {
let target = self.display_offset.saturating_sub(n);
self.set_display_offset(target);
}
pub fn scroll_to_bottom(&mut self) {
self.set_display_offset(0);
}
fn set_display_offset(&mut self, offset: usize) {
if self.on_alt {
return;
}
if offset != self.display_offset {
self.display_offset = offset;
self.mark_fully_damaged();
}
}
fn viewport_to_abs(&self, row: usize, col: usize) -> BufferPoint {
let top = self.scrollback.len() - self.display_offset;
let last = self.grid.rows().saturating_sub(1);
BufferPoint {
line: top + row.min(last),
col: col.min(self.grid.cols().saturating_sub(1)),
}
}
pub(crate) fn screen_link_at(&self, row: usize, col: usize) -> Option<Hyperlink> {
self.grid
.row_ref(row)
.link_at(col)
.cloned()
.map(Hyperlink::new)
}
pub(crate) fn screen_underline_color_at(&self, row: usize, col: usize) -> Color {
self.grid.row_ref(row).ucolor_at(col).unwrap_or_default()
}
pub(crate) fn viewport_link_at(&self, row: usize, col: usize) -> Option<Hyperlink> {
let idx = self.scrollback.len() - self.display_offset + row;
self.abs_row(idx).link_at(col).cloned().map(Hyperlink::new)
}
pub fn resize(&mut self, cols: usize, rows: usize) {
let cols = cols.clamp(MIN_COLUMNS, MAX_COLUMNS);
let rows = rows.clamp(1, MAX_ROWS);
let old_cols = self.grid.cols();
let old_rows = self.grid.rows();
let limit = self.scrollback_limit;
if (cols != old_cols || rows != old_rows)
&& (!self.normal_markers.is_empty() || !self.alt_markers.is_empty())
{
self.bump_marker_epoch();
}
self.invalidate_search_highlights();
if self.on_alt && (cols != old_cols || rows != old_rows) {
self.selection = None;
}
let dims = ReflowDims {
old_cols,
cols,
rows,
limit,
reflow: true,
};
let scrollback = std::mem::take(&mut self.scrollback);
if self.on_alt {
let old_base = scrollback.len();
let mut alt_pts: Vec<(usize, usize)> = self
.alt_markers
.iter()
.map(|m| (m.line - old_base, m.col))
.collect();
let alt_tracked_off = alt_pts.len();
alt_pts.extend(
self.alt_tracked
.iter()
.map(|p| (p.line.saturating_sub(old_base), p.col)),
);
let alt = self.grid.take_lines();
let r_alt = reflow_pane(
alt,
VecDeque::new(),
self.cursor.point(),
&alt_pts,
ReflowDims {
limit: 0,
reflow: false,
..dims
},
);
self.grid.set_screen(r_alt.screen, cols, rows);
self.cursor.set_point(r_alt.cursor, rows, cols);
let mut marker_pts: Vec<(usize, usize)> = self
.normal_markers
.iter()
.map(|m| (m.line, m.col))
.collect();
let tracked_off = marker_pts.len();
marker_pts.extend(self.normal_tracked.iter().map(|p| (p.line, p.col)));
let primary = self.alt_grid.take_lines();
let r = reflow_pane(
primary,
scrollback,
self.saved_cursor.point(),
&marker_pts,
dims,
);
self.alt_grid.set_screen(r.screen, cols, rows);
self.scrollback = r.scrollback;
self.saved_cursor.set_point(r.cursor, rows, cols);
for (i, m) in self.normal_markers.iter_mut().enumerate() {
m.line = r.extras[i].0.saturating_sub(r.evicted);
m.col = r.extras[i].1;
}
let mut ti = 0;
let evicted = r.evicted;
let extras = &r.extras;
self.normal_tracked.retain_mut(|p| {
let (line, col) = extras[tracked_off + ti];
ti += 1;
match line.checked_sub(evicted) {
Some(line) => {
p.line = line;
p.col = col;
true
}
None => false,
}
});
let new_base = self.scrollback.len();
let mut alt_disposed = Vec::new();
let mut i = 0;
self.alt_markers.retain_mut(|m| {
let (line, col) = r_alt.extras[i];
i += 1;
match line.checked_sub(r_alt.evicted) {
Some(row) if row < rows => {
m.line = new_base + row;
m.col = col;
true
}
_ => {
alt_disposed.push(m.id);
false
}
}
});
for id in alt_disposed {
self.events.push(TermEvent::MarkerDisposed(id));
}
let mut ai = 0;
let alt_extras = &r_alt.extras;
let alt_evicted = r_alt.evicted;
self.alt_tracked.retain_mut(|p| {
let (line, col) = alt_extras[alt_tracked_off + ai];
ai += 1;
match line.checked_sub(alt_evicted) {
Some(row) if row < rows => {
p.line = new_base + row;
p.col = col;
true
}
_ => false,
}
});
} else {
let sel_pts: Vec<(usize, usize)> = self
.selection
.as_ref()
.map(|s| {
vec![
(s.anchor.point.line, s.anchor.point.col),
(s.focus.point.line, s.focus.point.col),
]
})
.unwrap_or_default();
let mut pts = sel_pts.clone();
pts.extend(self.normal_markers.iter().map(|m| (m.line, m.col)));
let tracked_off = pts.len();
pts.extend(self.normal_tracked.iter().map(|p| (p.line, p.col)));
let primary = self.grid.take_lines();
let r = reflow_pane(primary, scrollback, self.cursor.point(), &pts, dims);
self.grid.set_screen(r.screen, cols, rows);
self.scrollback = r.scrollback;
self.cursor.set_point(r.cursor, rows, cols);
if let Some(sel) = &mut self.selection {
sel.anchor.point = BufferPoint {
line: r.extras[0].0.saturating_sub(r.evicted),
col: r.extras[0].1.min(cols - 1),
};
sel.focus.point = BufferPoint {
line: r.extras[1].0.saturating_sub(r.evicted),
col: r.extras[1].1.min(cols - 1),
};
}
let marker_off = sel_pts.len();
for (i, m) in self.normal_markers.iter_mut().enumerate() {
m.line = r.extras[marker_off + i].0.saturating_sub(r.evicted);
m.col = r.extras[marker_off + i].1;
}
let mut i = 0;
let evicted = r.evicted;
let extras = &r.extras;
self.normal_tracked.retain_mut(|p| {
let (line, col) = extras[tracked_off + i];
i += 1;
match line.checked_sub(evicted) {
Some(line) => {
p.line = line;
p.col = col;
true
}
None => false,
}
});
let alt = self.alt_grid.take_lines();
let r = reflow_pane(
alt,
VecDeque::new(),
(0, 0),
&[],
ReflowDims {
limit: 0,
reflow: false,
..dims
},
);
self.alt_grid.set_screen(r.screen, cols, rows);
}
if self.cursor.pending_wrap && self.cursor.col != cols - 1 {
self.cursor.pending_wrap = false;
self.cursor.col += 1;
}
self.scroll_top = 0;
self.scroll_bottom = rows - 1;
self.tabs = default_tabs(cols);
self.display_offset = self.display_offset.min(self.scrollback.len());
self.line_damage = vec![LineBounds::undamaged(cols); rows];
self.scroll = None;
self.mark_fully_damaged();
}
pub fn grid(&self) -> &Grid {
&self.grid
}
pub fn cursor(&self) -> &Cursor {
&self.cursor
}
pub fn bracketed_paste(&self) -> bool {
self.bracketed_paste
}
pub fn encode_key(&self, ev: KeyEvent) -> Option<Vec<u8>> {
encode_key(
&ev,
self.app_cursor_keys,
self.application_keypad,
self.kitty_flags,
)
}
pub fn encode_mouse(&self, ev: MouseEvent) -> Option<Vec<u8>> {
encode_mouse(&ev, self.mouse_protocol, self.mouse_encoding)
}
pub fn encode_paste(&self, text: &str) -> Vec<u8> {
encode_paste(text, self.bracketed_paste)
}
pub fn encode_focus(&self, focused: bool) -> Option<Vec<u8>> {
encode_focus(focused, self.focus_events)
}
pub fn drain_events(&mut self) -> Vec<TermEvent> {
std::mem::take(&mut self.events)
}
pub fn drain_replies(&mut self) -> Vec<u8> {
std::mem::take(&mut self.replies)
}
fn device_status_report(&mut self, param: u16) {
match param {
6 => {
let row = if self.origin_mode {
self.cursor.row.saturating_sub(self.scroll_top)
} else {
self.cursor.row
} + 1;
let col = self.cursor.col + 1;
self.replies
.extend_from_slice(format!("\x1b[{row};{col}R").as_bytes());
}
5 => self.replies.extend_from_slice(b"\x1b[0n"), _ => {}
}
}
fn kitty_dispatch(&mut self, lead: u8, params: &Params) {
match lead {
b'?' => self
.replies
.extend_from_slice(format!("\x1b[?{}u", self.kitty_flags).as_bytes()),
b'>' => {
const KITTY_STACK_CAP: usize = 16;
if self.kitty_stack.len() >= KITTY_STACK_CAP {
self.kitty_stack.remove(0); }
self.kitty_stack.push(self.kitty_flags);
self.kitty_flags = param_or(params, 0, 0) as u8;
}
b'<' => {
for _ in 0..param_or(params, 0, 1) {
self.kitty_flags = self.kitty_stack.pop().unwrap_or(0);
}
}
b'=' => {
let flags = param_or(params, 0, 0) as u8;
self.kitty_flags = match param_or(params, 1, 1) {
1 => flags,
2 => self.kitty_flags | flags,
3 => self.kitty_flags & !flags,
_ => self.kitty_flags,
};
}
_ => {}
}
}
fn decrqm(&mut self, mode: u16) {
let state = match mode {
1 => Some(self.app_cursor_keys),
2 => Some(!self.vt52_mode),
6 => Some(self.origin_mode),
3 => Some(self.grid.cols() == 132),
7 => Some(self.autowrap),
45 => Some(self.reverse_wraparound),
9 => Some(self.mouse_protocol == MouseProtocol::X10),
66 => Some(self.application_keypad),
12 => Some(self.cursor.blink),
25 => Some(self.cursor.visible),
1000 => Some(self.mouse_protocol == MouseProtocol::Normal),
1002 => Some(self.mouse_protocol == MouseProtocol::ButtonEvent),
1003 => Some(self.mouse_protocol == MouseProtocol::AnyEvent),
1004 => Some(self.focus_events),
1006 => Some(self.mouse_encoding == MouseEncoding::Sgr),
1015 => Some(self.mouse_encoding == MouseEncoding::Urxvt),
1005 => Some(self.mouse_encoding == MouseEncoding::Utf8),
1016 => Some(self.mouse_encoding == MouseEncoding::SgrPixels),
47 | 1047 | 1049 => Some(self.on_alt),
2004 => Some(self.bracketed_paste),
2026 => Some(self.synchronized_output),
2027 => Some(self.grapheme_clustering),
2031 => Some(self.color_scheme_updates),
9001 => Some(self.win32_input_mode),
_ => None,
};
let val = match state {
Some(true) => 1,
Some(false) => 2,
None => 0,
};
self.replies
.extend_from_slice(format!("\x1b[?{mode};{val}$y").as_bytes());
}
fn linefeed(&mut self) {
self.linefeed_inner(false);
}
fn linefeed_inner(&mut self, serves_wrap: bool) {
if self.newline_mode {
self.carriage_return();
}
if self.cursor.row == self.scroll_bottom {
if self.scroll_top == 0 && !self.on_alt {
let evicted = if self.scroll_bottom == self.grid.rows() - 1 {
let blank = self
.recycled_row
.take()
.unwrap_or_else(|| Row::from_cells(Vec::with_capacity(self.grid.cols())));
let evicted = self.grid.scroll_up_recycle(blank);
self.record_scroll(self.scroll_top, self.scroll_bottom, 1);
evicted
} else {
let below = self.scrollback.len() + self.scroll_bottom + 1;
self.selection_shift_below_margin(below);
self.markers_shift_below_margin(below);
self.tracked_shift_below_margin(below);
self.invalidate_search_highlights();
let evicted = self.grid.row_owned(0);
self.shift_region(
self.scroll_top,
self.scroll_bottom,
false,
true,
serves_wrap,
);
evicted
};
self.scrollback.push_back(evicted);
if self.display_offset > 0 {
self.display_offset = (self.display_offset + 1).min(self.scrollback.len());
}
if self.scrollback.len() > self.scrollback_limit {
self.recycled_row = self.scrollback.pop_front();
self.evicted_total += 1;
self.selection_evict_oldest();
self.invalidate_search_highlights();
self.markers_evict_oldest();
self.tracked_evict_oldest();
if self.display_offset > 0 {
self.display_offset -= 1;
self.mark_fully_damaged();
}
}
} else {
let base = self.scrollback.len();
self.selection_rotate_region(
base + self.scroll_top,
base + self.scroll_bottom,
true,
);
self.markers_rotate_region(base + self.scroll_top, base + self.scroll_bottom, true);
self.tracked_rotate_region(base + self.scroll_top, base + self.scroll_bottom, true);
self.invalidate_search_highlights();
self.shift_region(
self.scroll_top,
self.scroll_bottom,
false,
false,
serves_wrap,
);
}
} else if self.cursor.row + 1 < self.grid.rows() {
self.cursor.row += 1;
}
}
fn set_scroll_region(&mut self, top: usize, bottom: usize) {
let bottom = bottom.min(self.grid.rows());
if top >= bottom {
return;
}
self.scroll_top = top - 1;
self.scroll_bottom = bottom - 1;
self.goto(0, 0); }
fn save_alt_cursor(&mut self) {
self.saved_cursor = self.cursor;
}
fn restore_alt_cursor(&mut self) {
let visible = self.cursor.visible;
self.cursor = self.saved_cursor;
self.cursor.visible = visible;
}
fn switch_to_alt(&mut self) {
if self.on_alt {
return;
}
if !self.normal_markers.is_empty() || !self.alt_markers.is_empty() {
self.bump_marker_epoch();
}
std::mem::swap(&mut self.grid, &mut self.alt_grid);
self.grid.clear();
self.on_alt = true;
self.display_offset = 0; self.selection = None; self.invalidate_search_highlights(); self.mark_fully_damaged();
}
fn switch_to_primary(&mut self) {
if !self.on_alt {
return;
}
for m in self.alt_markers.drain(..) {
self.events.push(TermEvent::MarkerDisposed(m.id));
}
if !self.normal_markers.is_empty() {
self.bump_marker_epoch();
}
self.alt_tracked.clear();
std::mem::swap(&mut self.grid, &mut self.alt_grid);
self.on_alt = false;
self.display_offset = 0; self.selection = None; self.invalidate_search_highlights(); self.mark_fully_damaged();
}
fn enter_alt_screen(&mut self) {
if self.on_alt {
return;
}
self.save_alt_cursor();
self.switch_to_alt();
}
fn leave_alt_screen(&mut self) {
if !self.on_alt {
return;
}
self.switch_to_primary();
self.restore_alt_cursor();
}
fn reverse_index(&mut self) {
if self.cursor.row == self.scroll_top {
let base = self.scrollback.len();
self.selection_rotate_region(base + self.scroll_top, base + self.scroll_bottom, false);
self.markers_rotate_region(base + self.scroll_top, base + self.scroll_bottom, false);
self.tracked_rotate_region(base + self.scroll_top, base + self.scroll_bottom, false);
self.invalidate_search_highlights();
self.shift_region(self.scroll_top, self.scroll_bottom, true, false, false);
} else if self.cursor.row > 0 {
self.cursor.row -= 1;
}
}
fn save_cursor(&mut self) {
self.decsc = SavedCursor {
row: self.cursor.row,
col: self.cursor.col,
pen: self.cursor.pen,
pending_wrap: self.cursor.pending_wrap,
origin_mode: self.origin_mode,
charsets: self.charsets,
gl: self.gl,
};
}
fn restore_cursor(&mut self) {
let s = self.decsc;
self.cursor.row = s.row.min(self.grid.rows() - 1);
self.cursor.col = s.col.min(self.grid.cols() - 1);
self.cursor.pen = s.pen;
self.cursor.pending_wrap = s.pending_wrap;
self.origin_mode = s.origin_mode;
self.charsets = s.charsets;
self.gl = s.gl;
}
fn set_window_title(&mut self, title: String) {
self.window_title.clone_from(&title);
self.events.push(TermEvent::Title(title));
}
fn window_ops(&mut self, params: &Params) {
let axis = param_or(params, 1, 0);
let (window, icon) = (axis == 0 || axis == 2, axis == 0 || axis == 1);
match param_or(params, 0, 0) {
22 => {
if window {
let title = self.window_title.clone();
push_title(&mut self.window_title_stack, title);
}
if icon {
let name = self.icon_name.clone();
push_title(&mut self.icon_name_stack, name);
}
}
23 => {
if window && let Some(title) = self.window_title_stack.pop() {
self.set_window_title(title);
}
if icon && let Some(name) = self.icon_name_stack.pop() {
self.icon_name = name;
}
}
_ => {}
}
}
fn full_reset(&mut self) {
let replies = std::mem::take(&mut self.replies);
let mut events = std::mem::take(&mut self.events);
events.extend(
self.normal_markers
.iter()
.chain(&self.alt_markers)
.map(|m| TermEvent::MarkerDisposed(m.id)),
);
let (cols, rows) = (self.grid.cols(), self.grid.rows());
let word_separators = std::mem::take(&mut self.word_separators);
let next_tracked_id = self.next_tracked_id;
let marker_epoch = self.marker_epoch;
let next_marker_id = self.next_marker_id;
*self = Term::with_scrollback(cols, rows, self.scrollback_limit);
self.replies = replies;
self.events = events;
self.word_separators = word_separators;
self.next_tracked_id = next_tracked_id;
self.next_marker_id = next_marker_id;
self.marker_epoch = marker_epoch;
self.bump_marker_epoch();
self.mark_fully_damaged();
}
fn soft_reset(&mut self) {
self.cursor.visible = true;
self.cursor.pen = Pen::default();
self.scroll_top = 0;
self.scroll_bottom = self.grid.rows() - 1;
self.origin_mode = false;
self.app_cursor_keys = false;
self.bracketed_paste = false;
self.grapheme_clustering = false; self.autowrap = true; self.insert_mode = false;
self.charsets = [Charset::Ascii; 4];
self.gl = 0;
self.decsc = SavedCursor::default();
}
fn carriage_return(&mut self) {
self.cursor.col = 0;
self.cursor.pending_wrap = false;
}
fn set_cursor_style(&mut self, param: u16) {
let (shape, blink) = match param {
0 | 2 => (CursorShape::Block, false),
1 => (CursorShape::Block, true),
3 => (CursorShape::Underline, true),
4 => (CursorShape::Underline, false),
5 => (CursorShape::Bar, true),
6 => (CursorShape::Bar, false),
_ => return,
};
self.cursor.shape = shape;
self.cursor.blink = blink;
}
fn backspace(&mut self) {
self.cursor.pending_wrap = false;
if self.cursor.col > 0 {
self.cursor.col -= 1;
return;
}
if self.reverse_wraparound
&& self.cursor.row > self.scroll_top
&& self.cursor.row <= self.scroll_bottom
{
let prev = self.cursor.row - 1;
let last = self.grid.cols() - 1;
if self.grid.row_ref(prev).is_wrapped() {
self.grid.row_mut(prev).set_wrapped(false);
self.cursor.row = prev;
self.cursor.col = last;
}
}
}
fn wrapline(&mut self) {
self.linefeed_inner(true);
self.cursor.col = 0;
self.cursor.pending_wrap = false;
}
fn put_tab(&mut self) {
let cols = self.grid.cols();
let mut col = self.cursor.col;
while col + 1 < cols {
col += 1;
if self.tabs[col] {
break;
}
}
self.cursor.col = col;
self.cursor.pending_wrap = false;
}
fn set_tab_stop(&mut self) {
let col = self.cursor.col;
self.tabs[col] = true;
}
fn clear_tab_stop(&mut self, mode: u16) {
match mode {
0 => {
let col = self.cursor.col;
self.tabs[col] = false;
}
3 => self.tabs.iter_mut().for_each(|t| *t = false),
_ => {}
}
}
fn pen_ext_attrs(&self) -> ExtAttrs {
let ucolor = self.cursor.pen.underline_color;
let armed =
ucolor != Color::Default && self.cursor.pen.flags.contains(CellFlags::UNDERLINE);
ExtAttrs::from_pen(self.current_link.clone(), armed.then_some(ucolor))
}
fn free_cell(&mut self, row: usize, col: usize) {
let bg = self.cursor.pen.bg;
let cell = self.grid.cell_mut(row, col);
cell.reset();
cell.set_bg(bg);
self.grid.row_mut(row).purge_side_maps(col..col + 1);
self.damage_span(row, col, col);
}
fn wrapline_advances(&self) -> bool {
self.cursor.row == self.scroll_bottom || self.cursor.row + 1 < self.grid.rows()
}
fn vacate_for_wrap(&mut self, row: usize, col: usize) {
if col > 0 && self.grid.cell(row, col).is_wide_spacer() {
self.free_cell(row, col - 1);
}
let mut vacated = self.cursor.pen.cell(' ');
vacated.set_leading_spacer();
*self.grid.cell_mut(row, col) = vacated;
self.begin_wrap(row);
let ext = self.pen_ext_attrs();
self.grid.row_mut(row).set_ext_attrs(col, ext);
self.damage_span(row, col, col);
}
fn write_glyph(&mut self, c: char, width: usize) {
debug_assert!(
width <= 2,
"write_glyph({c:?}, {width}) — the cell model represents at most a pair"
);
let cols = self.grid.cols();
if self.cursor.pending_wrap {
let row = self.cursor.row;
if self.wrapline_advances() {
self.begin_wrap(row);
}
self.wrapline();
}
if width == 2 && self.cursor.col + 1 >= cols {
if !self.autowrap {
return;
}
if self.wrapline_advances() {
self.vacate_for_wrap(self.cursor.row, cols - 1);
}
self.wrapline();
}
if self.insert_mode {
self.insert_chars(width);
}
let (row, col) = (self.cursor.row, self.cursor.col);
if col <= 1 && self.wrapped_pair_at_row_start(row) && !(col == 0 && width == 2) {
self.void_wrap_artefact_above(row);
}
let last = col + width - 1;
if col > 0 && self.grid.cell(row, col).is_wide_spacer() {
self.free_cell(row, col - 1);
}
if last + 1 < cols && self.grid.cell(row, last).is_wide() {
self.free_cell(row, last + 1);
}
let mut cell = self.cursor.pen.cell(c);
if width == 2 {
cell.insert_flags(CellFlags::WIDE_CHAR);
}
*self.grid.cell_mut(row, col) = cell;
let ext = self.pen_ext_attrs();
self.grid.row_mut(row).set_ext_attrs(col, ext.clone());
if width == 2 && col + 1 < cols {
let mut spacer = self.cursor.pen.cell(' ');
spacer.insert_flags(CellFlags::WIDE_CHAR_SPACER);
*self.grid.cell_mut(row, col + 1) = spacer;
self.grid.row_mut(row).set_ext_attrs(col + 1, ext);
}
self.damage_span(row, col, col + width - 1);
let new_col = col + width;
if new_col >= cols {
self.cursor.col = cols - 1;
self.cursor.pending_wrap = self.autowrap;
} else {
self.cursor.col = new_col;
}
}
fn push_combining(&mut self, c: char) {
let row = self.cursor.row;
let mut col = if self.cursor.pending_wrap {
self.cursor.col
} else {
self.cursor.col.saturating_sub(1)
};
if self.grid.cell(row, col).is_wide_spacer() {
col = col.saturating_sub(1);
}
self.grid.row_mut(row).push_combining(col, c);
self.damage_span(row, col, col);
}
fn try_grapheme_join(&mut self, c: char) -> bool {
let row = self.cursor.row;
let col = if self.cursor.pending_wrap {
self.cursor.col
} else if self.cursor.col == 0 {
return false; } else {
self.cursor.col - 1
};
let col = if self.grid.cell(row, col).is_wide_spacer() {
col.saturating_sub(1)
} else {
col
};
let mut prev = String::new();
prev.push(self.grid.cell(row, col).c());
if let Some(marks) = self.grid.row_ref(row).combining_at(col) {
prev.extend(marks.iter().copied());
}
if !crate::grapheme::grapheme_extends(&prev, c) {
return false;
}
self.grid.row_mut(row).push_combining(col, c);
let cluster_w = {
prev.push(c);
UnicodeWidthStr::width(prev.as_str())
};
if cluster_w == 2 && !self.grid.cell(row, col).is_wide() {
self.promote_cluster_to_wide(row, col);
} else if cluster_w == 1 && self.grid.cell(row, col).is_wide() {
self.demote_cluster_to_narrow(row, col);
}
self.damage_span(row, col, col);
true
}
fn demote_cluster_to_narrow(&mut self, row: usize, col: usize) {
let cols = self.grid.cols();
self.grid
.cell_mut(row, col)
.remove_flags(CellFlags::WIDE_CHAR);
if col + 1 < cols {
self.free_cell(row, col + 1); }
self.cursor.pending_wrap = false;
self.cursor.col = (col + 1).min(cols - 1);
self.damage_span(row, col, (col + 1).min(cols - 1));
}
fn promote_cluster_to_wide(&mut self, row: usize, col: usize) {
let cols = self.grid.cols();
if col + 1 >= cols {
self.relocate_cluster_wide(row, col);
return;
}
if self.grid.cell(row, col + 1).is_wide() && col + 2 < cols {
self.free_cell(row, col + 2);
}
self.grid
.cell_mut(row, col)
.insert_flags(CellFlags::WIDE_CHAR);
let ext = self.grid.row_ref(row).ext_attrs_at(col);
let mut spacer = self.cursor.pen.cell(' ');
spacer.insert_flags(CellFlags::WIDE_CHAR_SPACER);
*self.grid.cell_mut(row, col + 1) = spacer;
self.grid.row_mut(row).set_ext_attrs(col + 1, ext);
let new_col = col + 2;
if new_col >= cols {
self.cursor.col = cols - 1;
self.cursor.pending_wrap = self.autowrap;
} else {
self.cursor.col = new_col;
}
self.damage_span(row, col, col + 1);
}
fn relocate_cluster_wide(&mut self, row: usize, col: usize) {
let cols = self.grid.cols();
if cols < 2 || !self.autowrap || !self.wrapline_advances() {
return;
}
let base = *self.grid.cell(row, col);
let marks: Vec<char> = self
.combining_at(row, col)
.map(<[char]>::to_vec)
.unwrap_or_default();
let ext = self.grid.row_ref(row).ext_attrs_at(col);
self.vacate_for_wrap(row, col);
self.wrapline();
let nr = self.cursor.row;
if 2 < cols && self.grid.cell(nr, 1).is_wide() {
self.free_cell(nr, 2);
}
let mut lead = base;
lead.set_combined(false);
lead.insert_flags(CellFlags::WIDE_CHAR);
*self.grid.cell_mut(nr, 0) = lead;
for m in marks {
self.grid.row_mut(nr).push_combining(0, m);
}
self.grid.row_mut(nr).set_ext_attrs(0, ext.clone());
let mut spacer = self.cursor.pen.cell(' ');
spacer.insert_flags(CellFlags::WIDE_CHAR_SPACER);
*self.grid.cell_mut(nr, 1) = spacer;
self.grid.row_mut(nr).set_ext_attrs(1, ext);
if cols <= 2 {
self.cursor.col = cols - 1;
self.cursor.pending_wrap = self.autowrap;
} else {
self.cursor.col = 2;
self.cursor.pending_wrap = false;
}
self.damage_span(nr, 0, 1);
}
fn move_up(&mut self, n: usize) {
self.cursor.row = self.cursor.row.saturating_sub(n);
self.cursor.pending_wrap = false;
}
fn move_down(&mut self, n: usize) {
self.cursor.row = (self.cursor.row + n).min(self.grid.rows() - 1);
self.cursor.pending_wrap = false;
}
fn move_forward(&mut self, n: usize) {
self.cursor.col = (self.cursor.col + n).min(self.grid.cols() - 1);
self.cursor.pending_wrap = false;
}
fn move_back(&mut self, n: usize) {
self.cursor.col = self.cursor.col.saturating_sub(n);
self.cursor.pending_wrap = false;
}
fn set_col(&mut self, col: usize) {
self.cursor.col = col.min(self.grid.cols() - 1);
self.cursor.pending_wrap = false;
}
fn set_row(&mut self, row: usize) {
self.cursor.row = row.min(self.grid.rows() - 1);
self.cursor.pending_wrap = false;
}
fn goto(&mut self, row: usize, col: usize) {
let (offset, max_row) = if self.origin_mode {
(self.scroll_top, self.scroll_bottom)
} else {
(0, self.grid.rows() - 1)
};
self.cursor.row = (row + offset).min(max_row);
self.cursor.col = col.min(self.grid.cols() - 1);
self.cursor.pending_wrap = false;
}
fn clear_cells(&mut self, row: usize, from: usize, to: usize) {
let cols = self.grid.cols();
if from <= 1 && to > from && self.wrapped_pair_at_row_start(row) {
self.void_wrap_artefact_above(row);
}
if from > 0 && self.grid.cell(row, from).is_wide_spacer() {
self.free_cell(row, from - 1);
}
if to > from && to < cols && self.grid.cell(row, to - 1).is_wide() {
self.free_cell(row, to);
}
let bg = self.cursor.pen.bg;
for col in from..to {
let cell = self.grid.cell_mut(row, col);
cell.reset();
cell.set_bg(bg);
}
self.grid.row_mut(row).purge_side_maps(from..to);
if to > from {
self.damage_span(row, from, to - 1);
}
}
fn begin_wrap(&mut self, row: usize) {
self.grid.row_mut(row).set_wrapped(true);
let last = self.grid.cols() - 1;
self.damage_span(row, last, last);
}
fn end_wrap(&mut self, row: usize) {
self.grid.row_mut(row).set_wrapped(false);
let last = self.grid.cols() - 1;
self.damage_span(row, last, last);
self.grid.cell_mut(row, last).clear_leading_spacer();
}
fn void_wrap_artefact_above(&mut self, row: usize) {
if row > 0 {
let last = self.grid.cols() - 1;
if self.grid.cell(row - 1, last).is_leading_spacer() {
self.grid.cell_mut(row - 1, last).clear_leading_spacer();
self.damage_span(row - 1, last, last);
}
} else if !self.on_alt
&& let Some(cell) = self.scrollback.back_mut().and_then(|r| r.last_mut())
{
cell.clear_leading_spacer();
}
}
fn wrapped_pair_at_row_start(&self, row: usize) -> bool {
self.grid.cell(row, 0).is_wide()
}
fn drop_artefact_if_erased(&mut self, row: usize, from: usize, to: usize) {
let last = self.grid.cols() - 1;
if from <= last && to > last {
self.grid.cell_mut(row, last).clear_leading_spacer();
}
}
fn shift_region(
&mut self,
top: usize,
bottom: usize,
down: bool,
evicts_to_scrollback: bool,
serves_wrap: bool,
) {
if down {
self.grid.scroll_down_region(top, bottom);
} else {
self.grid.scroll_up_region(top, bottom);
}
self.record_scroll(top, bottom, if down { -1 } else { 1 });
if top > 0 {
self.end_wrap(top - 1);
} else if !evicts_to_scrollback && !self.on_alt {
if let Some(row) = self.scrollback.back_mut() {
row.set_wrapped(false);
if let Some(cell) = row.last_mut() {
cell.clear_leading_spacer();
}
}
}
let orphaned = if serves_wrap {
None
} else if down {
Some(bottom)
} else if bottom + 1 < self.grid.rows() {
bottom.checked_sub(1)
} else {
None
};
if let Some(row) = orphaned {
self.end_wrap(row);
}
}
fn erase_display(&mut self, mode: u16) {
let (cols, rows) = (self.grid.cols(), self.grid.rows());
let (cr, cc) = (self.cursor.row, self.cursor.col);
match mode {
0 => {
self.clear_cells(cr, cc, cols);
self.end_wrap(cr);
for row in (cr + 1)..rows {
self.clear_cells(row, 0, cols);
self.end_wrap(row);
self.dispose_markers_on_row(row);
}
}
1 => {
for row in 0..cr {
self.clear_cells(row, 0, cols);
self.end_wrap(row);
self.dispose_markers_on_row(row);
}
self.clear_cells(cr, 0, cc + 1);
self.drop_artefact_if_erased(cr, 0, cc + 1);
if cc + 1 == cols {
self.end_wrap(cr);
}
}
2 => {
for row in 0..rows {
self.clear_cells(row, 0, cols);
self.end_wrap(row);
self.dispose_markers_on_row(row);
}
}
_ => {}
}
}
fn erase_line(&mut self, mode: u16) {
let cols = self.grid.cols();
let (cr, cc) = (self.cursor.row, self.cursor.col);
match mode {
0 => {
self.clear_cells(cr, cc, cols);
self.end_wrap(cr);
}
1 => {
self.clear_cells(cr, 0, cc + 1);
self.drop_artefact_if_erased(cr, 0, cc + 1);
}
2 => {
self.clear_cells(cr, 0, cols);
self.end_wrap(cr);
}
_ => {}
}
}
fn erase_chars(&mut self, n: usize) {
let cols = self.grid.cols();
let (row, col) = (self.cursor.row, self.cursor.col);
let to = (col + n).min(cols);
self.clear_cells(row, col, to);
self.end_wrap(row);
}
fn insert_chars(&mut self, n: usize) {
let cols = self.grid.cols();
let (r, col) = (self.cursor.row, self.cursor.col);
let n = n.min(cols - col);
if n == 0 {
return;
}
if col <= 1 && self.wrapped_pair_at_row_start(r) {
self.void_wrap_artefact_above(r);
}
let bg = self.cursor.pen.bg;
let row = self.grid.row_mut(r);
row.copy_within(col..cols - n, col + n);
row.move_maps(col..cols - n, col + n);
for cell in &mut row[col..col + n] {
cell.reset();
cell.set_bg(bg);
}
if col > 0 && self.grid.cell(r, col - 1).is_wide() {
self.free_cell(r, col - 1);
}
if col + n < cols && self.grid.cell(r, col + n).is_wide_spacer() {
self.free_cell(r, col + n);
}
if self.grid.cell(r, cols - 1).is_wide() {
self.free_cell(r, cols - 1);
}
self.damage_span(r, col, cols - 1);
}
fn delete_chars(&mut self, n: usize) {
let cols = self.grid.cols();
let (r, col) = (self.cursor.row, self.cursor.col);
let n = n.min(cols - col);
if n == 0 {
return;
}
self.end_wrap(r);
if col <= 1 && self.wrapped_pair_at_row_start(r) {
self.void_wrap_artefact_above(r);
}
let bg = self.cursor.pen.bg;
let row = self.grid.row_mut(r);
row.copy_within(col + n..cols, col);
row.move_maps(col + n..cols, col);
for cell in &mut row[cols - n..cols] {
cell.reset();
cell.set_bg(bg);
}
if col > 0 && self.grid.cell(r, col - 1).is_wide() {
self.free_cell(r, col - 1);
}
if self.grid.cell(r, col).is_wide_spacer() {
self.free_cell(r, col);
}
self.damage_span(r, col, cols - 1);
}
fn scroll_region_lines(&mut self, top: usize, bottom: usize, n: usize, down: bool) {
let height = bottom - top + 1;
let n = n.min(height);
if n == 0 {
return;
}
let base = self.scrollback.len();
for _ in 0..n {
self.shift_region(top, bottom, down, false, false);
self.selection_rotate_region(base + top, base + bottom, !down);
self.markers_rotate_region(base + top, base + bottom, !down);
self.tracked_rotate_region(base + top, base + bottom, !down);
}
self.invalidate_search_highlights();
let bg = self.cursor.pen.bg;
let (fill_top, fill_end) = if down {
(top, top + n)
} else {
(bottom + 1 - n, bottom + 1)
};
let cols = self.grid.cols();
for r in fill_top..fill_end {
for c in 0..cols {
let cell = self.grid.cell_mut(r, c);
cell.reset();
cell.set_bg(bg);
}
}
}
fn scroll_up_lines(&mut self, n: usize) {
self.scroll_region_lines(self.scroll_top, self.scroll_bottom, n, false);
}
fn scroll_down_lines(&mut self, n: usize) {
self.scroll_region_lines(self.scroll_top, self.scroll_bottom, n, true);
}
fn insert_lines(&mut self, n: usize) {
let cur = self.cursor.row;
if cur < self.scroll_top || cur > self.scroll_bottom {
return;
}
self.scroll_region_lines(cur, self.scroll_bottom, n, true);
}
fn delete_lines(&mut self, n: usize) {
let cur = self.cursor.row;
if cur < self.scroll_top || cur > self.scroll_bottom {
return;
}
self.scroll_region_lines(cur, self.scroll_bottom, n, false);
}
fn sgr(&mut self, params: &Params) {
let pen = &mut self.cursor.pen;
let mut iter = params.iter();
while let Some(param) = iter.next() {
let code = param.first().copied().unwrap_or(0);
match code {
0 => pen.reset(),
1 => pen.flags.insert(CellFlags::BOLD),
2 => pen.flags.insert(CellFlags::DIM),
3 => pen.flags.insert(CellFlags::ITALIC),
4 => pen.flags.insert(CellFlags::UNDERLINE),
5 => pen.flags.insert(CellFlags::BLINK),
7 => pen.flags.insert(CellFlags::INVERSE),
8 => pen.flags.insert(CellFlags::HIDDEN),
9 => pen.flags.insert(CellFlags::STRIKETHROUGH),
22 => pen.flags.remove(CellFlags::BOLD | CellFlags::DIM),
23 => pen.flags.remove(CellFlags::ITALIC),
24 => pen.flags.remove(CellFlags::UNDERLINE),
25 => pen.flags.remove(CellFlags::BLINK),
27 => pen.flags.remove(CellFlags::INVERSE),
28 => pen.flags.remove(CellFlags::HIDDEN),
29 => pen.flags.remove(CellFlags::STRIKETHROUGH),
30..=37 => pen.fg = Color::Indexed((code - 30) as u8),
38 => {
if let Some(c) = parse_extended_color(param, &mut iter) {
pen.fg = c;
}
}
39 => pen.fg = Color::Default,
40..=47 => pen.bg = Color::Indexed((code - 40) as u8),
48 => {
if let Some(c) = parse_extended_color(param, &mut iter) {
pen.bg = c;
}
}
49 => pen.bg = Color::Default,
58 => {
if let Some(c) = parse_extended_color(param, &mut iter) {
pen.underline_color = c;
}
}
59 => pen.underline_color = Color::Default,
90..=97 => pen.fg = Color::Indexed((code - 90 + 8) as u8),
100..=107 => pen.bg = Color::Indexed((code - 100 + 8) as u8),
_ => {}
}
}
}
}
fn cap_scroll(op: ScrollOp) -> ScrollOp {
let height = op.bottom.saturating_sub(op.top).saturating_add(1) as isize;
let bound = height.min(MAX_SCROLL_COUNT);
ScrollOp {
count: op.count.clamp(-bound, bound),
..op
}
}
fn parse_extended_color<'a, I>(param: &[u16], iter: &mut I) -> Option<Color>
where
I: Iterator<Item = &'a [u16]>,
{
if param.len() > 1 {
match param[1] {
2 => {
let off = if param.len() >= 6 { 3 } else { 2 };
let r = *param.get(off)? as u8;
let g = *param.get(off + 1)? as u8;
let b = *param.get(off + 2)? as u8;
Some(Color::Rgb(r, g, b))
}
5 => Some(Color::Indexed(*param.get(2)? as u8)),
_ => None,
}
} else {
match iter.next()?.first().copied()? {
2 => {
let r = iter.next()?.first().copied()? as u8;
let g = iter.next()?.first().copied()? as u8;
let b = iter.next()?.first().copied()? as u8;
Some(Color::Rgb(r, g, b))
}
5 => Some(Color::Indexed(iter.next()?.first().copied()? as u8)),
_ => None,
}
}
}
#[derive(Clone, Copy)]
struct ReflowDims {
old_cols: usize,
cols: usize,
rows: usize,
limit: usize,
reflow: bool,
}
struct PaneReflow {
screen: Vec<Row>,
scrollback: VecDeque<Row>,
cursor: (usize, usize),
extras: Vec<(usize, usize)>,
evicted: usize,
}
fn reflow_pane(
screen: Vec<Row>,
scrollback: VecDeque<Row>,
point: (usize, usize),
extra_abs: &[(usize, usize)],
dims: ReflowDims,
) -> PaneReflow {
let scroll_len = scrollback.len();
let mut all: Vec<Row> = scrollback.into();
all.extend(screen);
let mut pts: Vec<(usize, usize)> = Vec::with_capacity(1 + extra_abs.len());
pts.push((scroll_len + point.0, point.1));
pts.extend_from_slice(extra_abs);
let pts = if dims.reflow && dims.cols != dims.old_cols {
let (reflowed, np) = crate::grid::reflow(all, dims.cols, &pts);
all = reflowed;
np
} else {
pts
};
let cursor_abs = pts[0].0 + usize::from(pts[0].1 == dims.cols);
if dims.limit > 0 {
while all.len() <= cursor_abs {
all.push(Row::blank(dims.cols));
}
}
let split = all.len().saturating_sub(dims.rows);
let history: Vec<Row> = all.drain(0..split).collect();
let mut sb: VecDeque<Row> = history.into();
let mut dropped = 0usize;
while sb.len() > dims.limit {
sb.pop_front();
dropped += 1;
}
let cursor_row = pts[0].0.saturating_sub(split);
let cursor = if pts[0].1 == dims.cols {
(cursor_row + 1, 0)
} else {
(cursor_row, pts[0].1)
};
let max_line = split + dims.rows - 1;
PaneReflow {
cursor,
extras: pts[1..]
.iter()
.map(|&(l, c)| (l.min(max_line), c))
.collect(),
evicted: dropped,
screen: all,
scrollback: sb,
}
}
fn default_tabs(cols: usize) -> Vec<bool> {
(0..cols).map(|i| i % 8 == 0).collect()
}
fn push_title(stack: &mut Vec<String>, value: String) {
if stack.len() >= TITLE_STACK_DEPTH {
stack.remove(0);
}
stack.push(value);
}
fn param_or(params: &Params, idx: usize, default: u16) -> u16 {
match params.iter().nth(idx).and_then(|p| p.first().copied()) {
Some(v) if v != 0 => v,
_ => default,
}
}
const fn version_number(version: &str) -> u32 {
let bytes = version.as_bytes();
let mut parts = [0u32; 3];
let mut part = 0usize;
let mut i = 0usize;
while i < bytes.len() {
let b = bytes[i];
if b == b'-' || b == b'+' {
break;
} else if b == b'.' {
part += 1;
if part >= 3 {
break;
}
} else if b.is_ascii_digit() {
parts[part] = parts[part] * 10 + (b - b'0') as u32;
}
i += 1;
}
parts[0] * 10_000 + parts[1] * 100 + parts[2]
}
const DA2_TERMINAL_TYPE: u32 = 1;
const DA2_ROM_CARTRIDGE: u32 = 0;
const DA2_VERSION: u32 = version_number(env!("CARGO_PKG_VERSION"));
impl Term {
fn set_dec_private_mode(&mut self, action: char, mode: u16) {
match (action, mode) {
('h', 1049) => self.enter_alt_screen(),
('l', 1049) => self.leave_alt_screen(),
('h', 47) | ('h', 1047) => self.switch_to_alt(),
('l', 47) | ('l', 1047) => self.switch_to_primary(),
('h', 1048) => self.save_alt_cursor(),
('l', 1048) => self.restore_alt_cursor(),
('h', 6) => {
self.origin_mode = true;
self.goto(0, 0);
}
('l', 6) => self.origin_mode = false, ('h', 7) => self.autowrap = true, ('l', 7) => self.autowrap = false,
('h', 45) => self.reverse_wraparound = true, ('l', 45) => self.reverse_wraparound = false,
('h', 3) => self.events.push(TermEvent::ColumnMode { cols: 132 }),
('l', 3) => self.events.push(TermEvent::ColumnMode { cols: 80 }),
('h', 25) => self.cursor.visible = true, ('l', 25) => self.cursor.visible = false, ('h', 12) => self.cursor.blink = true, ('l', 12) => self.cursor.blink = false,
('h', 2004) => self.bracketed_paste = true,
('l', 2004) => self.bracketed_paste = false,
('h', 2026) => self.synchronized_output = true, ('l', 2026) => self.synchronized_output = false,
('h', 2027) => self.grapheme_clustering = true, ('l', 2027) => self.grapheme_clustering = false,
('h', 2031) => self.color_scheme_updates = true, ('l', 2031) => self.color_scheme_updates = false,
('h', 9001) => self.win32_input_mode = true, ('l', 9001) => self.win32_input_mode = false,
('h', 1) => self.app_cursor_keys = true, ('l', 1) => self.app_cursor_keys = false,
('h', 66) => self.application_keypad = true, ('l', 66) => self.application_keypad = false,
('l', 2) => self.vt52_mode = true,
('h', 9) => self.mouse_protocol = MouseProtocol::X10, ('h', 1000) => self.mouse_protocol = MouseProtocol::Normal,
('h', 1002) => self.mouse_protocol = MouseProtocol::ButtonEvent,
('h', 1003) => self.mouse_protocol = MouseProtocol::AnyEvent,
('l', 9) | ('l', 1000) | ('l', 1002) | ('l', 1003) => {
self.mouse_protocol = MouseProtocol::Off
}
('h', 1006) => self.mouse_encoding = MouseEncoding::Sgr,
('l', 1006) => self.mouse_encoding = MouseEncoding::Default,
('h', 1015) => self.mouse_encoding = MouseEncoding::Urxvt,
('l', 1015) => self.mouse_encoding = MouseEncoding::Default,
('h', 1005) => self.mouse_encoding = MouseEncoding::Utf8,
('l', 1005) => self.mouse_encoding = MouseEncoding::Default,
('h', 1016) => self.mouse_encoding = MouseEncoding::SgrPixels,
('l', 1016) => self.mouse_encoding = MouseEncoding::Default,
('h', 1004) => self.focus_events = true,
('l', 1004) => self.focus_events = false,
_ => {} }
}
fn vt52_dispatch(&mut self, byte: u8) {
match byte {
b'A' => self.move_up(1), b'B' => self.move_down(1), b'C' => self.move_forward(1), b'D' => self.move_back(1), b'H' => self.goto(0, 0), b'I' => self.reverse_index(), b'J' => self.erase_display(0), b'K' => self.erase_line(0), b'Y' => self.vt52_y_pending = 2, b'Z' => self.replies.extend_from_slice(b"\x1b/Z"),
b'=' => self.application_keypad = true, b'>' => self.application_keypad = false, b'<' => self.vt52_mode = false, b'c' => self.full_reset(),
b'F' | b'G' => {}
_ => {} }
}
fn vt52_take_coord(&mut self, c: char) {
let coord = (c as usize).saturating_sub(0x20);
if self.vt52_y_pending == 2 {
self.vt52_y_row = coord;
self.vt52_y_pending = 1;
} else {
self.vt52_y_pending = 0;
self.goto(self.vt52_y_row, coord);
}
}
fn link_for_id(&mut self, id: &str, uri: &str) -> std::sync::Arc<str> {
let key = format!("{id};;{uri}");
if let Some(live) = self.link_ids.get(&key).and_then(std::sync::Weak::upgrade) {
return live;
}
if self.link_ids.len() >= self.link_ids_sweep_at {
self.link_ids.retain(|_, weak| weak.strong_count() > 0);
self.link_ids_sweep_at = (self.link_ids.len() * 2).max(LINK_IDS_FIRST_SWEEP);
}
let fresh: std::sync::Arc<str> = std::sync::Arc::from(uri);
self.link_ids.insert(key, std::sync::Arc::downgrade(&fresh));
fresh
}
}
impl Perform for Term {
fn print(&mut self, c: char) {
if self.vt52_y_pending > 0 {
self.vt52_take_coord(c);
return;
}
let c = self.charsets[self.gl].map(c);
if self.grapheme_clustering && self.try_grapheme_join(c) {
return;
}
match c.width() {
Some(0) => self.push_combining(c),
None => {}
Some(width) => self.write_glyph(c, width.min(2)),
}
}
fn execute(&mut self, byte: u8) {
match byte {
b'\n' | 0x0b | 0x0c => self.linefeed(),
b'\r' => self.carriage_return(),
0x08 => self.backspace(),
b'\t' => self.put_tab(),
0x07 => self.events.push(TermEvent::Bell), 0x0e => self.gl = 1, 0x0f => self.gl = 0, _ => {}
}
}
fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {
if action == 'u'
&& let Some(&lead) = intermediates.first()
&& matches!(lead, b'>' | b'<' | b'=' | b'?')
{
self.kitty_dispatch(lead, params);
return;
}
if intermediates.first() == Some(&b'?') {
if action == 'p' && intermediates.contains(&b'$') {
self.decrqm(param_or(params, 0, 0));
return;
}
if action == 'n' {
if param_or(params, 0, 0) == 996 {
self.events.push(TermEvent::ColorSchemeQuery);
}
return;
}
for mode in params.iter().filter_map(|p| p.first().copied()) {
self.set_dec_private_mode(action, mode);
}
return;
}
if intermediates.first() == Some(&b'!') && action == 'p' {
self.soft_reset();
return;
}
if intermediates.first() == Some(&b' ') && action == 'q' {
let param = params.iter().next().and_then(|p| p.first().copied());
self.set_cursor_style(param.unwrap_or(1));
return;
}
if intermediates == [b'>'] && action == 'c' {
if param_or(params, 0, 0) == 0 {
self.replies.extend_from_slice(
format!("\x1b[>{DA2_TERMINAL_TYPE};{DA2_VERSION};{DA2_ROM_CARTRIDGE}c")
.as_bytes(),
);
}
return;
}
if !intermediates.is_empty() {
return;
}
match action {
'A' => self.move_up(param_or(params, 0, 1) as usize),
'B' | 'e' => self.move_down(param_or(params, 0, 1) as usize),
'C' | 'a' => self.move_forward(param_or(params, 0, 1) as usize),
'D' => self.move_back(param_or(params, 0, 1) as usize),
'G' | '`' => self.set_col(param_or(params, 0, 1) as usize - 1),
'd' => self.set_row(param_or(params, 0, 1) as usize - 1),
'H' | 'f' => {
let row = param_or(params, 0, 1) as usize - 1;
let col = param_or(params, 1, 1) as usize - 1;
self.goto(row, col);
}
'J' => self.erase_display(param_or(params, 0, 0)),
'K' => self.erase_line(param_or(params, 0, 0)),
'X' => self.erase_chars(param_or(params, 0, 1) as usize),
'@' => self.insert_chars(param_or(params, 0, 1) as usize),
'P' => self.delete_chars(param_or(params, 0, 1) as usize),
'S' => self.scroll_up_lines(param_or(params, 0, 1) as usize),
'T' => self.scroll_down_lines(param_or(params, 0, 1) as usize),
'L' => self.insert_lines(param_or(params, 0, 1) as usize),
'M' => self.delete_lines(param_or(params, 0, 1) as usize),
'g' => self.clear_tab_stop(param_or(params, 0, 0)),
'r' => {
let rows = self.grid.rows() as u16;
let top = param_or(params, 0, 1) as usize;
let bottom = param_or(params, 1, rows) as usize;
self.set_scroll_region(top, bottom);
}
'm' => self.sgr(params),
's' => self.save_cursor(), 't' => self.window_ops(params), 'u' => self.restore_cursor(), 'c' => self.replies.extend_from_slice(b"\x1b[?62;22c"),
'n' => self.device_status_report(param_or(params, 0, 0)),
'h' => {
for m in params.iter().filter_map(|p| p.first().copied()) {
match m {
4 => self.insert_mode = true,
20 => self.newline_mode = true,
_ => {}
}
}
}
'l' => {
for m in params.iter().filter_map(|p| p.first().copied()) {
match m {
4 => self.insert_mode = false,
20 => self.newline_mode = false,
_ => {}
}
}
}
_ => {}
}
}
fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
if self.vt52_mode && intermediates.is_empty() {
self.vt52_dispatch(byte);
return;
}
if let Some(&i) = intermediates.first() {
if matches!(i, b'(' | b')') {
let set = match byte {
b'0' => Charset::DecSpecialGraphics,
b'A' => Charset::Uk,
b'B' => Charset::Ascii,
_ => return, };
self.charsets[if i == b'(' { 0 } else { 1 }] = set;
}
return;
}
match byte {
b'D' => self.linefeed(), b'E' => {
self.carriage_return();
self.linefeed();
}
b'H' => self.set_tab_stop(), b'M' => self.reverse_index(), b'7' => self.save_cursor(), b'8' => self.restore_cursor(), b'c' => self.full_reset(), b'=' => self.application_keypad = true, b'>' => self.application_keypad = false, _ => {}
}
}
fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {
let Some(&number) = params.first() else {
return;
};
match number {
b"0" | b"2" => {
if let Some(&title) = params.get(1) {
let title = String::from_utf8_lossy(title).into_owned();
if number == b"0" {
self.icon_name.clone_from(&title);
}
self.set_window_title(title);
}
}
b"7" => {
if let Some(&cwd) = params.get(1) {
self.events
.push(TermEvent::Cwd(String::from_utf8_lossy(cwd).into_owned()));
}
}
b"133" => match params.get(1).copied() {
Some(b"A") => self.add_command_mark(MarkerKind::PromptStart),
Some(b"B") => self.add_command_mark(MarkerKind::CommandStart),
Some(b"C") => self.add_command_mark(MarkerKind::OutputStart),
Some(b"D") => {
let exit = params
.get(2)
.and_then(|p| core::str::from_utf8(p).ok())
.and_then(|s| s.parse::<i32>().ok());
self.add_command_mark(MarkerKind::CommandFinished(exit));
}
_ => {}
},
b"8" => {
let uri: Vec<u8> = params.get(2..).unwrap_or_default().join(&b';');
self.current_link = if uri.is_empty() {
None
} else {
let uri = String::from_utf8_lossy(&uri);
Some(match osc8_link_id(params.get(1).copied().unwrap_or(b"")) {
None => std::sync::Arc::from(&*uri),
Some(id) => self.link_for_id(&String::from_utf8_lossy(id), &uri),
})
};
}
b"4" => {
let mut rest = ¶ms[1..];
while let [idx, spec, tail @ ..] = rest {
rest = tail;
if let Ok(index) = String::from_utf8_lossy(idx).parse::<u8>() {
if *spec == b"?" {
self.events.push(TermEvent::QueryPaletteColor { index });
} else {
self.events.push(TermEvent::SetPaletteColor {
index,
spec: String::from_utf8_lossy(spec).into_owned(),
});
}
}
}
}
b"104" => {
if params.len() <= 1 || (params.len() == 2 && params[1].is_empty()) {
self.events.push(TermEvent::ResetPaletteColor(None));
} else {
for &idx in ¶ms[1..] {
if let Ok(index) = String::from_utf8_lossy(idx).parse::<u8>() {
self.events.push(TermEvent::ResetPaletteColor(Some(index)));
}
}
}
}
b"10" => self.special_color(params, 0),
b"11" => self.special_color(params, 1),
b"12" => self.special_color(params, 2),
b"52" => self.clipboard(params),
b"110" => self.events.push(TermEvent::ResetForeground),
b"111" => self.events.push(TermEvent::ResetBackground),
b"112" => self.events.push(TermEvent::ResetCursorColor),
_ => {} }
}
}
#[cfg(test)]
mod tests {
use super::cap_scroll;
use super::version_number;
use crate::Engine;
use crate::damage::ScrollOp;
use crate::serialize::MAX_SCROLL_COUNT;
#[test]
fn version_number_pads_semver_base_100() {
assert_eq!(version_number("0.0.1"), 1);
assert_eq!(version_number("0.15.0"), 15_00);
assert_eq!(version_number("1.2.3"), 1_02_03);
assert_eq!(version_number("999.99.99"), 9_99_99_99);
assert!(version_number("0.16.0") > version_number("0.15.9"));
assert!(version_number("1.0.0") > version_number("0.99.99"));
}
#[test]
fn version_number_strips_a_pre_release_suffix() {
assert_eq!(version_number("0.15.0-dev"), 15_00);
assert_eq!(version_number("1.2.3-rc.1-dev"), 1_02_03);
assert_eq!(version_number("1.2.3+build.5"), 1_02_03);
assert_eq!(version_number("0.15.0-rc2"), 15_00);
assert_eq!(version_number("1.2.3-4"), 1_02_03);
}
#[test]
fn version_number_tolerates_a_short_or_odd_version() {
assert_eq!(version_number("2"), 2_00_00);
assert_eq!(version_number("2.7"), 2_07_00);
assert_eq!(version_number("1.2.3.4"), 1_02_03);
assert_eq!(version_number(""), 0);
}
#[test]
fn a_region_taller_than_the_wire_field_truncates_rather_than_wraps() {
let up = cap_scroll(ScrollOp {
top: 0,
bottom: 40_000,
count: 35_000,
});
assert_eq!(up.count, MAX_SCROLL_COUNT, "capped, and still an up-scroll");
let down = cap_scroll(ScrollOp {
top: 0,
bottom: 40_000,
count: -35_000,
});
assert_eq!(down.count, -MAX_SCROLL_COUNT, "sign survives the cap");
}
#[test]
fn a_scroll_inside_both_bounds_passes_through_unchanged() {
let op = ScrollOp {
top: 4,
bottom: 9,
count: -2,
};
assert_eq!(cap_scroll(op), op);
}
#[test]
fn a_link_evicted_from_the_buffer_stops_being_stored() {
let mut e = Engine::with_scrollback(20, 2, 2);
for i in 0..50 {
e.feed(format!("\x1b]8;;https://example.com/{i}\x07L{i}\x1b]8;;\x07\r\n").as_bytes());
}
let mut e2 = Engine::with_scrollback(20, 2, 2);
e2.feed(b"\x1b]8;;https://example.com/first\x07L\x1b]8;;\x07\r\n");
let weak = {
let arc = e2
.term
.grid
.row_ref(0)
.link_at(0)
.expect("on screen")
.clone();
std::sync::Arc::downgrade(&arc)
};
assert!(
weak.upgrade().is_some(),
"the URI must be alive while its cell is on screen",
);
for i in 0..50 {
e2.feed(format!("filler {i}\r\n").as_bytes());
}
assert!(
weak.upgrade().is_none(),
"the first link scrolled out of a 4-line buffer and nothing should still \
hold its URI — before #628 every OSC 8 open lived for the life of the Term",
);
let owned: std::collections::HashSet<*const u8> = e
.term
.scrollback
.iter()
.chain((0..2).map(|r| e.term.grid.row_ref(r)))
.flat_map(|r| r.owned_links())
.map(|u| std::sync::Arc::as_ptr(u) as *const u8)
.collect();
assert!(
owned.len() <= 4,
"a 4-line buffer cannot own more than 4 distinct URIs, found {}",
owned.len(),
);
}
#[test]
fn an_erased_cell_releases_its_uri_not_only_its_bit() {
let mut e = Engine::new(80, 24);
e.feed(b"]8;;https://example.com/erasedL]8;;");
let weak = {
let a = e
.term
.grid
.row_ref(0)
.link_at(0)
.expect("on screen")
.clone();
std::sync::Arc::downgrade(&a)
};
assert!(weak.upgrade().is_some(), "alive while on screen");
e.feed(b"[2J");
assert!(e.link_at(0, 0).is_none(), "the presence bit is cleared");
assert!(
weak.upgrade().is_none(),
"and the URI itself is released — before the purge the map kept owning it, so an erased screen retained every link it had shown",
);
}
#[test]
fn the_engine_stays_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Engine>();
}
#[test]
fn two_opens_of_one_uri_are_two_links() {
let mut e = Engine::new(40, 2);
e.feed(b"]8;;https://example.com/xAB]8;;");
e.feed(b"]8;;https://example.com/xC]8;;");
let row = e.term.grid.row_ref(0);
let ptr = |c: usize| std::sync::Arc::as_ptr(row.link_at(c).expect("linked")) as *const u8;
assert_eq!(
e.link_at(0, 0).map(|h| h.uri().to_owned()),
e.link_at(0, 2).map(|h| h.uri().to_owned()),
"the text is the same",
);
assert_eq!(ptr(0), ptr(1), "A and B are one open, so one allocation");
assert_ne!(
ptr(0),
ptr(2),
"…but C is a second open — merging the two would override the distinction `id=` exists to express (#635)",
);
}
#[test]
fn the_same_id_and_uri_group_into_one_link() {
let mut e = Engine::new(40, 2);
e.feed(b"\x1b]8;id=xyz;https://example.com/a\x07A\x1b]8;;\x07\r\n");
e.feed(b"\x1b]8;id=xyz;https://example.com/a\x07B\x1b]8;;\x07");
let ptr = |r: usize, c: usize| {
std::sync::Arc::as_ptr(e.term.grid.row_ref(r).link_at(c).expect("linked")) as *const u8
};
assert_eq!(
ptr(0, 0),
ptr(1, 0),
"the application said these two runs are one link, so they share one allocation",
);
let f = e.frame();
assert_eq!(f.link_table.len(), 1, "one link ships once");
}
#[test]
fn the_same_id_with_a_different_uri_stays_two_links() {
let mut e = Engine::new(40, 2);
e.feed(b"\x1b]8;id=xyz;https://example.com/a\x07A\x1b]8;;\x07\r\n");
e.feed(b"\x1b]8;id=xyz;https://example.com/b\x07B\x1b]8;;\x07");
let ptr = |r: usize, c: usize| {
std::sync::Arc::as_ptr(e.term.grid.row_ref(r).link_at(c).expect("linked")) as *const u8
};
assert_ne!(
ptr(0, 0),
ptr(1, 0),
"same id, different target — two links"
);
assert_eq!(e.frame().link_table.len(), 2, "and both ship");
}
#[test]
fn an_empty_id_value_is_no_id_at_all() {
let mut e = Engine::new(40, 2);
e.feed(b"\x1b]8;id=;https://example.com/a\x07A\x1b]8;;\x07\r\n");
e.feed(b"\x1b]8;id=;https://example.com/a\x07B\x1b]8;;\x07");
let ptr = |r: usize, c: usize| {
std::sync::Arc::as_ptr(e.term.grid.row_ref(r).link_at(c).expect("linked")) as *const u8
};
assert_ne!(
ptr(0, 0),
ptr(1, 0),
"no id declared, so the reference-correct fresh-per-open rule still holds",
);
}
#[test]
fn the_id_param_is_found_among_other_params() {
let mut e = Engine::new(40, 2);
e.feed(b"\x1b]8;foo=bar:id=xyz:baz=quux;https://example.com/a\x07A\x1b]8;;\x07\r\n");
e.feed(b"\x1b]8;id=xyz;https://example.com/a\x07B\x1b]8;;\x07");
let ptr = |r: usize, c: usize| {
std::sync::Arc::as_ptr(e.term.grid.row_ref(r).link_at(c).expect("linked")) as *const u8
};
assert_eq!(
ptr(0, 0),
ptr(1, 0),
"the id is the same whatever else rides beside it",
);
}
#[test]
fn the_id_registry_does_not_keep_a_link_alive() {
let mut e = Engine::new(80, 24);
e.feed(b"\x1b]8;id=xyz;https://example.com/grouped\x07L\x1b]8;;\x07");
let weak = {
let a = e
.term
.grid
.row_ref(0)
.link_at(0)
.expect("on screen")
.clone();
std::sync::Arc::downgrade(&a)
};
assert!(weak.upgrade().is_some(), "alive while on screen");
e.feed(b"\x1b[2J");
assert!(
weak.upgrade().is_none(),
"the id registry must hold a Weak — a strong entry would outlive the screen and rebuild #628's leak one id at a time",
);
}
}