vterm/term.rs
1//! The terminal emulator: bytes in, a live [`ansi::Screen`] out.
2//!
3//! [`Term`] holds a primary and an alternate [`ansi::Screen`] (the active one
4//! owns the cursor — there is no second copy to keep in sync), the
5//! current SGR [`ansi::Style`], a scroll region, tab stops, a saved-cursor
6//! slot, and the flags a VT100 tracks (autowrap, cursor visibility, and the
7//! pending-wrap latch at the right edge). [`Term::feed`] runs the child's
8//! output through an internal [`ansi::Parser`] and applies each token.
9//!
10//! Coordinates are 0-based internally; CSI parameters are 1-based on the wire
11//! and converted on the way in. Where VT defaults a missing parameter to 1
12//! (cursor motion, CUP), we treat a 0 from the parser as that default; where
13//! it defaults to 0 (erase/SGR selectors), 0 is used directly.
14
15use ansi::{Cell, Cursor, Event, Parser, Screen, Style, Utf8Decoder};
16
17mod checkpoint;
18pub use checkpoint::RestoreError;
19
20/// Mouse events a program asked its terminal to send, from the xterm
21/// tracking modes. Each includes the ones above it.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum MouseTracking {
24 /// None: the terminal uses the mouse itself (selection, scrolling).
25 #[default]
26 Off,
27 /// Button presses and releases, and the wheel (`?1000h`).
28 Click,
29 /// Also motion while a button is held (`?1002h`).
30 Drag,
31 /// Also motion with no button held (`?1003h`).
32 Motion,
33}
34
35/// A minimal VT100/ECMA-48 terminal emulator over an [`ansi::Screen`].
36pub struct Term {
37 /// The primary (normal) screen buffer. It carries the scrollback, if any
38 /// ([`Term::with_history`]).
39 primary: Screen,
40 /// Scrollback capacity in lines (0: none).
41 history: usize,
42 /// Scrollback capacity in bytes as stored (0: no cap beyond the lines).
43 history_bytes: usize,
44 /// The alternate screen buffer, used while an app enters alt-screen mode
45 /// (`?1049h`/`?47h`/`?1047h`). Kept the same size as `primary`.
46 alt: Screen,
47 /// True while the alternate buffer is active.
48 in_alt: bool,
49 /// Current SGR style applied to newly written cells.
50 style: Style,
51 /// Scroll region, 0-based inclusive `[top, bottom]` (DECSTBM).
52 scroll_top: usize,
53 scroll_bottom: usize,
54 /// Saved cursor state (DECSC/`ESC 7` and CSI `s`): position and style.
55 saved: Option<(Cursor, Style)>,
56 /// Tab stops: `tabs[c]` true means a tab stop at column `c`.
57 tabs: Vec<bool>,
58 /// Pending-wrap latch: set after writing the last column so the *next*
59 /// printable wraps to the next line first (DECAWM deferred-wrap).
60 wrap_pending: bool,
61 /// Autowrap mode (DECAWM). Default on.
62 autowrap: bool,
63 /// Cursor visibility (DECTCEM, `?25h`/`?25l`), for the host to draw or
64 /// not; the screen cursor position is always synced regardless.
65 cursor_visible: bool,
66 /// Bracketed paste (`?2004h`/`?2004l`): the program wants pasted text
67 /// wrapped in `ESC [200~` ... `ESC [201~`. The host does the wrapping.
68 pub(crate) bracketed_paste: bool,
69 /// Mouse reporting the program asked for (`?1000`/`?1002`/`?1003`), and
70 /// in which encoding (`?1006`, SGR). The host reports the mouse.
71 mouse_tracking: MouseTracking,
72 mouse_sgr: bool,
73 /// Line feed / new line mode (LNM, ANSI mode 20, `CSI 20 h`/`l`): while
74 /// set, LF, VT and FF also return the cursor to column 0. A host that
75 /// feeds output from plain pipes (no tty to turn LF into CR LF) sets it
76 /// instead of rewriting every byte.
77 pub(crate) newline_mode: bool,
78 /// The parser driving `feed`; one per stream so mid-sequence chunk splits
79 /// are handled by `ansi`.
80 parser: Parser,
81 /// Decodes text runs, which `ansi::Event` hands over as raw bytes, and
82 /// carries a character split across chunks. Allocates nothing.
83 decoder: Utf8Decoder,
84 /// Set to `true` by `feed` (non-empty bytes) and `resize`; reset and
85 /// returned by `take_dirty`. Lets callers skip a composite when nothing
86 /// has changed since the last rendered frame.
87 dirty: bool,
88 /// Synchronized-output depth (DEC private mode 2026, `?2026h`/`?2026l`).
89 /// While an app is inside a synchronized update it is redrawing the frame;
90 /// a compositor that samples the screen mid-update sees a half-drawn frame
91 /// (leftover/overlapping text). While `in_sync`, [`Term::screen`] hands back
92 /// `sync_frame`, a clone of the last *complete* frame captured when the
93 /// update opened, so external readers only ever see whole frames. Writes
94 /// still land on the live buffer; the new frame is revealed on `?2026l`.
95 in_sync: bool,
96 /// The last complete frame, snapshotted when a synchronized update opens.
97 /// `Some` exactly while `in_sync`; `None` otherwise.
98 sync_frame: Option<Screen>,
99}
100
101impl Term {
102 /// A blank emulator of `rows x cols` (both clamped to at least 1), cursor
103 /// at the origin, full-height scroll region, default tab stops every 8
104 /// columns, autowrap on, cursor visible.
105 pub fn new(rows: usize, cols: usize) -> Self {
106 Term::with_history(rows, cols, 0)
107 }
108
109 /// Like [`Term::new`], keeping up to `lines` lines that scroll off the
110 /// top of the primary screen, readable through [`Term::scrollback`].
111 /// Each line is stored compactly as it leaves (`ansi::Screen::with_history`:
112 /// its text plus runs of style, not a row of cells), so memory follows
113 /// the text held rather than `lines x cols`.
114 /// The alternate screen, and scroll regions, keep no history.
115 pub fn with_history(rows: usize, cols: usize, lines: usize) -> Self {
116 let rows = rows.max(1);
117 let cols = cols.max(1);
118 Term {
119 primary: Screen::with_history(rows, cols, lines),
120 history: lines,
121 history_bytes: 0,
122 alt: Screen::new(rows, cols),
123 in_alt: false,
124 style: Style::default(),
125 scroll_top: 0,
126 scroll_bottom: rows - 1,
127 saved: None,
128 tabs: default_tabs(cols),
129 wrap_pending: false,
130 autowrap: true,
131 cursor_visible: true,
132 bracketed_paste: false,
133 mouse_tracking: MouseTracking::Off,
134 mouse_sgr: false,
135 newline_mode: false,
136 parser: Parser::new(),
137 decoder: Utf8Decoder::new(),
138 dirty: false,
139 in_sync: false,
140 sync_frame: None,
141 }
142 }
143
144 /// Feed a chunk of child output. Chunks may split at any byte boundary
145 /// (mid-escape, mid-UTF-8); the internal parser holds partial state across
146 /// calls, so the final screen is independent of how bytes were chunked.
147 pub fn feed(&mut self, bytes: &[u8]) {
148 if bytes.is_empty() {
149 return;
150 }
151 self.dirty = true;
152 // The parser and decoder are lifted out for the call so the callback
153 // can borrow the rest of `self`; both are restored before returning.
154 let mut parser = std::mem::take(&mut self.parser);
155 let mut decoder = std::mem::take(&mut self.decoder);
156 parser.feed_with(bytes, |event| self.apply(event, &mut decoder));
157 self.parser = parser;
158 self.decoder = decoder;
159 }
160
161 /// Returns `true` if any bytes have been fed (or a resize has occurred)
162 /// since the last call to `take_dirty`, and resets the flag. Call once
163 /// per frame decision to avoid redundant compositing.
164 pub fn take_dirty(&mut self) -> bool {
165 std::mem::replace(&mut self.dirty, false)
166 }
167
168 /// The active buffer (alternate while in alt-screen, else primary). Its
169 /// cursor is the emulator's cursor, so a compositor / `Screen::diff` parks
170 /// the terminal cursor correctly.
171 pub fn screen(&self) -> &Screen {
172 // The active buffer owns the cursor, so it is always current. While a
173 // synchronized update (`?2026h`) is open we hand back the last complete
174 // frame instead of the live, half-drawn buffer (double-buffering).
175 match &self.sync_frame {
176 Some(frame) => frame,
177 None => self.active(),
178 }
179 }
180
181 /// The primary screen, whose history ([`ansi::Screen::history_len`],
182 /// [`ansi::Screen::history_row`]) holds the lines scrolled off its top.
183 /// Empty unless made with [`Term::with_history`].
184 pub fn scrollback(&self) -> &Screen {
185 &self.primary
186 }
187
188 /// Cap the scrollback at `max` bytes as stored (0: no cap), dropping the
189 /// oldest lines past it (`ansi::Screen::set_history_bytes`): a memory
190 /// bound however wide or colourful lines are. Kept across resizes.
191 pub fn set_history_bytes(&mut self, max: usize) {
192 self.history_bytes = max;
193 self.primary.set_history_bytes(max);
194 }
195
196 /// True while the alternate screen is active (full-screen programs), where
197 /// scrolling back through history does not apply.
198 pub fn in_alternate_screen(&self) -> bool {
199 self.in_alt
200 }
201
202 /// Whether the program has the cursor shown (`?25h`, the default) or
203 /// hidden (`?25l`), for the host to draw it or not.
204 pub fn cursor_visible(&self) -> bool {
205 self.cursor_visible
206 }
207
208 /// Whether the program asked for bracketed paste (`?2004h`): the host
209 /// then wraps what it pastes in `ESC [200~` and `ESC [201~`.
210 pub fn bracketed_paste(&self) -> bool {
211 self.bracketed_paste
212 }
213
214 /// Which mouse events the program asked to be sent (`?1000h` clicks,
215 /// `?1002h` also drags, `?1003h` also any motion); the host reports
216 /// them instead of using the mouse itself.
217 pub fn mouse_tracking(&self) -> MouseTracking {
218 self.mouse_tracking
219 }
220
221 /// Whether mouse reports use the SGR encoding (`?1006h`,
222 /// `ESC [< b ; x ; y M`), else the original one (`ESC [M` and three
223 /// bytes, for positions up to 223).
224 pub fn mouse_sgr(&self) -> bool {
225 self.mouse_sgr
226 }
227
228 /// True while the child is inside a synchronized update (DEC mode 2026):
229 /// it has emitted `?2026h` and not yet the matching `?2026l`. During this
230 /// window [`Term::screen`] returns the last complete frame. Exposed so a
231 /// host can also gate its own compositing on it if desired.
232 pub fn in_sync(&self) -> bool {
233 self.in_sync
234 }
235
236 /// Resize to `rows x cols` (clamped to at least 1). For 0.1 this rebuilds
237 /// fresh buffers of the new size (the caller repaints); the cursor and
238 /// scroll region are clamped, tab stops rebuilt, and pending wrap cleared.
239 pub fn resize(&mut self, rows: usize, cols: usize) {
240 let rows = rows.max(1);
241 let cols = cols.max(1);
242 let cursor = self.cursor();
243 let mut primary = Screen::with_history(rows, cols, self.history);
244 primary.set_history_bytes(self.history_bytes);
245 let old_primary = std::mem::replace(&mut self.primary, primary);
246 let old_alt = std::mem::replace(&mut self.alt, Screen::new(rows, cols));
247 self.primary.copy_history_from(&old_primary);
248 copy_top_left(&old_primary, &mut self.primary);
249 copy_top_left(&old_alt, &mut self.alt);
250 self.scroll_top = self.scroll_top.min(rows - 1);
251 self.scroll_bottom = rows - 1;
252 // The fresh active buffer takes the cursor, clamped onto the new size.
253 self.active_mut().set_cursor(cursor);
254 self.tabs = default_tabs(cols);
255 self.wrap_pending = false;
256 // A resize rebuilds the buffers; any held sync frame is now the wrong
257 // size and stale. Drop it and reveal the freshly-sized live buffer.
258 self.in_sync = false;
259 self.sync_frame = None;
260 self.dirty = true;
261 }
262
263 // --- internals --------------------------------------------------------
264
265 fn active(&self) -> &Screen {
266 if self.in_alt {
267 &self.alt
268 } else {
269 &self.primary
270 }
271 }
272
273 fn active_mut(&mut self) -> &mut Screen {
274 if self.in_alt {
275 &mut self.alt
276 } else {
277 &mut self.primary
278 }
279 }
280
281 fn rows(&self) -> usize {
282 self.active().rows()
283 }
284
285 fn cols(&self) -> usize {
286 self.active().cols()
287 }
288
289 /// The emulator cursor: the active buffer's.
290 fn cursor(&self) -> Cursor {
291 self.active().cursor()
292 }
293
294 fn set_row(&mut self, row: usize) {
295 let col = self.cursor().col;
296 self.active_mut().set_cursor(Cursor { row, col });
297 }
298
299 fn set_col(&mut self, col: usize) {
300 let row = self.cursor().row;
301 self.active_mut().set_cursor(Cursor { row, col });
302 }
303
304 fn apply(&mut self, event: Event<'_>, decoder: &mut Utf8Decoder) {
305 if let Event::Text(bytes) = event {
306 // Most output is plain ASCII: with no character half-received, it
307 // needs no decoding at all (a word-wide `is_ascii` decides).
308 if decoder.is_idle() && bytes.is_ascii() {
309 self.print_run(bytes);
310 } else {
311 decoder.decode(bytes, |s| self.print_run(s.as_bytes()));
312 }
313 return;
314 }
315 // Anything but text ends a run, so a half-finished character can never
316 // be completed: render it as U+FFFD, exactly as `ansi::Parser::feed`
317 // would have.
318 decoder.flush_incomplete(|s| self.print_run(s.as_bytes()));
319 match event {
320 Event::Control(b) => self.control(b),
321 Event::Csi {
322 private,
323 params,
324 final_byte,
325 ..
326 } => self.csi(private.map(char::from), params, char::from(final_byte)),
327 Event::Esc { final_byte, .. } => self.esc(final_byte),
328 // Titles, DCS/SOS/PM/APC strings: nothing to render.
329 Event::Text(_) | Event::Osc(_) | Event::Other { .. } => {}
330 }
331 }
332
333 /// Write a run of text at the cursor.
334 ///
335 /// Terminal output is overwhelmingly plain ASCII, and going through
336 /// [`Term::print`] for each character re-reads the cursor, re-borrows the
337 /// active buffer, looks up a width and bounds-checks a cell — per character.
338 /// So a run of ordinary single-width ASCII that fits on the current row is
339 /// written in one [`Screen::write_ascii`] with the cursor moved once.
340 ///
341 /// The fast path is deliberately narrow, and anything it does not cover
342 /// falls through to `print` unchanged: no wrap is pending, the characters
343 /// are printable ASCII (never control, never wide, never zero-width), and
344 /// the run stops one column short of the edge so the pending-wrap latch is
345 /// still decided by `print`.
346 ///
347 /// `text` is valid UTF-8: either decoded, or plain ASCII passed through.
348 fn print_run(&mut self, text: &[u8]) {
349 let mut rest = text;
350 while !rest.is_empty() {
351 let n = self.plain_run(rest);
352 if n == 0 {
353 // Not the fast path: one character the careful way.
354 let len = utf8_len(rest[0]).min(rest.len());
355 let ch = std::str::from_utf8(&rest[..len])
356 .ok()
357 .and_then(|s| s.chars().next())
358 .unwrap_or(char::REPLACEMENT_CHARACTER);
359 self.print(ch);
360 rest = &rest[len..];
361 continue;
362 }
363 let style = self.style;
364 let screen = if self.in_alt {
365 &mut self.alt
366 } else {
367 &mut self.primary
368 };
369 let Cursor { row, col } = screen.cursor();
370 screen.write_ascii(row, col, &rest[..n], style);
371 screen.set_cursor(Cursor { row, col: col + n });
372 rest = &rest[n..];
373 }
374 }
375
376 /// How many leading bytes of `s` the fast path in [`Term::print_run`] can
377 /// write: printable ASCII, stopping before the last column of the row.
378 fn plain_run(&self, s: &[u8]) -> usize {
379 if self.wrap_pending {
380 return 0;
381 }
382 let screen = if self.in_alt {
383 &self.alt
384 } else {
385 &self.primary
386 };
387 let col = screen.cursor().col;
388 // Leave the final column to `print`, which owns the pending-wrap latch.
389 let room = screen.cols().saturating_sub(col + 1);
390 let head = &s[..room.min(s.len())];
391 // Text reaches here from the parser, which never passes C0 controls or
392 // DEL in a text run, so "printable ASCII" is just "ASCII": checked a
393 // word at a time by `is_ascii`, byte by byte only when it fails.
394 debug_assert!(!head.iter().any(|&b| b < 0x20 || b == 0x7F));
395 if head.is_ascii() {
396 head.len()
397 } else {
398 head.iter()
399 .take_while(|&&b| (0x20..0x7f).contains(&b))
400 .count()
401 }
402 }
403
404 /// Write one printable character at the cursor, honoring autowrap via the
405 /// pending-wrap latch and East Asian double-width via [`uwidth`].
406 ///
407 /// A width-2 glyph (CJK, fullwidth, emoji) occupies two columns: its left
408 /// half is a [`Cell::wide`] carrying the character, its right half a
409 /// [`Cell::continuation`] carrying no glyph. The cursor advances by two. A
410 /// wide glyph that would straddle the right edge is not split — the whole
411 /// glyph wraps to the next line (autowrap on) or is dropped (autowrap off),
412 /// matching real terminals. Zero-width characters (combining marks, joiners,
413 /// variation selectors) are dropped: this grid stores one character per
414 /// cell and cannot compose them, and dropping avoids the column drift that
415 /// giving them a cell of their own would cause.
416 fn print(&mut self, ch: char) {
417 let w = uwidth::char_width(ch) as usize;
418
419 // Zero-width: no cell, no cursor movement (see doc comment).
420 if w == 0 {
421 return;
422 }
423
424 if self.wrap_pending && self.autowrap {
425 // The previous write filled the last column; wrap now.
426 self.autowrap_line();
427 }
428
429 // A double-width glyph needs two columns; if only one remains it cannot
430 // be split. Wrap the whole glyph to the next line (or drop it when
431 // autowrap is off, rather than overwrite half a cell at the edge).
432 if w == 2 && self.cursor().col + 1 >= self.cols() {
433 if !self.autowrap {
434 return;
435 }
436 self.autowrap_line();
437 }
438
439 // The per-character hot path borrows the active buffer once for the
440 // cells and the cursor together (field-disjoint, so `self.wrap_pending`
441 // stays writable below).
442 let (autowrap, style) = (self.autowrap, self.style);
443 let screen = if self.in_alt {
444 &mut self.alt
445 } else {
446 &mut self.primary
447 };
448 let cols = screen.cols();
449 let Cursor { row: r, col: c } = screen.cursor();
450 if w == 2 {
451 screen.set(r, c, Cell::wide(ch, style));
452 screen.set(r, c + 1, Cell::continuation(style));
453 } else {
454 screen.set(r, c, Cell::new(ch, style));
455 }
456 // Advance by the glyph's width. If that lands at or past the last
457 // column, stay at the last column and latch a pending wrap (deferred to
458 // the next printable), matching real terminals and the `ansi` diff's
459 // pending-wrap comment. A width-1 glyph advances exactly as before.
460 let at_edge = c + w >= cols;
461 let next = if at_edge { cols - 1 } else { c + w };
462 screen.set_cursor(Cursor { row: r, col: next });
463 if at_edge && autowrap {
464 self.wrap_pending = true;
465 }
466 }
467
468 /// Autowrap: the row is marked as continuing onto the next (so a host
469 /// copies them as one line), then the cursor moves to the next row's
470 /// start. Marked before the line feed, so a row the feed scrolls into the
471 /// history takes the mark with it.
472 fn autowrap_line(&mut self) {
473 let row = self.cursor().row;
474 self.active_mut().set_row_wrapped(row, true);
475 self.set_col(0);
476 self.line_feed();
477 self.wrap_pending = false;
478 }
479
480 fn control(&mut self, b: u8) {
481 match b {
482 b'\r' => {
483 self.set_col(0);
484 self.wrap_pending = false;
485 }
486 b'\n' | 0x0b | 0x0c => {
487 // LF, VT, FF: move down one line, scrolling at the margin
488 // (and to column 0 in new line mode).
489 self.line_feed();
490 if self.newline_mode {
491 self.set_col(0);
492 }
493 self.wrap_pending = false;
494 }
495 0x08 => {
496 // BS: cursor left one column, clamped at column 0.
497 if self.cursor().col > 0 {
498 self.set_col(self.cursor().col - 1);
499 }
500 self.wrap_pending = false;
501 }
502 b'\t' => {
503 self.set_col(self.next_tab_stop());
504 self.wrap_pending = false;
505 }
506 _ => {} // BEL and other C0 controls: nothing to render.
507 }
508 }
509
510 /// Move the cursor down one line; if it is at the bottom margin, scroll the
511 /// scroll region up by one instead.
512 fn line_feed(&mut self) {
513 if self.cursor().row == self.scroll_bottom {
514 self.scroll_up(1);
515 } else if self.cursor().row + 1 < self.rows() {
516 self.set_row(self.cursor().row + 1);
517 }
518 }
519
520 /// Next tab stop strictly right of the cursor, or the last column.
521 fn next_tab_stop(&self) -> usize {
522 let cols = self.cols();
523 let mut c = self.cursor().col + 1;
524 while c < cols {
525 if self.tabs.get(c).copied().unwrap_or(false) {
526 return c;
527 }
528 c += 1;
529 }
530 cols - 1
531 }
532
533 // --- CSI dispatch -----------------------------------------------------
534
535 fn csi(&mut self, private: Option<char>, params: &[u16], final_byte: char) {
536 // Private-marker sequences (`?...`) are the mode toggles.
537 if private == Some('?') {
538 match final_byte {
539 'h' => self.set_mode(params, true),
540 'l' => self.set_mode(params, false),
541 _ => {} // other private CSI: ignore quietly
542 }
543 return;
544 }
545 if private.is_some() {
546 return; // `>`/`<`/`=` device sequences: ignore quietly
547 }
548
549 // Helpers: VT motion params default to 1 (0 from the parser means the
550 // param was absent → use 1). Erase/line selectors default to 0.
551 let n1 = |i: usize| -> usize { at1(params, i) };
552
553 match final_byte {
554 // Cursor absolute position: CUP / HVP (row;col, 1-based).
555 'H' | 'f' => {
556 let r = n1(0).saturating_sub(1);
557 let c = n1(1).saturating_sub(1);
558 self.move_to(r, c);
559 }
560 'A' => self.move_up(n1(0)),
561 'B' => self.move_down(n1(0)),
562 'C' => self.move_right(n1(0)),
563 'D' => self.move_left(n1(0)),
564 'E' => {
565 // CNL: cursor next line, column 0.
566 self.move_down(n1(0));
567 self.set_col(0);
568 self.wrap_pending = false;
569 }
570 'F' => {
571 // CPL: cursor previous line, column 0.
572 self.move_up(n1(0));
573 self.set_col(0);
574 self.wrap_pending = false;
575 }
576 'G' => {
577 // CHA: cursor to absolute column (1-based).
578 let c = n1(0).saturating_sub(1);
579 self.set_col(c.min(self.cols() - 1));
580 self.wrap_pending = false;
581 }
582 'd' => {
583 // VPA: cursor to absolute row (1-based).
584 let r = n1(0).saturating_sub(1);
585 self.set_row(r.min(self.rows() - 1));
586 self.wrap_pending = false;
587 }
588 'J' => self.erase_display(at0(params, 0)),
589 'K' => self.erase_line(at0(params, 0)),
590 'X' => self.erase_chars(n1(0)),
591 'm' => self.style.apply_sgr(params),
592 // SM / RM: of the ANSI modes only LNM (20) changes rendering.
593 'h' | 'l' if params.contains(&20) => self.newline_mode = final_byte == 'h',
594 'r' => self.set_scroll_region(params),
595 'L' => self.insert_lines(n1(0)),
596 'M' => self.delete_lines(n1(0)),
597 'S' => self.scroll_up(n1(0)),
598 'T' => self.scroll_down(n1(0)),
599 '@' => self.insert_chars(n1(0)),
600 'P' => self.delete_chars(n1(0)),
601 's' => self.save_cursor(),
602 'u' => self.restore_cursor(),
603 _ => {} // unimplemented CSI: ignore quietly
604 }
605 }
606
607 fn set_mode(&mut self, params: &[u16], set: bool) {
608 for &p in params {
609 match p {
610 25 => self.cursor_visible = set,
611 2004 => self.bracketed_paste = set,
612 // The tracking modes replace one another; resetting the one
613 // in force (or any, as programs send all three) turns it off.
614 1000 | 1002 | 1003 => {
615 self.mouse_tracking = match (set, p) {
616 (true, 1000) => MouseTracking::Click,
617 (true, 1002) => MouseTracking::Drag,
618 (true, _) => MouseTracking::Motion,
619 (false, _) => MouseTracking::Off,
620 }
621 }
622 1006 => self.mouse_sgr = set,
623 7 => self.autowrap = set, // DECAWM
624 47 | 1047 | 1049 => self.set_alt(set),
625 2026 => self.set_sync(set), // synchronized output
626 _ => {} // other private modes: ignore quietly
627 }
628 }
629 }
630
631 fn set_alt(&mut self, enter: bool) {
632 // One logical cursor across both buffers: it moves with the switch.
633 let cursor = self.cursor();
634 if enter && !self.in_alt {
635 // Entering: switch to a cleared alternate buffer.
636 self.alt = Screen::new(self.rows(), self.cols());
637 self.in_alt = true;
638 } else if !enter && self.in_alt {
639 // Leaving: restore the primary buffer as-is.
640 self.in_alt = false;
641 }
642 self.active_mut().set_cursor(cursor);
643 self.wrap_pending = false;
644 }
645
646 /// Enter/leave a synchronized update (DEC mode 2026). On open we snapshot
647 /// the current active buffer as the last complete frame and serve that from
648 /// `screen()` until close, so a mid-redraw frame is never exposed. Nested
649 /// opens keep the first (good) snapshot rather than capturing a half-drawn
650 /// one. On close we drop the snapshot, revealing the now-complete buffer.
651 fn set_sync(&mut self, open: bool) {
652 if open {
653 if !self.in_sync {
654 // The visible rows only: never a copy of the scrollback.
655 self.sync_frame = Some(self.active().visible());
656 self.in_sync = true;
657 }
658 } else {
659 self.in_sync = false;
660 self.sync_frame = None;
661 }
662 }
663
664 // --- cursor motion (clamped) -----------------------------------------
665
666 fn move_to(&mut self, r: usize, c: usize) {
667 self.active_mut().set_cursor(Cursor { row: r, col: c });
668 self.wrap_pending = false;
669 }
670
671 fn move_up(&mut self, n: usize) {
672 self.set_row(self.cursor().row.saturating_sub(n));
673 self.wrap_pending = false;
674 }
675
676 fn move_down(&mut self, n: usize) {
677 self.set_row((self.cursor().row + n).min(self.rows() - 1));
678 self.wrap_pending = false;
679 }
680
681 fn move_right(&mut self, n: usize) {
682 self.set_col((self.cursor().col + n).min(self.cols() - 1));
683 self.wrap_pending = false;
684 }
685
686 fn move_left(&mut self, n: usize) {
687 self.set_col(self.cursor().col.saturating_sub(n));
688 self.wrap_pending = false;
689 }
690
691 // --- erase ------------------------------------------------------------
692
693 /// ED: 0 = cursor to end of screen, 1 = start of screen to cursor, 2 = all.
694 fn erase_display(&mut self, mode: u16) {
695 let (rows, cols) = (self.rows(), self.cols());
696 let Cursor { row: cr, col: cc } = self.cursor();
697 let blank = Cell::default();
698 match mode {
699 0 => {
700 for c in cc..cols {
701 self.active_mut().set(cr, c, blank);
702 }
703 for r in (cr + 1)..rows {
704 for c in 0..cols {
705 self.active_mut().set(r, c, blank);
706 }
707 }
708 }
709 1 => {
710 for r in 0..cr {
711 for c in 0..cols {
712 self.active_mut().set(r, c, blank);
713 }
714 }
715 for c in 0..=cc {
716 self.active_mut().set(cr, c, blank);
717 }
718 }
719 2 => {
720 for r in 0..rows {
721 for c in 0..cols {
722 self.active_mut().set(r, c, blank);
723 }
724 }
725 }
726 // ED 3: erase the scrollback (xterm), leaving the screen.
727 3 => self.primary.clear_history(),
728 _ => {}
729 }
730 }
731
732 /// EL: 0 = cursor to end of line, 1 = start of line to cursor, 2 = whole line.
733 fn erase_line(&mut self, mode: u16) {
734 let cols = self.cols();
735 let Cursor { row: cr, col: cc } = self.cursor();
736 let blank = Cell::default();
737 let range = match mode {
738 0 => cc..cols,
739 1 => 0..(cc + 1).min(cols),
740 2 => 0..cols,
741 _ => return,
742 };
743 for c in range {
744 self.active_mut().set(cr, c, blank);
745 }
746 // Its end erased, the row no longer runs on into the next.
747 if mode != 1 {
748 self.active_mut().set_row_wrapped(cr, false);
749 }
750 }
751
752 /// ECH: erase (blank) `n` cells from the cursor rightward, **without** moving
753 /// the cursor or shifting the rest of the line (unlike DCH, which closes the
754 /// gap). This is a common way to clear a run of cells (e.g. a status line or
755 /// trailing content), so dropping it leaves stale text behind. Clamped to the
756 /// row's right edge.
757 fn erase_chars(&mut self, n: usize) {
758 let cols = self.cols();
759 let Cursor { row: cr, col: cc } = self.cursor();
760 let end = (cc + n.max(1)).min(cols);
761 let blank = Cell::default();
762 for c in cc..end {
763 self.active_mut().set(cr, c, blank);
764 }
765 self.wrap_pending = false;
766 }
767
768 // --- scroll region + line/char insert/delete --------------------------
769
770 /// DECSTBM: set scroll region `top;bottom` (1-based, clamped); resets the
771 /// cursor to the origin. An empty/`0` bottom means the last row.
772 fn set_scroll_region(&mut self, params: &[u16]) {
773 let rows = self.rows();
774 let top = at1(params, 0).saturating_sub(1);
775 let bottom = match at0(params, 1) {
776 0 => rows - 1,
777 b => (b as usize - 1).min(rows - 1),
778 };
779 // Ignore an inverted/empty region, as VT100 does.
780 if top < bottom {
781 self.scroll_top = top;
782 self.scroll_bottom = bottom;
783 }
784 self.active_mut().set_cursor(Cursor::default());
785 self.wrap_pending = false;
786 }
787
788 /// Scroll the scroll region up by `n` lines: top lines lost, blanks in at
789 /// the bottom margin.
790 ///
791 /// One block move in the screen ([`Screen::scroll_rows_up`]), not a copy per
792 /// cell: this runs on every line of output, and the per-cell loop held the
793 /// emulator to ~11 MB/s on a 40x160 grid.
794 fn scroll_up(&mut self, n: usize) {
795 let (top, bottom) = (self.scroll_top, self.scroll_bottom);
796 self.active_mut()
797 .scroll_rows_up(top, bottom, n, Cell::default());
798 }
799
800 /// Scroll the scroll region down by `n` lines: bottom lines lost, blanks in
801 /// at the top margin.
802 fn scroll_down(&mut self, n: usize) {
803 let (top, bottom) = (self.scroll_top, self.scroll_bottom);
804 self.active_mut()
805 .scroll_rows_down(top, bottom, n, Cell::default());
806 }
807
808 /// IL: insert `n` blank lines at the cursor row, within the scroll region;
809 /// lines below shift down and fall off the bottom margin. No-op outside the
810 /// region.
811 fn insert_lines(&mut self, n: usize) {
812 if self.cursor().row < self.scroll_top || self.cursor().row > self.scroll_bottom {
813 return;
814 }
815 let saved_top = self.scroll_top;
816 self.scroll_top = self.cursor().row;
817 self.scroll_down(n);
818 self.scroll_top = saved_top;
819 self.set_col(0);
820 self.wrap_pending = false;
821 }
822
823 /// DL: delete `n` lines at the cursor row, within the scroll region; lines
824 /// below shift up and blanks fill the bottom margin. No-op outside region.
825 fn delete_lines(&mut self, n: usize) {
826 if self.cursor().row < self.scroll_top || self.cursor().row > self.scroll_bottom {
827 return;
828 }
829 let saved_top = self.scroll_top;
830 self.scroll_top = self.cursor().row;
831 self.scroll_up(n);
832 self.scroll_top = saved_top;
833 self.set_col(0);
834 self.wrap_pending = false;
835 }
836
837 /// ICH: insert `n` blank cells at the cursor, shifting the rest of the line
838 /// right off the edge.
839 fn insert_chars(&mut self, n: usize) {
840 let cols = self.cols();
841 let Cursor { row: r, col: start } = self.cursor();
842 let n = n.min(cols - start);
843 for c in (start..cols).rev() {
844 let cell = if c >= start + n {
845 self.active().cell(r, c - n)
846 } else {
847 Cell::default()
848 };
849 self.active_mut().set(r, c, cell);
850 }
851 self.wrap_pending = false;
852 }
853
854 /// DCH: delete `n` cells at the cursor, shifting the rest of the line left
855 /// and filling the tail with blanks.
856 fn delete_chars(&mut self, n: usize) {
857 let cols = self.cols();
858 let Cursor { row: r, col: start } = self.cursor();
859 let n = n.min(cols - start);
860 for c in start..cols {
861 let cell = if c + n < cols {
862 self.active().cell(r, c + n)
863 } else {
864 Cell::default()
865 };
866 self.active_mut().set(r, c, cell);
867 }
868 self.wrap_pending = false;
869 }
870
871 // --- save / restore + ESC --------------------------------------------
872
873 fn save_cursor(&mut self) {
874 self.saved = Some((self.cursor(), self.style));
875 }
876
877 fn restore_cursor(&mut self) {
878 if let Some((cursor, style)) = self.saved {
879 self.active_mut().set_cursor(cursor);
880 self.style = style;
881 }
882 self.wrap_pending = false;
883 }
884
885 fn esc(&mut self, final_byte: u8) {
886 match final_byte {
887 b'7' => self.save_cursor(), // DECSC
888 b'8' => self.restore_cursor(), // DECRC
889 _ => {} // charset selection etc.: ignore quietly
890 }
891 }
892}
893
894/// Tab stops every 8 columns (VT default), with a stop at column 0.
895fn default_tabs(cols: usize) -> Vec<bool> {
896 (0..cols).map(|c| c % 8 == 0).collect()
897}
898
899/// Read a param defaulting a missing/zero value to 1 (VT motion default).
900fn at1(params: &[u16], i: usize) -> usize {
901 match params.get(i).copied().unwrap_or(0) {
902 0 => 1,
903 v => v as usize,
904 }
905}
906
907/// Read a param defaulting a missing value to 0 (erase/selector default).
908fn at0(params: &[u16], i: usize) -> u16 {
909 params.get(i).copied().unwrap_or(0)
910}
911
912/// Copy the overlapping top-left region of `src` into `dst` (for resize).
913fn copy_top_left(src: &Screen, dst: &mut Screen) {
914 let rows = src.rows().min(dst.rows());
915 let cols = src.cols().min(dst.cols());
916 for r in 0..rows {
917 for c in 0..cols {
918 dst.set(r, c, src.cell(r, c));
919 }
920 }
921}
922
923/// Length of the UTF-8 sequence a lead byte starts (1 for ASCII and for bytes
924/// that cannot lead, which valid input never has here).
925fn utf8_len(lead: u8) -> usize {
926 match lead {
927 0xC0..=0xDF => 2,
928 0xE0..=0xEF => 3,
929 0xF0..=0xF7 => 4,
930 _ => 1,
931 }
932}