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