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}