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`], a cursor, the
4//! current SGR [`ansi::Style`], a scroll region, tab stops, a saved-cursor
5//! slot, and the flags a VT100 tracks (autowrap, cursor visibility, and the
6//! pending-wrap latch at the right edge). [`Term::feed`] runs the child's
7//! output through an internal [`ansi::Parser`] and applies each token.
8//!
9//! Coordinates are 0-based internally; CSI parameters are 1-based on the wire
10//! and converted on the way in. Where VT defaults a missing parameter to 1
11//! (cursor motion, CUP), we treat a 0 from the parser as that default; where
12//! it defaults to 0 (erase/SGR selectors), 0 is used directly.
13
14use ansi::{Cell, Parser, Screen, Style, Token};
15
16/// A minimal VT100/ECMA-48 terminal emulator over an [`ansi::Screen`].
17pub struct Term {
18    /// The primary (normal) screen buffer.
19    primary: Screen,
20    /// The alternate screen buffer, used while an app enters alt-screen mode
21    /// (`?1049h`/`?47h`/`?1047h`). Kept the same size as `primary`.
22    alt: Screen,
23    /// True while the alternate buffer is active.
24    in_alt: bool,
25    /// Cursor position, 0-based `(row, col)`.
26    row: usize,
27    col: usize,
28    /// Current SGR style applied to newly written cells.
29    style: Style,
30    /// Scroll region, 0-based inclusive `[top, bottom]` (DECSTBM).
31    scroll_top: usize,
32    scroll_bottom: usize,
33    /// Saved cursor state (DECSC/`ESC 7` and CSI `s`): row, col, style.
34    saved: Option<(usize, usize, Style)>,
35    /// Tab stops: `tabs[c]` true means a tab stop at column `c`.
36    tabs: Vec<bool>,
37    /// Pending-wrap latch: set after writing the last column so the *next*
38    /// printable wraps to the next line first (DECAWM deferred-wrap).
39    wrap_pending: bool,
40    /// Autowrap mode (DECAWM). Default on.
41    autowrap: bool,
42    /// Cursor visibility (DECTCEM, `?25h`/`?25l`). Tracked for the host; the
43    /// screen cursor position is always synced regardless.
44    cursor_visible: bool,
45    /// The parser driving `feed`; one per stream so mid-sequence chunk splits
46    /// are handled by `ansi`.
47    parser: Parser,
48    /// Synchronized-output depth (DEC private mode 2026, `?2026h`/`?2026l`).
49    /// While an app is inside a synchronized update it is redrawing the frame;
50    /// a compositor that samples the screen mid-update sees a half-drawn frame
51    /// (leftover/overlapping text). While `in_sync`, [`Term::screen`] hands back
52    /// `sync_frame`, a clone of the last *complete* frame captured when the
53    /// update opened, so external readers only ever see whole frames. Writes
54    /// still land on the live buffer; the new frame is revealed on `?2026l`.
55    in_sync: bool,
56    /// The last complete frame, snapshotted when a synchronized update opens.
57    /// `Some` exactly while `in_sync`; `None` otherwise.
58    sync_frame: Option<Screen>,
59}
60
61impl Term {
62    /// A blank emulator of `rows x cols` (both clamped to at least 1), cursor
63    /// at the origin, full-height scroll region, default tab stops every 8
64    /// columns, autowrap on, cursor visible.
65    pub fn new(rows: usize, cols: usize) -> Self {
66        let rows = rows.max(1);
67        let cols = cols.max(1);
68        Term {
69            primary: Screen::new(rows, cols),
70            alt: Screen::new(rows, cols),
71            in_alt: false,
72            row: 0,
73            col: 0,
74            style: Style::default(),
75            scroll_top: 0,
76            scroll_bottom: rows - 1,
77            saved: None,
78            tabs: default_tabs(cols),
79            wrap_pending: false,
80            autowrap: true,
81            cursor_visible: true,
82            parser: Parser::new(),
83            in_sync: false,
84            sync_frame: None,
85        }
86    }
87
88    /// Feed a chunk of child output. Chunks may split at any byte boundary
89    /// (mid-escape, mid-UTF-8); the internal parser holds partial state across
90    /// calls, so the final screen is independent of how bytes were chunked.
91    pub fn feed(&mut self, bytes: &[u8]) {
92        let tokens = self.parser.feed(bytes);
93        for tok in tokens {
94            self.apply(&tok);
95        }
96    }
97
98    /// The active buffer (alternate while in alt-screen, else primary), with
99    /// its public `cursor` field synced to the emulator's position so a
100    /// compositor / `Screen::diff` parks the cursor correctly.
101    pub fn screen(&self) -> &Screen {
102        // Sync happens in `sync_cursor`, called after every `apply`. This is a
103        // read-only accessor; the cursor is already current. While a
104        // synchronized update (`?2026h`) is open we hand back the last complete
105        // frame instead of the live, half-drawn buffer (double-buffering).
106        match &self.sync_frame {
107            Some(frame) => frame,
108            None => self.active(),
109        }
110    }
111
112    /// True while the child is inside a synchronized update (DEC mode 2026):
113    /// it has emitted `?2026h` and not yet the matching `?2026l`. During this
114    /// window [`Term::screen`] returns the last complete frame. Exposed so a
115    /// host can also gate its own compositing on it if desired.
116    pub fn in_sync(&self) -> bool {
117        self.in_sync
118    }
119
120    /// Resize to `rows x cols` (clamped to at least 1). For 0.1 this rebuilds
121    /// fresh buffers of the new size (the caller repaints); the cursor and
122    /// scroll region are clamped, tab stops rebuilt, and pending wrap cleared.
123    pub fn resize(&mut self, rows: usize, cols: usize) {
124        let rows = rows.max(1);
125        let cols = cols.max(1);
126        let old_primary = std::mem::replace(&mut self.primary, Screen::new(rows, cols));
127        let old_alt = std::mem::replace(&mut self.alt, Screen::new(rows, cols));
128        copy_top_left(&old_primary, &mut self.primary);
129        copy_top_left(&old_alt, &mut self.alt);
130        self.scroll_top = self.scroll_top.min(rows - 1);
131        self.scroll_bottom = rows - 1;
132        self.row = self.row.min(rows - 1);
133        self.col = self.col.min(cols - 1);
134        self.tabs = default_tabs(cols);
135        self.wrap_pending = false;
136        // A resize rebuilds the buffers; any held sync frame is now the wrong
137        // size and stale. Drop it and reveal the freshly-sized live buffer.
138        self.in_sync = false;
139        self.sync_frame = None;
140        self.sync_cursor();
141    }
142
143    // --- internals --------------------------------------------------------
144
145    fn active(&self) -> &Screen {
146        if self.in_alt {
147            &self.alt
148        } else {
149            &self.primary
150        }
151    }
152
153    fn active_mut(&mut self) -> &mut Screen {
154        if self.in_alt {
155            &mut self.alt
156        } else {
157            &mut self.primary
158        }
159    }
160
161    fn rows(&self) -> usize {
162        self.active().rows()
163    }
164
165    fn cols(&self) -> usize {
166        self.active().cols()
167    }
168
169    /// Mirror the emulator cursor into the active screen's public field.
170    fn sync_cursor(&mut self) {
171        let (r, c) = (self.row, self.col);
172        self.active_mut().cursor = (r, c);
173    }
174
175    fn apply(&mut self, tok: &Token) {
176        match tok {
177            Token::Text(s) => {
178                for ch in s.chars() {
179                    self.print(ch);
180                }
181            }
182            Token::Control(b) => self.control(*b),
183            Token::Csi {
184                private,
185                params,
186                final_byte,
187                ..
188            } => self.csi(*private, params, *final_byte),
189            Token::Esc { final_byte, .. } => self.esc(*final_byte),
190            // Titles, DCS/SOS/PM/APC strings: nothing to render.
191            Token::Osc(_) | Token::Other { .. } => {}
192        }
193        self.sync_cursor();
194    }
195
196    /// Write one printable character at the cursor, honoring autowrap via the
197    /// pending-wrap latch.
198    fn print(&mut self, ch: char) {
199        let cols = self.cols();
200        if self.wrap_pending && self.autowrap {
201            // The previous write filled the last column; wrap now.
202            self.col = 0;
203            self.line_feed();
204            self.wrap_pending = false;
205        }
206        let (r, c) = (self.row, self.col);
207        let cell = Cell {
208            ch,
209            style: self.style,
210        };
211        self.active_mut().set(r, c, cell);
212        if self.col + 1 >= cols {
213            // At the right edge: stay put and latch a pending wrap (deferred to
214            // the next printable), matching real terminals and the `ansi`
215            // diff's pending-wrap comment.
216            if self.autowrap {
217                self.wrap_pending = true;
218            }
219            // With autowrap off, the cursor sticks at the last column.
220        } else {
221            self.col += 1;
222        }
223    }
224
225    fn control(&mut self, b: u8) {
226        match b {
227            b'\r' => {
228                self.col = 0;
229                self.wrap_pending = false;
230            }
231            b'\n' | 0x0b | 0x0c => {
232                // LF, VT, FF: move down one line, scrolling at the margin.
233                self.line_feed();
234                self.wrap_pending = false;
235            }
236            0x08 => {
237                // BS: cursor left one column, clamped at column 0.
238                if self.col > 0 {
239                    self.col -= 1;
240                }
241                self.wrap_pending = false;
242            }
243            b'\t' => {
244                self.col = self.next_tab_stop();
245                self.wrap_pending = false;
246            }
247            _ => {} // BEL and other C0 controls: nothing to render.
248        }
249    }
250
251    /// Move the cursor down one line; if it is at the bottom margin, scroll the
252    /// scroll region up by one instead.
253    fn line_feed(&mut self) {
254        if self.row == self.scroll_bottom {
255            self.scroll_up(1);
256        } else if self.row + 1 < self.rows() {
257            self.row += 1;
258        }
259    }
260
261    /// Next tab stop strictly right of the cursor, or the last column.
262    fn next_tab_stop(&self) -> usize {
263        let cols = self.cols();
264        let mut c = self.col + 1;
265        while c < cols {
266            if self.tabs.get(c).copied().unwrap_or(false) {
267                return c;
268            }
269            c += 1;
270        }
271        cols - 1
272    }
273
274    // --- CSI dispatch -----------------------------------------------------
275
276    fn csi(&mut self, private: Option<char>, params: &[u16], final_byte: char) {
277        // Private-marker sequences (`?...`) are the mode toggles.
278        if private == Some('?') {
279            match final_byte {
280                'h' => self.set_mode(params, true),
281                'l' => self.set_mode(params, false),
282                _ => {} // other private CSI: ignore quietly
283            }
284            return;
285        }
286        if private.is_some() {
287            return; // `>`/`<`/`=` device sequences: ignore quietly
288        }
289
290        // Helpers: VT motion params default to 1 (0 from the parser means the
291        // param was absent → use 1). Erase/line selectors default to 0.
292        let n1 = |i: usize| -> usize { at1(params, i) };
293
294        match final_byte {
295            // Cursor absolute position: CUP / HVP (row;col, 1-based).
296            'H' | 'f' => {
297                let r = n1(0).saturating_sub(1);
298                let c = n1(1).saturating_sub(1);
299                self.move_to(r, c);
300            }
301            'A' => self.move_up(n1(0)),
302            'B' => self.move_down(n1(0)),
303            'C' => self.move_right(n1(0)),
304            'D' => self.move_left(n1(0)),
305            'E' => {
306                // CNL: cursor next line, column 0.
307                self.move_down(n1(0));
308                self.col = 0;
309                self.wrap_pending = false;
310            }
311            'F' => {
312                // CPL: cursor previous line, column 0.
313                self.move_up(n1(0));
314                self.col = 0;
315                self.wrap_pending = false;
316            }
317            'G' => {
318                // CHA: cursor to absolute column (1-based).
319                let c = n1(0).saturating_sub(1);
320                self.col = c.min(self.cols() - 1);
321                self.wrap_pending = false;
322            }
323            'd' => {
324                // VPA: cursor to absolute row (1-based).
325                let r = n1(0).saturating_sub(1);
326                self.row = r.min(self.rows() - 1);
327                self.wrap_pending = false;
328            }
329            'J' => self.erase_display(at0(params, 0)),
330            'K' => self.erase_line(at0(params, 0)),
331            'X' => self.erase_chars(n1(0)),
332            'm' => self.style.apply_sgr(params),
333            'r' => self.set_scroll_region(params),
334            'L' => self.insert_lines(n1(0)),
335            'M' => self.delete_lines(n1(0)),
336            'S' => self.scroll_up(n1(0)),
337            'T' => self.scroll_down(n1(0)),
338            '@' => self.insert_chars(n1(0)),
339            'P' => self.delete_chars(n1(0)),
340            's' => self.save_cursor(),
341            'u' => self.restore_cursor(),
342            _ => {} // unimplemented CSI: ignore quietly
343        }
344    }
345
346    fn set_mode(&mut self, params: &[u16], set: bool) {
347        for &p in params {
348            match p {
349                25 => self.cursor_visible = set,
350                7 => self.autowrap = set, // DECAWM
351                47 | 1047 | 1049 => self.set_alt(set),
352                2026 => self.set_sync(set), // synchronized output
353                _ => {}                     // other private modes: ignore quietly
354            }
355        }
356    }
357
358    fn set_alt(&mut self, enter: bool) {
359        if enter && !self.in_alt {
360            // Entering: switch to a cleared alternate buffer.
361            self.alt = Screen::new(self.rows(), self.cols());
362            self.in_alt = true;
363        } else if !enter && self.in_alt {
364            // Leaving: restore the primary buffer as-is.
365            self.in_alt = false;
366        }
367        self.wrap_pending = false;
368    }
369
370    /// Enter/leave a synchronized update (DEC mode 2026). On open we snapshot
371    /// the current active buffer as the last complete frame and serve that from
372    /// `screen()` until close, so a mid-redraw frame is never exposed. Nested
373    /// opens keep the first (good) snapshot rather than capturing a half-drawn
374    /// one. On close we drop the snapshot, revealing the now-complete buffer.
375    fn set_sync(&mut self, open: bool) {
376        if open {
377            if !self.in_sync {
378                self.sync_frame = Some(self.active().clone());
379                self.in_sync = true;
380            }
381        } else {
382            self.in_sync = false;
383            self.sync_frame = None;
384        }
385    }
386
387    // --- cursor motion (clamped) -----------------------------------------
388
389    fn move_to(&mut self, r: usize, c: usize) {
390        self.row = r.min(self.rows() - 1);
391        self.col = c.min(self.cols() - 1);
392        self.wrap_pending = false;
393    }
394
395    fn move_up(&mut self, n: usize) {
396        self.row = self.row.saturating_sub(n);
397        self.wrap_pending = false;
398    }
399
400    fn move_down(&mut self, n: usize) {
401        self.row = (self.row + n).min(self.rows() - 1);
402        self.wrap_pending = false;
403    }
404
405    fn move_right(&mut self, n: usize) {
406        self.col = (self.col + n).min(self.cols() - 1);
407        self.wrap_pending = false;
408    }
409
410    fn move_left(&mut self, n: usize) {
411        self.col = self.col.saturating_sub(n);
412        self.wrap_pending = false;
413    }
414
415    // --- erase ------------------------------------------------------------
416
417    /// ED: 0 = cursor to end of screen, 1 = start of screen to cursor, 2 = all.
418    fn erase_display(&mut self, mode: u16) {
419        let (rows, cols) = (self.rows(), self.cols());
420        let (cr, cc) = (self.row, self.col);
421        let blank = Cell::default();
422        match mode {
423            0 => {
424                for c in cc..cols {
425                    self.active_mut().set(cr, c, blank);
426                }
427                for r in (cr + 1)..rows {
428                    for c in 0..cols {
429                        self.active_mut().set(r, c, blank);
430                    }
431                }
432            }
433            1 => {
434                for r in 0..cr {
435                    for c in 0..cols {
436                        self.active_mut().set(r, c, blank);
437                    }
438                }
439                for c in 0..=cc {
440                    self.active_mut().set(cr, c, blank);
441                }
442            }
443            2 => {
444                for r in 0..rows {
445                    for c in 0..cols {
446                        self.active_mut().set(r, c, blank);
447                    }
448                }
449            }
450            _ => {}
451        }
452    }
453
454    /// EL: 0 = cursor to end of line, 1 = start of line to cursor, 2 = whole line.
455    fn erase_line(&mut self, mode: u16) {
456        let cols = self.cols();
457        let (cr, cc) = (self.row, self.col);
458        let blank = Cell::default();
459        let range = match mode {
460            0 => cc..cols,
461            1 => 0..(cc + 1).min(cols),
462            2 => 0..cols,
463            _ => return,
464        };
465        for c in range {
466            self.active_mut().set(cr, c, blank);
467        }
468    }
469
470    /// ECH: erase (blank) `n` cells from the cursor rightward, **without** moving
471    /// the cursor or shifting the rest of the line (unlike DCH, which closes the
472    /// gap). This is a common way to clear a run of cells (e.g. a status line or
473    /// trailing content), so dropping it leaves stale text behind. Clamped to the
474    /// row's right edge.
475    fn erase_chars(&mut self, n: usize) {
476        let cols = self.cols();
477        let (cr, cc) = (self.row, self.col);
478        let end = (cc + n.max(1)).min(cols);
479        let blank = Cell::default();
480        for c in cc..end {
481            self.active_mut().set(cr, c, blank);
482        }
483        self.wrap_pending = false;
484    }
485
486    // --- scroll region + line/char insert/delete --------------------------
487
488    /// DECSTBM: set scroll region `top;bottom` (1-based, clamped); resets the
489    /// cursor to the origin. An empty/`0` bottom means the last row.
490    fn set_scroll_region(&mut self, params: &[u16]) {
491        let rows = self.rows();
492        let top = at1(params, 0).saturating_sub(1);
493        let bottom = match at0(params, 1) {
494            0 => rows - 1,
495            b => (b as usize - 1).min(rows - 1),
496        };
497        // Ignore an inverted/empty region, as VT100 does.
498        if top < bottom {
499            self.scroll_top = top;
500            self.scroll_bottom = bottom;
501        }
502        self.row = 0;
503        self.col = 0;
504        self.wrap_pending = false;
505    }
506
507    /// Scroll the scroll region up by `n` lines: top lines lost, blanks in at
508    /// the bottom margin.
509    fn scroll_up(&mut self, n: usize) {
510        let (top, bottom) = (self.scroll_top, self.scroll_bottom);
511        let cols = self.cols();
512        let n = n.min(bottom - top + 1);
513        for r in top..=bottom {
514            for c in 0..cols {
515                let cell = if r + n <= bottom {
516                    self.active().cell(r + n, c)
517                } else {
518                    Cell::default()
519                };
520                self.active_mut().set(r, c, cell);
521            }
522        }
523    }
524
525    /// Scroll the scroll region down by `n` lines: bottom lines lost, blanks in
526    /// at the top margin.
527    fn scroll_down(&mut self, n: usize) {
528        let (top, bottom) = (self.scroll_top, self.scroll_bottom);
529        let cols = self.cols();
530        let n = n.min(bottom - top + 1);
531        for r in (top..=bottom).rev() {
532            for c in 0..cols {
533                let cell = if r >= top + n {
534                    self.active().cell(r - n, c)
535                } else {
536                    Cell::default()
537                };
538                self.active_mut().set(r, c, cell);
539            }
540        }
541    }
542
543    /// IL: insert `n` blank lines at the cursor row, within the scroll region;
544    /// lines below shift down and fall off the bottom margin. No-op outside the
545    /// region.
546    fn insert_lines(&mut self, n: usize) {
547        if self.row < self.scroll_top || self.row > self.scroll_bottom {
548            return;
549        }
550        let saved_top = self.scroll_top;
551        self.scroll_top = self.row;
552        self.scroll_down(n);
553        self.scroll_top = saved_top;
554        self.col = 0;
555        self.wrap_pending = false;
556    }
557
558    /// DL: delete `n` lines at the cursor row, within the scroll region; lines
559    /// below shift up and blanks fill the bottom margin. No-op outside region.
560    fn delete_lines(&mut self, n: usize) {
561        if self.row < self.scroll_top || self.row > self.scroll_bottom {
562            return;
563        }
564        let saved_top = self.scroll_top;
565        self.scroll_top = self.row;
566        self.scroll_up(n);
567        self.scroll_top = saved_top;
568        self.col = 0;
569        self.wrap_pending = false;
570    }
571
572    /// ICH: insert `n` blank cells at the cursor, shifting the rest of the line
573    /// right off the edge.
574    fn insert_chars(&mut self, n: usize) {
575        let cols = self.cols();
576        let (r, start) = (self.row, self.col);
577        let n = n.min(cols - start);
578        for c in (start..cols).rev() {
579            let cell = if c >= start + n {
580                self.active().cell(r, c - n)
581            } else {
582                Cell::default()
583            };
584            self.active_mut().set(r, c, cell);
585        }
586        self.wrap_pending = false;
587    }
588
589    /// DCH: delete `n` cells at the cursor, shifting the rest of the line left
590    /// and filling the tail with blanks.
591    fn delete_chars(&mut self, n: usize) {
592        let cols = self.cols();
593        let (r, start) = (self.row, self.col);
594        let n = n.min(cols - start);
595        for c in start..cols {
596            let cell = if c + n < cols {
597                self.active().cell(r, c + n)
598            } else {
599                Cell::default()
600            };
601            self.active_mut().set(r, c, cell);
602        }
603        self.wrap_pending = false;
604    }
605
606    // --- save / restore + ESC --------------------------------------------
607
608    fn save_cursor(&mut self) {
609        self.saved = Some((self.row, self.col, self.style));
610    }
611
612    fn restore_cursor(&mut self) {
613        if let Some((r, c, style)) = self.saved {
614            self.row = r.min(self.rows() - 1);
615            self.col = c.min(self.cols() - 1);
616            self.style = style;
617        }
618        self.wrap_pending = false;
619    }
620
621    fn esc(&mut self, final_byte: u8) {
622        match final_byte {
623            b'7' => self.save_cursor(),    // DECSC
624            b'8' => self.restore_cursor(), // DECRC
625            _ => {}                        // charset selection etc.: ignore quietly
626        }
627    }
628}
629
630/// Tab stops every 8 columns (VT default), with a stop at column 0.
631fn default_tabs(cols: usize) -> Vec<bool> {
632    (0..cols).map(|c| c % 8 == 0).collect()
633}
634
635/// Read a param defaulting a missing/zero value to 1 (VT motion default).
636fn at1(params: &[u16], i: usize) -> usize {
637    match params.get(i).copied().unwrap_or(0) {
638        0 => 1,
639        v => v as usize,
640    }
641}
642
643/// Read a param defaulting a missing value to 0 (erase/selector default).
644fn at0(params: &[u16], i: usize) -> u16 {
645    params.get(i).copied().unwrap_or(0)
646}
647
648/// Copy the overlapping top-left region of `src` into `dst` (for resize).
649fn copy_top_left(src: &Screen, dst: &mut Screen) {
650    let rows = src.rows().min(dst.rows());
651    let cols = src.cols().min(dst.cols());
652    for r in 0..rows {
653        for c in 0..cols {
654            dst.set(r, c, src.cell(r, c));
655        }
656    }
657}