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