Skip to main content

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}