Skip to main content

shpool_vterm/
lib.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::BTreeMap;
16
17use crate::{
18    cell::Cell,
19    screen::{SavedCursor, Screen},
20    term::{
21        AsTermInput, BlinkStyle, ControlCodes, FontWeight, FrameStyle, LinkTarget, OriginMode,
22        Region, UnderlineStyle,
23    },
24};
25
26use bitvec::{bitvec, vec::BitVec};
27use smallvec::SmallVec;
28use unicode_width::UnicodeWidthChar;
29
30#[macro_use]
31mod visibility;
32
33#[macro_use]
34mod log;
35
36mod altscreen;
37mod cell;
38mod line;
39mod screen;
40mod scrollback;
41
42#[cfg(not(feature = "unstable-internal-test"))]
43mod term;
44
45#[cfg(feature = "unstable-internal-test")]
46pub mod term;
47
48const MAX_TITLE_STACK_DEPTH: usize = 64;
49
50/// A representation of a terminal.
51pub struct Term {
52    parser: vte::Parser,
53    state: State,
54    logger: log::Context,
55}
56
57impl Term {
58    /// Create a new terminal with the given width and height.
59    ///
60    /// Note that width will only be used when generated output
61    /// to determine where wrapping should be place.
62    ///
63    /// scrollback_lines must be at least size.height. If it is
64    /// less than size.height, it will be automatically adjusted
65    /// to be equal to size.height.
66    pub fn new(scrollback_lines: usize, size: Size) -> Self {
67        Term {
68            parser: vte::Parser::new(),
69            state: State::new(scrollback_lines, size),
70            logger: log::Context::None,
71        }
72    }
73
74    /// Attach a tag to this term to help uniquely identify it
75    /// in log and error messages. This is useful for applications
76    /// which juggle multiple vterm instances at once.
77    pub fn tag(&mut self, tag: String) {
78        let logger = log::Context::Tag(tag);
79        self.logger = logger.clone();
80        self.state.set_logger(logger);
81    }
82
83    /// Get the current terminal size.
84    pub fn size(&self) -> Size {
85        self.state.screen().size
86    }
87
88    /// Set the terminal size.
89    ///
90    /// This will implicitly size up the scrollback_lines if
91    /// it is currently less than size.height.
92    pub fn resize(&mut self, size: Size) {
93        if size.height > self.scrollback_lines() {
94            self.set_scrollback_lines(size.height);
95        }
96
97        self.state.resize(size);
98    }
99
100    /// Get the current number of lines of stored scrollback.
101    pub fn scrollback_lines(&self) -> usize {
102        self.state.scrollback.scrollback_lines().expect("scrollback screen to have lines")
103    }
104
105    /// Set the number of lines of scrollback to store. This will drop
106    /// data when resizing down. When resizing up, no new memory is allocated,
107    /// capacity is simply expanded.
108    ///
109    /// If the given value is less than size().height, it will be overridden
110    /// to match the current height. You cannot store less scrollback than
111    /// there are lines in the visible screen region.
112    pub fn set_scrollback_lines(&mut self, scrollback_lines: usize) {
113        self.state.scrollback.set_scrollback_lines(scrollback_lines);
114    }
115
116    /// Process the given chunk of input. This should be the data read off
117    /// a pty running a shell.
118    pub fn process(&mut self, buf: &[u8]) {
119        self.parser.advance(&mut self.state, buf);
120    }
121
122    /// Get the current contents of the terminal encoded via terminal
123    /// escape sequences. The contents buffer will be prefixed with
124    /// a reset code, so inputing this to any terminal emulator will
125    /// reset the emulator to the contents of this Term instance.
126    pub fn contents(&self, dump_region: ContentRegion) -> Vec<u8> {
127        let mut buf = vec![];
128
129        // Reset alone does not terminate active links, so before
130        // we issue a reset, we'll issue an end link to fully
131        // reset the link.
132        term::control_codes().end_link.term_input_into(&mut buf);
133
134        term::control_codes().clear_attrs.term_input_into(&mut buf);
135
136        // We cannot know what state the terminal we are restoring into is
137        // in, and a leftover scroll region or origin mode would scroll the
138        // contents we are about to paint. Clear them before homing the
139        // cursor, since origin mode moves where home is.
140        term::control_codes().unset_scroll_region.term_input_into(&mut buf);
141        term::control_codes().disable_scroll_region_origin_mode.term_input_into(&mut buf);
142
143        term::ControlCodes::cursor_position(1, 1).term_input_into(&mut buf);
144        term::control_codes().clear_screen.term_input_into(&mut buf);
145        self.state.dump_contents_into(&mut buf, dump_region);
146
147        buf
148    }
149}
150
151/// A section of the screen to dump.
152#[derive(Debug, Eq, PartialEq, Clone)]
153pub enum ContentRegion {
154    /// The whole terminal state, including all scrollback data.
155    All,
156    /// Only the visible lines.
157    Screen,
158    /// The bottom N lines, including (N - height) lines of scrollback.
159    BottomLines(usize),
160}
161
162impl std::fmt::Display for Term {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        self.state.fmt(f)
165    }
166}
167
168/// The mouse reporting modes we track, in the order they get replayed into
169/// a restore buffer.
170///
171/// 1000, 1002 and 1003 select how much the client reports (press only, press
172/// plus drag, or all motion). 1005, 1006, 1015 and 1016 select how those
173/// reports are encoded. All of them change what the client writes to the pty,
174/// so dropping them on reattach leaves the client and the application
175/// disagreeing about the wire format.
176const MOUSE_MODES: [u16; 7] = [1000, 1002, 1003, 1005, 1006, 1015, 1016];
177
178/// The index into `State::mouse_modes` for a DEC private mode parameter.
179fn mouse_mode_idx(param: &[u16]) -> Option<usize> {
180    match param {
181        [mode] => MOUSE_MODES.iter().position(|m| m == mode),
182        _ => None,
183    }
184}
185
186/// The size of the terminal.
187#[derive(Debug, Clone, Copy, Eq, PartialEq)]
188pub struct Size {
189    pub width: usize,
190    pub height: usize,
191}
192
193/// The complete terminal state. An internal implementation detail.
194struct State {
195    /// The state for the normal terminal screen.
196    scrollback: Screen,
197    /// The state for the alternate screen.
198    altscreen: Screen,
199    /// The currently active screen mode.
200    screen_mode: ScreenMode,
201    /// The last graphic char that was printed. This is used by REP
202    /// (CSI Pn b).
203    last_print_char: Option<char>,
204    /// The current cursor attrs. These are shared between the scrollback
205    /// and alt screens, which is why they are stored here rather than
206    /// with the curors themsevles. If we think of the cursor as a paintbrush,
207    /// these attrs are the color paint that it is currently holding.
208    cursor_attrs: term::Attrs,
209    /// The style for the cursor itself, not for the characters that
210    /// the cursor is emitting.
211    cursor_style: term::CursorStyle,
212    /// The terminal title, as set by `OSC 0` and `OSC 2`.
213    title_stack: Vec<SmallVec<[u8; 8]>>,
214    /// The terminal icon name, as set by `OSC 0` and `OSC 1`.
215    icon_name_stack: Vec<SmallVec<[u8; 8]>>,
216    /// The terminal working directory (some terminal emulators use this
217    /// to know what directory to start new shells in).
218    working_dir: Option<WorkingDir>,
219    /// A table mapping color index to a particular color spec.
220    /// This is set by OSC 4. We use a tree for deterministic output
221    /// to make testing easier. A hash would work just as well.
222    palette_overrides: BTreeMap<usize, Vec<u8>>,
223    /// Color overrides for things like foreground and background.
224    /// These slots extend from OSC 10 to OSC 19.
225    functional_colors: [Option<Vec<u8>>; 10],
226    /// Tracks if the cursor is currently hidden. Controlled
227    /// via the `CSI ? 25 {h,l}` codes.
228    cursor_hidden: bool,
229    /// Tracks cursor blinking mode. Controlled via `CSI ? 12 {h,l}`.
230    cursor_blinking: Option<bool>,
231    /// Tracks application cursor keys mode (DECCKM), which changes what the
232    /// arrow keys send. Controlled via `CSI ? 1 {h,l}`.
233    application_cursor_keys_enabled: bool,
234    /// Tracks application keypad mode (DECKPAM), which changes what the
235    /// numeric keypad sends. Controlled via `ESC =` and `ESC >`.
236    ///
237    /// This is a different mode to DECCKM above, covering a different group
238    /// of keys, so the two cannot share a flag.
239    application_keypad_mode_enabled: bool,
240    /// Tracks the mouse reporting modes listed in `MOUSE_MODES`, indexed
241    /// in parallel with it.
242    ///
243    /// The tracking modes and the encoding modes are recorded independently
244    /// and replayed exactly as the application set them, rather than being
245    /// collapsed into a single effective mode, since the application will
246    /// expect everything it set to still be in force after a reattach.
247    mouse_modes: [bool; MOUSE_MODES.len()],
248    /// When set, the underlying terminal is supposed to emit
249    /// `\x1b[I` sentinals when the window gains focus. For our
250    /// purposes we just need to know how to track and restore
251    /// the state.
252    ///
253    /// Controlled via `CSI ? 1004 {h,l}`.
254    report_focus: bool,
255    /// Tracks paste mode. Controlled via `CSI ? 2004 {h,l}`.
256    in_paste_mode: bool,
257    /// Tracks insertion / replacement mode (IRM). Controlled via `CSI 4 {h,l}`.
258    insert_mode: bool,
259    /// Tab stop columns. By default, these are spaced 8 cols apart
260    /// starting at col 9, but they can be directly manipulated by certain
261    /// control codes as well.
262    tabstops: BitVec,
263    logger: log::Context,
264}
265
266struct WorkingDir {
267    host: SmallVec<[u8; 8]>,
268    dir: SmallVec<[u8; 8]>,
269}
270
271impl std::fmt::Display for State {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        match self.screen_mode {
274            ScreenMode::Scrollback => {
275                writeln!(f, "Screen Mode: Scrollback")?;
276                write!(f, "{}", self.scrollback)?;
277            }
278            ScreenMode::Alt => {
279                writeln!(f, "Screen Mode: AltScreen")?;
280                write!(f, "{}", self.altscreen)?;
281            }
282        }
283
284        Ok(())
285    }
286}
287
288impl State {
289    fn new(scrollback_lines: usize, size: Size) -> Self {
290        let mut st = State {
291            scrollback: Screen::scrollback(scrollback_lines, size),
292            altscreen: Screen::alt(size),
293            screen_mode: ScreenMode::Scrollback,
294            cursor_attrs: term::Attrs::default(),
295            cursor_style: term::CursorStyle::Default,
296            title_stack: vec![],
297            icon_name_stack: vec![],
298            working_dir: None,
299            palette_overrides: BTreeMap::new(),
300            functional_colors: [NONE_VEC; 10],
301            cursor_hidden: false,
302            cursor_blinking: None,
303            application_cursor_keys_enabled: false,
304            application_keypad_mode_enabled: false,
305            mouse_modes: [false; MOUSE_MODES.len()],
306            report_focus: false,
307            in_paste_mode: false,
308            insert_mode: false,
309            tabstops: bitvec![0; size.width],
310            last_print_char: None,
311            logger: log::Context::None,
312        };
313        st.fill_tabstops(0, size.width);
314        st
315    }
316
317    fn set_logger(&mut self, logger: log::Context) {
318        self.scrollback.set_logger(logger.clone());
319        self.altscreen.set_logger(logger.clone());
320        self.logger = logger;
321    }
322
323    fn screen_mut(&mut self) -> &mut Screen {
324        match self.screen_mode {
325            ScreenMode::Scrollback => &mut self.scrollback,
326            ScreenMode::Alt => &mut self.altscreen,
327        }
328    }
329
330    fn screen(&self) -> &Screen {
331        match self.screen_mode {
332            ScreenMode::Scrollback => &self.scrollback,
333            ScreenMode::Alt => &self.altscreen,
334        }
335    }
336
337    fn resize(&mut self, size: Size) {
338        let orig_len = self.tabstops.len();
339        self.tabstops.resize(size.width, false);
340        if size.width > orig_len {
341            self.fill_tabstops(orig_len, size.width);
342        }
343
344        self.scrollback.resize(size);
345        self.altscreen.resize(size);
346    }
347
348    /// Fill in the default tabstops within the given range.
349    fn fill_tabstops(&mut self, start: usize, end: usize) {
350        assert!(end <= self.tabstops.len());
351
352        for i in start..end {
353            if i > 0 && i % 8 == 0 {
354                self.tabstops.set(i, true);
355            }
356        }
357    }
358
359    /// Dump the current tabstop state into the given control code
360    /// vector. This is assumed to be right after a reset, so it will
361    /// elide setting tabstops in the default position. The cursor
362    /// MUST be in position (1, 1) when this routine is called.
363    fn dump_tabstops(&self, buf: &mut Vec<u8>) {
364        let controls = term::control_codes();
365        if self.tabstops.len() > 8 && self.tabstops.not_any() {
366            // If there are no tabstops, we just clobber them all as
367            // a special case to help speed things up a bit.
368            ControlCodes::tab_clear(Some(3)).term_input_into(buf);
369            return;
370        }
371
372        let mut codes = vec![];
373        for i in 0..self.tabstops.len() {
374            let bit = self.tabstops.get(i).is_some_and(|b| *b);
375            let i: u16 = match i.try_into() {
376                Ok(i) => i,
377                Err(e) => {
378                    warn!(self.logger, "generating tabstop codes: index out of bounds: {:?}", e);
379                    return;
380                }
381            };
382            if i > 0 && i % 8 == 0 {
383                // this is set by default
384                if !bit {
385                    codes.push(ControlCodes::cursor_position(1, i + 1));
386                    codes.push(ControlCodes::tab_clear(None));
387                }
388            } else {
389                // this is unset by default
390                if bit {
391                    codes.push(ControlCodes::cursor_position(1, i + 1));
392                    codes.push(controls.horizontal_tab_set.clone());
393                }
394            }
395        }
396
397        if !codes.is_empty() {
398            for code in codes.into_iter() {
399                code.term_input_into(buf);
400            }
401            ControlCodes::cursor_position(1, 1).term_input_into(buf);
402        }
403    }
404
405    fn dump_contents_into(&self, buf: &mut Vec<u8>, dump_region: ContentRegion) {
406        self.dump_tabstops(buf);
407
408        match self.screen_mode {
409            ScreenMode::Scrollback => self.scrollback.dump_contents_into(buf, dump_region),
410            ScreenMode::Alt => {
411                // Restore the regular scrollback first so that after the user
412                // exits their curses app, they can still see shell history.
413                self.scrollback.dump_contents_into(buf, dump_region.clone());
414
415                // Re-enable alt screen, then dump the contents. This is
416                // not actually super important in practice because basically
417                // every curses app respects SIGWINCH. We may even want to
418                // consider exposing a knob to disable alt-screen dumping
419                // since it might make things less flickery. Not worth doing
420                // for now though.
421                term::control_codes().enable_alt_screen.term_input_into(buf);
422
423                // Switching screens does not clear the scroll region or
424                // origin mode the scrollback restore just set, and neither
425                // is per-screen in a real terminal, so we have to clear them
426                // ourselves. This has to happen before the contents get
427                // painted, since it is the paint that a stranded scroll
428                // region corrupts.
429                self.scrollback.dump_global_state_reset_into(buf);
430
431                self.altscreen.dump_contents_into(buf, dump_region)
432            }
433        }
434
435        let controls = term::control_codes();
436
437        // restore cursor attributes (the screen will have already restored our
438        // position).
439        controls.clear_attrs.term_input_into(buf);
440        let mut cursor_attrs = self.cursor_attrs.clone();
441        // Avoid starting a link even if there is one active in the
442        // terminal state because the reconnecting terminal almost
443        // certainly has forgotten it was in the middle of drawing
444        // a link and will wind up creating a massive link if we
445        // fully faithfully restore the cursor attr state..
446        cursor_attrs.link_target = None;
447        let codes = term::Attrs::default().transition_to(&cursor_attrs);
448        for c in codes.into_iter() {
449            c.term_input_into(buf);
450        }
451        if self.cursor_style != term::CursorStyle::Default {
452            self.cursor_style.term_input_into(buf);
453        }
454
455        // Restore the title / icon name. Most terminals treat theses as the
456        // same thing these days, but we'll go the extra mile and differentiate
457        // rather than just always sending `OSC 0 ; <title> ST` in case there is
458        // a terminal that actually makes a distinction.
459        match (self.title_stack.last(), self.icon_name_stack.last()) {
460            (Some(title), Some(icon_name)) if !title.is_empty() && title == icon_name => {
461                ControlCodes::set_title_and_icon_name(title.clone()).term_input_into(buf)
462            }
463            (Some(title), Some(icon_name)) => {
464                if !title.is_empty() {
465                    ControlCodes::set_title(title.clone()).term_input_into(buf);
466                }
467                if !icon_name.is_empty() {
468                    ControlCodes::set_icon_name(icon_name.clone()).term_input_into(buf);
469                }
470            }
471            (Some(title), None) => {
472                if !title.is_empty() {
473                    ControlCodes::set_title(title.clone()).term_input_into(buf);
474                }
475            }
476            (None, Some(icon_name)) => {
477                if !icon_name.is_empty() {
478                    ControlCodes::set_icon_name(icon_name.clone()).term_input_into(buf);
479                }
480            }
481            (None, None) => {}
482        }
483
484        if let Some(working_dir) = &self.working_dir {
485            ControlCodes::set_working_dir(working_dir.host.clone(), working_dir.dir.clone())
486                .term_input_into(buf);
487        }
488
489        if !self.palette_overrides.is_empty() {
490            ControlCodes::set_color_indices(
491                self.palette_overrides
492                    .iter()
493                    .map(|(idx, color_spec)| (*idx, SmallVec::from(color_spec.as_slice()))),
494            )
495            .term_input_into(buf);
496        }
497
498        if self.cursor_hidden {
499            controls.hide_cursor.term_input_into(buf);
500        }
501        if let Some(blinking) = self.cursor_blinking {
502            if blinking {
503                controls.enable_cursor_blink.term_input_into(buf);
504            } else {
505                controls.disable_cursor_blink.term_input_into(buf);
506            }
507        }
508        if self.application_cursor_keys_enabled {
509            controls.enable_application_cursor_keys.term_input_into(buf);
510        }
511        if self.application_keypad_mode_enabled {
512            controls.enable_application_keypad_mode.term_input_into(buf);
513        }
514        if self.report_focus {
515            controls.enable_report_focus.term_input_into(buf);
516        }
517        if self.in_paste_mode {
518            controls.enable_paste_mode.term_input_into(buf);
519        }
520        if self.insert_mode {
521            controls.enable_insert_mode.term_input_into(buf);
522        }
523        for (idx, mode) in MOUSE_MODES.iter().enumerate() {
524            if self.mouse_modes[idx] {
525                ControlCodes::dec_private_modes_set(&[*mode]).term_input_into(buf);
526            }
527        }
528
529        // Generate fused functional color commands from any runs in the
530        // functional colors table.
531        let mut functional_color_idx = 0;
532        while functional_color_idx < self.functional_colors.len() {
533            if let Some(color_spec) = &self.functional_colors[functional_color_idx] {
534                let start_idx = functional_color_idx;
535                let mut color_specs = vec![color_spec.as_slice()];
536
537                functional_color_idx += 1;
538                while functional_color_idx < self.functional_colors.len() {
539                    if let Some(s) = &self.functional_colors[functional_color_idx] {
540                        color_specs.push(s.as_slice());
541                    } else {
542                        break;
543                    }
544                    functional_color_idx += 1;
545                }
546
547                ControlCodes::set_functional_color(start_idx, color_specs).term_input_into(buf);
548            }
549
550            functional_color_idx += 1;
551        }
552    }
553
554    /// Set a run within the functional colors table starting at the given
555    /// index. This implements OSC 10 through OSC 19.
556    fn set_functional_color<'a, I>(&mut self, mut idx: usize, mut params_iter: I)
557    where
558        I: Iterator<Item = &'a &'a [u8]>,
559    {
560        while let Some(color_spec) = params_iter.next() {
561            if idx >= self.functional_colors.len() {
562                return;
563            }
564
565            if *color_spec != [b'?'] {
566                self.functional_colors[idx] = Some(Vec::from(*color_spec));
567            }
568
569            idx += 1;
570        }
571    }
572
573    fn set_title(&mut self, title: SmallVec<[u8; 8]>) {
574        if let Some(top) = self.title_stack.last_mut() {
575            *top = title;
576        } else {
577            self.title_stack.push(title);
578        }
579    }
580
581    fn set_icon_name(&mut self, icon_name: SmallVec<[u8; 8]>) {
582        if let Some(top) = self.icon_name_stack.last_mut() {
583            *top = icon_name;
584        } else {
585            self.icon_name_stack.push(icon_name);
586        }
587    }
588
589    fn write_char_at_cursor(&mut self, cell: Cell) {
590        let insert_mode = self.insert_mode;
591        let screen = self.screen_mut();
592        screen.snap_to_bottom();
593
594        // In insert mode (ECMA-48 IRM), incoming characters do not overwrite
595        // existing text under the cursor. Instead, existing characters are
596        // shifted to the right, dropping any characters that spill past the
597        // terminal width.
598        //
599        // `Line::insert_character` does not write `cell` itself; it inserts
600        // blank cells to make room for `cell.width()`. The subsequent
601        // call to `screen.write_at_cursor(cell)` then writes the actual
602        // character into the newly opened space at the cursor position
603        // and advances the cursor.
604        if insert_mode {
605            let width = screen.size.width;
606            let col = screen.cursor.col;
607            if col < width {
608                if let Some(l) = screen.get_line_mut() {
609                    l.insert_character(width, col, cell.width() as usize);
610                }
611            }
612        }
613
614        if let Err(e) = screen.write_at_cursor(cell) {
615            warn!(self.logger, "writing char at cursor: {:?}", e);
616        }
617    }
618
619    /// Attach a zero width char to the grapheme cluster it modifies.
620    ///
621    /// A zero width char describes the glyph to its left, which lives in the
622    /// cell the cursor most recently moved past. Wide chars leave padding
623    /// cells behind them, so we skip back over those to reach the cell that
624    /// actually owns the glyph. If there is no glyph to the left of the
625    /// cursor there is nothing to modify and we drop the char, which is what
626    /// xterm does.
627    fn add_modifier_char(&mut self, c: char) {
628        let screen = self.screen_mut();
629        let width = screen.size.width;
630        let Some(mut col) = screen.cursor.col.checked_sub(1) else {
631            return;
632        };
633
634        let Some(line) = screen.get_line_mut() else {
635            return;
636        };
637
638        while line.get_cell(width, col).is_some_and(|cell| cell.is_wide_padding()) {
639            match col.checked_sub(1) {
640                Some(prev) => col = prev,
641                None => return,
642            }
643        }
644
645        match line.get_cell_mut(width, col) {
646            // An empty cell renders as a space and has no cluster to extend.
647            Some(cell) if !cell.is_empty() => cell.add_char(c),
648            _ => {}
649        }
650    }
651}
652
653/// Indicates which screen mode is active.
654enum ScreenMode {
655    Scrollback,
656    Alt,
657}
658
659impl vte::Perform for State {
660    fn print(&mut self, c: char) {
661        trace!(self.logger, "print: {}", c);
662
663        match UnicodeWidthChar::width(c) {
664            // Control chars have no printable form. vte routes the C0 set to
665            // `execute`, but anything else that lands here would corrupt the
666            // restore buffer if we stored it in a cell.
667            None => {
668                warn!(self.logger, "print: dropping control char {:?}", c);
669                return;
670            }
671            // Combining marks, variation selectors and ZWJ modify the cluster
672            // to their left instead of occupying a column of their own.
673            Some(0) => {
674                self.add_modifier_char(c);
675                return;
676            }
677            Some(_) => {}
678        }
679
680        self.last_print_char = Some(c);
681        let attrs = self.cursor_attrs.clone();
682        self.write_char_at_cursor(Cell::new(c, attrs));
683    }
684
685    fn execute(&mut self, byte: u8) {
686        self.last_print_char = None;
687        trace!(self.logger, "execute: byte {}", byte);
688        match byte {
689            b'\n' => {
690                let screen = self.screen_mut();
691                let (scroll_top, scroll_bottom) =
692                    screen.scroll_region(false).as_region(&screen.size).row_bounds();
693                let within_scroll =
694                    scroll_top <= screen.cursor.row && screen.cursor.row < scroll_bottom;
695                screen.cursor.row += 1;
696                if within_scroll {
697                    if screen.cursor.row >= scroll_bottom {
698                        screen.scroll_down(1);
699                        screen.cursor.row -= 1;
700                    }
701                } else {
702                    screen.clamp();
703                }
704            }
705            b'\r' => self.screen_mut().cursor.col = 0,
706            b'\t' => {
707                let mut col = self.screen().cursor.col;
708                col += 1;
709                while col < self.tabstops.len() && !self.tabstops.get(col).is_some_and(|b| *b) {
710                    col += 1;
711                }
712
713                let screen = self.screen_mut();
714                screen.cursor.col = col;
715                screen.clamp();
716            }
717            b'\x08' => {
718                // backspace
719                let screen = self.screen_mut();
720                screen.cursor.col = screen.cursor.col.saturating_sub(1);
721            }
722            // bell, ignore
723            b'\x07' => {}
724            _ => {
725                warn!(self.logger, "execute: unhandled byte {}", byte);
726            }
727        }
728    }
729
730    fn hook(&mut self, _params: &vte::Params, intermediates: &[u8], ignore: bool, action: char) {
731        self.last_print_char = None;
732        debug!(
733            self.logger,
734            "unhandled hook{}: {:?} {}",
735            if ignore { " (ignored)" } else { "" },
736            intermediates,
737            action
738        );
739    }
740
741    fn put(&mut self, byte: u8) {
742        trace!(self.logger, "unhandled put: {}", byte);
743        self.last_print_char = None;
744    }
745
746    fn unhook(&mut self) {
747        debug!(self.logger, "unhandled unhook");
748        self.last_print_char = None;
749    }
750
751    // OSC commands are of the form
752    // `OSC <p1> ; <p2> ... <pn> <terminator>` where
753    // `OSC` is always `ESC]`, the params are byte sequences seperated by
754    // semicolons, and the terminator is either `BEL` (0x7) or
755    // `ST` (`ESC\`, 0x1b 0x5c). Modern applications use ST for the most
756    // part, but some older applications will send BEL. We should be able
757    // to just ignore the _bell_terminated flag and treat commands the
758    // same regardless of the terminator they have.
759    #[rustfmt::skip]
760    fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
761        trace!(self.logger, "osc_dispatch: {:?}", params);
762        self.last_print_char = None;
763
764        let mut params_iter = params.iter();
765        match params_iter.next() {
766            // Title manipulation
767            Some([b'0']) => if let Some(title) = params_iter.next() {
768                let title: SmallVec<[u8; 8]> = title.to_vec().into();
769                self.set_title(title.clone());
770                self.set_icon_name(title);
771            } else {
772                warn!(self.logger, "OSC 0 with no title param");
773            },
774            Some([b'1']) => if let Some(icon_name) = params_iter.next() {
775                let icon_name: SmallVec<[u8; 8]> = icon_name.to_vec().into();
776                self.set_icon_name(icon_name);
777            } else {
778                warn!(self.logger, "OSC 1 with no icon_name param");
779            },
780            Some([b'2']) => if let Some(title) = params_iter.next() {
781                let title: SmallVec<[u8; 8]> = title.to_vec().into();
782                self.set_title(title);
783            } else {
784                warn!(self.logger, "OSC 2 with no title param");
785            },
786
787            // Color Palette
788            Some([b'4']) => while let (Some(idx), Some(color_spec)) = (params_iter.next(), params_iter.next()) {
789                if *color_spec == [b'?'] {
790                    // If the program is querying for a color, we just ignore
791                    // that control code. The real terminal is responsible for
792                    // responding.
793                    continue;
794                }
795
796                match std::str::from_utf8(idx) {
797                    Ok(s) => match s.parse::<usize>() {
798                        Ok(i) => {
799                            self.palette_overrides.insert(i, color_spec.to_vec());
800                        },
801                        Err(e) => warn!(self.logger, "OSC 4: idx is an invalid number '{}': {}", s, e),
802                    },
803                    Err(e) => warn!(self.logger, "OSC 4: invalid idx '{:?}': {}", idx, e),
804                }
805            },
806            Some([b'1', b'0', b'4']) => while let Some(idx) = params_iter.next() {
807                match std::str::from_utf8(idx) {
808                    Ok(s) => match s.parse::<usize>() {
809                        Ok(i) => {
810                            self.palette_overrides.remove(&i);
811                        },
812                        Err(e) => warn!(self.logger, "OSC 104: idx is an invalid number '{}': {}", s, e),
813                    },
814                    Err(e) => warn!(self.logger, "OSC 104: invalid idx '{:?}': {}", idx, e),
815                }
816            },
817
818            // Working dir
819            Some([b'7']) => if let (Some(host), Some(dir)) = (params_iter.next(), params_iter.next()) {
820                self.working_dir = Some(WorkingDir {
821                    host: host.to_vec().into(),
822                    dir: dir.to_vec().into(),
823                });
824            } else {
825                warn!(self.logger, "OSC 7 with fewer than 2 params");
826            },
827
828            // Links. Depending on params, OSC 8 both starts and ends links.
829            Some([b'8']) => if let (Some(params), Some(url)) = (params_iter.next(), params_iter.next()) {
830                if params.is_empty() && url.is_empty() {
831                    self.cursor_attrs.link_target = None;
832                } else {
833                    self.cursor_attrs.link_target = Some(LinkTarget {
834                        params: SmallVec::from_slice(params),
835                        url: SmallVec::from_slice(url),
836                    });
837                }
838            } else {
839                self.cursor_attrs.link_target = None;
840            },
841
842            // Functional colors (foreground, background and whatnot).
843            Some([b'1', x]) if b'0' <= *x && *x <= b'9' =>
844                self.set_functional_color((*x - b'0') as usize, params_iter),
845
846            Some([b'5', b'2']) => debug!(self.logger, "ignoring OSC 52 (clipboard)"),
847            Some([b'9']) => debug!(self.logger, "ignoring OSC 9 (desktop notification)"),
848            Some([b'7', b'7', b'7']) => debug!(self.logger, "ignoring OSC 777"),
849            Some([b'1', b'3', b'3']) => debug!(self.logger, "ignoring OSC 133 (iterm2 marks)"),
850            Some([b'3', b'0', b'0', b'8']) => debug!(self.logger, "ignoring OSC 3008 (systemd context signaling)"),
851
852            _ => warn!(self.logger, "unhandled 'OSC {:?} {}'", params, if bell_terminated {
853                "BEL"
854            } else {
855                "ST"
856            }),
857        }
858    }
859
860    // Handle escape codes beginning with the CSI indicator ('\x1b[').
861    //
862    // rustfmt has insane ideas about match arm formatting and there is
863    // apparently no way to make it do the reasonable thing of preserving
864    // horizontal whitespace by placing loops directly in match arm statement
865    // position.
866    #[rustfmt::skip]
867    fn csi_dispatch(
868        &mut self,
869        params: &vte::Params,
870        intermediates: &[u8],
871        ignore: bool,
872        action: char,
873    ) {
874        if ignore {
875            warn!(self.logger, "malformed CSI seq");
876            return;
877        }
878        if tracing::enabled!(tracing::Level::TRACE) {
879            trace!(self.logger, "csi_dispatch: intermediates={:?} params={:?} {}",
880                intermediates, params.iter().collect::<Vec<_>>(), action);
881        }
882
883        let mut params_iter = params.iter();
884
885        if action != 'b' || !intermediates.is_empty() {
886            self.last_print_char = None;
887        }
888
889        match action {
890            // CUU (Cursor Up)
891            'A' => {
892                let n = param_or(&mut params_iter, 1) as usize;
893                let screen = self.screen_mut();
894                screen.cursor.row = screen.cursor.row.saturating_sub(n);
895                screen.clamp();
896            }
897            // CUD (Cursor Down)
898            'B' => {
899                let n = param_or(&mut params_iter, 1) as usize;
900                let screen = self.screen_mut();
901                screen.cursor.row += n;
902                screen.clamp();
903            }
904            // CUF (Cursor Forward)
905            'C' => {
906                let n = param_or(&mut params_iter, 1) as usize;
907                let screen = self.screen_mut();
908                screen.cursor.col += n;
909                screen.clamp();
910            }
911            // CUF (Cursor Backwards)
912            'D' => {
913                let n = param_or(&mut params_iter, 1) as usize;
914                let screen = self.screen_mut();
915                screen.cursor.col = screen.cursor.col.saturating_sub(n);
916                screen.clamp();
917            }
918            // CNL (Cursor Next Line)
919            'E' => {
920                let n = param_or(&mut params_iter, 1) as usize;
921                let screen = self.screen_mut();
922                screen.cursor.row += n;
923                screen.cursor.col = 0;
924                screen.clamp();
925            }
926            // CPL (Cursor Prev Line)
927            'F' => {
928                let n = param_or(&mut params_iter, 1) as usize;
929                let screen = self.screen_mut();
930                screen.cursor.row = screen.cursor.row.saturating_sub(n);
931                screen.cursor.col = 0;
932                screen.clamp();
933            }
934            // HPA (Horizontal Position Absolute, CSI n `)
935            // CHA (Cursor Horizontal Absolute, CSI n G)
936            '`' | 'G' => {
937                let n = param_or(&mut params_iter, 1) as usize;
938                let n = n.saturating_sub(1); // translate to 0 indexing
939
940                let screen = self.screen_mut();
941                screen.cursor.col = n;
942                screen.clamp();
943            }
944            // HVP (Horizontal and Vertical Position)
945            // CUP (Cursor Set Position)
946            'f' | 'H' => {
947                // parse the params and adjust 1 indexing to 0 indexing
948                let row = param_or(&mut params_iter, 1) as usize;
949                let col = param_or(&mut params_iter, 1) as usize;
950                let screen = self.screen_mut();
951                screen.set_cursor(term::Pos { row, col });
952                screen.clamp();
953            }
954            // ED (Erase in Display)
955            'J' => while let Some(code) = params_iter.next() {
956                match code {
957                    [] | [0] => self.screen_mut().erase_to_end(),
958                    [1] => self.screen_mut().erase_from_start(),
959                    [2] => self.screen_mut().erase(false),
960                    [3] => self.screen_mut().erase(true),
961                    _ => warn!(self.logger, "unhandled 'CSI {:?} J'", code),
962                }
963            }
964            // EL (Erase in Line)
965            'K' => while let Some(code) = params_iter.next() {
966                match code {
967                    [] | [0] => {
968                        let screen = self.screen_mut();
969                        let col = screen.cursor.col;
970                        if let Some(l) = screen.get_line_mut() {
971                            l.erase(line::Section::ToEnd(col));
972                        }
973                    }
974                    [1] => {
975                        let screen = self.screen_mut();
976                        let col = screen.cursor.col;
977                        if let Some(l) = screen.get_line_mut() {
978                            l.erase(line::Section::StartTo(col));
979                        }
980                    }
981                    [2] => if let Some(l) = self.screen_mut().get_line_mut() {
982                        l.erase(line::Section::Whole);
983                    }
984                    _ => warn!(self.logger, "unhandled 'CSI {:?} K'", code),
985                }
986            }
987            // IL (Insert Line)
988            'L' => {
989                let n = param_or(&mut params_iter, 1) as usize;
990                self.screen_mut().insert_lines(n);
991            }
992            // DL (Delete Line)
993            'M' => {
994                let n = param_or(&mut params_iter, 1) as usize;
995                self.screen_mut().delete_lines(n);
996            }
997            // SU (Scroll Up)
998            'S' => {
999                let n = param_or(&mut params_iter, 1) as usize;
1000                self.screen_mut().scroll_up(n as usize);
1001            }
1002            // CTC (Cusor Tabulation Control)
1003            'W' => {
1004                let code = param_or(&mut params_iter, 0) as usize;
1005                match code {
1006                    0 => {
1007                        let col = self.screen().cursor.col;
1008                        self.tabstops.set(col, true);
1009                    },
1010                    2 => {
1011                        let col = self.screen().cursor.col;
1012                        self.tabstops.set(col, false);
1013                    }
1014                    5 => {
1015                        self.tabstops.fill(false);
1016                    }
1017                    _ => warn!(self.logger, "unhandled 'CSI {:?} W'", code),
1018                }
1019            }
1020            // CBT (Cursor Backward Tabulation)
1021            'Z' if intermediates.is_empty() => {
1022                let n = param_or(&mut params_iter, 1) as usize;
1023                let mut col = self.screen().cursor.col;
1024                for _ in 0..n {
1025                    if col == 0 {
1026                        break;
1027                    }
1028                    col -= 1;
1029                    while col > 0 && !self.tabstops.get(col).is_some_and(|b| *b) {
1030                        col -= 1;
1031                    }
1032                }
1033
1034                let screen = self.screen_mut();
1035                screen.cursor.col = col;
1036                screen.clamp();
1037            }
1038            // SD (Scroll Down)
1039            'T' => {
1040                let n = param_or(&mut params_iter, 1) as usize;
1041                self.screen_mut().scroll_down(n as usize);
1042            }
1043
1044            // ICH (Insert Character)
1045            '@' => {
1046                let n = param_or(&mut params_iter, 1) as usize;
1047
1048                let screen = self.screen_mut();
1049                let width = screen.size.width;
1050                let col = screen.cursor.col;
1051                if let Some(l) = screen.get_line_mut() {
1052                    l.insert_character(width, col, n);
1053                }
1054            }
1055            // DCH (Delete Character)
1056            'P' => {
1057                let n = param_or(&mut params_iter, 1) as usize;
1058
1059                let attrs = self.cursor_attrs.clone();
1060
1061                let screen = self.screen_mut();
1062                let width = screen.size.width;
1063                let col = screen.cursor.col;
1064                if let Some(l) = screen.get_line_mut() {
1065                    l.delete_character(width, col, &attrs, n);
1066                }
1067            }
1068            // ECH (Erase Character)
1069            'X' => {
1070                let n = param_or(&mut params_iter, 1) as usize;
1071
1072                let attrs = self.cursor_attrs.clone();
1073
1074                let screen = self.screen_mut();
1075                let width = screen.size.width;
1076                let col = screen.cursor.col;
1077                if let Some(l) = screen.get_line_mut() {
1078                    l.erase_character(width, col, &attrs, n);
1079                }
1080            }
1081            // REP (Repeat Preceding Character)
1082            'b' if intermediates.is_empty() => if let Some(c) = self.last_print_char {
1083                let n = param_or(&mut params_iter, 1) as usize;
1084
1085                let cell = Cell::new(c, self.cursor_attrs.clone());
1086                for _ in 0..n {
1087                    self.write_char_at_cursor(cell.clone());
1088                }
1089            }
1090            'c' => debug!(self.logger, "CSI ... c - device attribute query"),
1091            // VPA (Vertical Line Position Absolute)
1092            'd' => {
1093                let row = param_or(&mut params_iter, 1) as usize;
1094                let col = self.screen().cursor.col + 1;
1095                let screen = self.screen_mut();
1096                screen.set_cursor(term::Pos { row, col });
1097                screen.clamp();
1098            }
1099
1100            // SCP (Save Cursor Position)
1101            's' => {
1102                let screen = self.screen_mut();
1103                let cursor = screen.cursor.clone();
1104                screen.saved_cursor.pos = cursor;
1105            }
1106            // Window Title Operations
1107            't' => while let Some(code) = params_iter.next() {
1108                match code {
1109                    [] | [0] => debug!(self.logger, "CSI 0 t - ignoring"),
1110                    [14, ..] => debug!(self.logger, "CSI 14 t - pixel size query"),
1111                    [16, ..] => debug!(self.logger, "CSI 16 t - cell size query"),
1112                    [18, ..] => debug!(self.logger, "CSI 18 t - term size query"),
1113                    [19, ..] => debug!(self.logger, "CSI 19 t - display size query"),
1114                    [22] => {
1115                        let code = param_or(&mut params_iter, 0) as usize;
1116                        if (code == 0 || code == 1) && self.icon_name_stack.len() < MAX_TITLE_STACK_DEPTH {
1117                            if let Some(icon_name) = self.icon_name_stack.last().cloned() {
1118                                self.icon_name_stack.push(icon_name);
1119                            } else {
1120                                self.icon_name_stack.push(SmallVec::new());
1121                            }
1122                        }
1123
1124                        if (code == 0 || code == 2) && self.title_stack.len() < MAX_TITLE_STACK_DEPTH {
1125                            if let Some(title) = self.title_stack.last().cloned() {
1126                                self.title_stack.push(title);
1127                            } else {
1128                                self.title_stack.push(SmallVec::new());
1129                            }
1130                        }
1131                    }
1132                    [23] => {
1133                        let code = param_or(&mut params_iter, 0) as usize;
1134                        if code == 0 || code == 1 {
1135                            self.icon_name_stack.pop();
1136                        }
1137
1138                        if code == 0 || code == 2 {
1139                            self.title_stack.pop();
1140                        }
1141                    }
1142                    _ => warn!(self.logger, "unhandled CSI ... {:?} t", code),
1143                }
1144            }
1145            // RCP (Restore Cursor Position)
1146            'u' => {
1147                let screen = self.screen_mut();
1148                screen.cursor = screen.saved_cursor.pos;
1149                screen.clamp();
1150            }
1151
1152            // TBC (Tabulation Clear, CSI 3 g, CSI 0 g, CSI g)
1153            'g' => {
1154                let code = param_or(&mut params_iter, 0) as usize;
1155                match code {
1156                    0 => {
1157                        let col = self.screen().cursor.col;
1158                        self.tabstops.set(col, false);
1159                    },
1160                    3 => {
1161                        self.tabstops.fill(false);
1162                    }
1163                    _ => warn!(self.logger, "unhandled 'CSI {:?} g'", code),
1164                }
1165            }
1166
1167            'h' => match intermediates {
1168                [] => while let Some(code) = params_iter.next() {
1169                    match code {
1170                        [4] => self.insert_mode = true,
1171                        _ => {
1172                            warn!(
1173                                self.logger,
1174                                "Unhandled CSI h command: CSI {:?} {:?} h",
1175                                intermediates,
1176                                params.iter().collect::<Vec<&[u16]>>()
1177                            );
1178                        }
1179                    }
1180                }
1181                [b'?'] => while let Some(code) = params_iter.next() {
1182                    match code {
1183                        [1] => self.application_cursor_keys_enabled = true,
1184                        // 132 Column Mode (DECCOLM). Terminal dimensions are controlled
1185                        // by the client window/multiplexer, not child process escape sequences.
1186                        [3] => {},
1187                        // Smooth Scroll Mode (DECSCLM). Visual display scrolling timing
1188                        // is irrelevant in a headless virtual terminal.
1189                        [4] => {},
1190                        [6] => self.screen_mut().set_origin_mode(OriginMode::ScrollRegion),
1191                        [12] => self.cursor_blinking = Some(true),
1192                        [25] => self.cursor_hidden = false,
1193                        [1004] => self.report_focus = true,
1194                        // enable alt screen
1195                        [1049] => {
1196                            // The alt-screen gets reset upon entry, so we need to
1197                            // clobber it here.
1198                            self.altscreen = Screen::alt(self.altscreen.size);
1199                            self.screen_mode = ScreenMode::Alt;
1200                        }
1201                        [2004] => self.in_paste_mode = true,
1202                        // Means "pause visual rendering." We are not rendering
1203                        // anything visually so we don't care.
1204                        [2026] => {},
1205
1206                        _ => {
1207                            if let Some(idx) = mouse_mode_idx(code) {
1208                                self.mouse_modes[idx] = true;
1209                            } else {
1210                                warn!(
1211                                    self.logger,
1212                                    "Unhandled CSI h command: CSI {:?} {:?} h",
1213                                    intermediates,
1214                                    params.iter().collect::<Vec<&[u16]>>()
1215                                );
1216                            }
1217                        }
1218                    }
1219                }
1220                _ => warn!(
1221                    self.logger,
1222                    "Unhandled CSI h command: CSI {:?} {:?} h",
1223                    intermediates,
1224                    params.iter().collect::<Vec<&[u16]>>()
1225                ),
1226            }
1227            'l' => match intermediates {
1228                [] => while let Some(code) = params_iter.next() {
1229                    match code {
1230                        [4] => self.insert_mode = false,
1231                        _ => {
1232                            warn!(
1233                                self.logger,
1234                                "Unhandled CSI l command: CSI {:?} {:?} l",
1235                                intermediates,
1236                                params.iter().collect::<Vec<&[u16]>>()
1237                            );
1238                        }
1239                    }
1240                }
1241                [b'?'] => while let Some(code) = params_iter.next() {
1242                    match code {
1243                        [1] => self.application_cursor_keys_enabled = false,
1244                        // 80 Column Mode (DECCOLM). Terminal dimensions are controlled
1245                        // by the client window/multiplexer. Standard terminfo `is2` sends
1246                        // `\E[?3;4l` on startup; resetting column width or clearing the screen
1247                        // here would break sessions wider than 80 columns.
1248                        [3] => {},
1249                        // Jump Scroll Mode (DECSCLM). Visual display scrolling timing
1250                        // is irrelevant in a headless virtual terminal.
1251                        [4] => {},
1252                        [6] => self.screen_mut().set_origin_mode(OriginMode::Term),
1253                        [12] => self.cursor_blinking = Some(false),
1254                        [25] => self.cursor_hidden = true,
1255                        [1004] => self.report_focus = false,
1256                        [1049] => self.screen_mode = ScreenMode::Scrollback,
1257                        [2004] => self.in_paste_mode = false,
1258                        // Means "resume & flush visual rendering." We are
1259                        // not rendering anything visually so we don't care.
1260                        [2026] => {},
1261                        _ => {
1262                            if let Some(idx) = mouse_mode_idx(code) {
1263                                self.mouse_modes[idx] = false;
1264                            } else {
1265                                warn!(
1266                                    self.logger,
1267                                    "Unhandled CSI l command: CSI {:?} {:?} l",
1268                                    intermediates,
1269                                    params.iter().collect::<Vec<&[u16]>>()
1270                                );
1271                            }
1272                        }
1273                    }
1274                }
1275                _ => warn!(
1276                    self.logger,
1277                    "Unhandled CSI l command: CSI {:?} {:?} l",
1278                    intermediates,
1279                    params.iter().collect::<Vec<&[u16]>>()
1280                ),
1281            },
1282            // DSR (Device Status Report)
1283            'n' => while let Some(param) = params_iter.next() {
1284                match param {
1285                    // TODO: We might want to store this to assert against the
1286                    // terminal output stream once we start scanning that.
1287                    // We'll need to implement terminal output stream scanning
1288                    // in order to properly handle kitty extensions at some
1289                    // point (since we need to know if the real terminal
1290                    // responded with a code indicating that it supported the
1291                    // extensions in order to determine how we should interpret
1292                    // control codes).
1293                    [6] => debug!(self.logger, "ignoring DSR (CSI 6 n), that's the real terminal's job"),
1294                    _ => {}
1295                }
1296            },
1297
1298            // cell attribute manipulation
1299            'm' => while let Some(param) = params_iter.next() {
1300                match param {
1301                    [] | [0] => self.cursor_attrs = term::Attrs::default(),
1302
1303                    // Underline Handling
1304                    // TODO: there are lots of other underline styles. To fix,
1305                    // we need to update attrs.
1306                    //
1307                    // Kitty extensions:
1308                    //      CSI 4 : 3 m => curly
1309                    //      CSI 4 : 2 m => double
1310                    //
1311                    // Other:
1312                    //      CSI 58 ; 2 ; r ; g ; b m => RGB colored underline
1313                    [4] => self.cursor_attrs.underline = Some(UnderlineStyle::Single),
1314                    [21] => self.cursor_attrs.underline = Some(UnderlineStyle::Double),
1315                    [24] => self.cursor_attrs.underline = None,
1316
1317                    // Font Weight Handling.
1318                    [1] => self.cursor_attrs.font_weight = Some(FontWeight::Bold),
1319                    [2] => self.cursor_attrs.font_weight = Some(FontWeight::Faint),
1320                    [22] => self.cursor_attrs.font_weight = None,
1321
1322                    // Italic Handling.
1323                    [3] => self.cursor_attrs.italic = true,
1324                    [23] => self.cursor_attrs.italic = false,
1325
1326                    // Inverse Handling.
1327                    [7] => self.cursor_attrs.inverse = true,
1328                    [27] => self.cursor_attrs.inverse = false,
1329
1330                    // Blink Handling
1331                    [5] => self.cursor_attrs.blink = Some(BlinkStyle::Slow),
1332                    [6] => self.cursor_attrs.blink = Some(BlinkStyle::Rapid),
1333                    [25] => self.cursor_attrs.blink = None,
1334
1335                    // Conceal Handling
1336                    [8] => self.cursor_attrs.conceal = true,
1337                    [28] => self.cursor_attrs.conceal = false,
1338
1339                    // Strikethrough Handling.
1340                    [9] => self.cursor_attrs.strikethrough = true,
1341                    [29] => self.cursor_attrs.strikethrough = false,
1342
1343                    // Frame Handling.
1344                    [51] => self.cursor_attrs.framed = Some(FrameStyle::Frame),
1345                    [52] => self.cursor_attrs.framed = Some(FrameStyle::Circle),
1346                    [54] => self.cursor_attrs.framed = None,
1347
1348                    // Overline Handling.
1349                    [53] => self.cursor_attrs.overline = true,
1350                    [55] => self.cursor_attrs.overline = false,
1351
1352                    // Underline Color Handling.
1353                    [59] => self.cursor_attrs.underline_color = term::Color::Default,
1354                    param if !param.is_empty() && param[0] == 58 => {
1355                        match parse_extended_color(param, &mut params_iter) {
1356                            Some(color) => self.cursor_attrs.underline_color = color,
1357                            None => warn!(self.logger, "unhandled incomplete 'CSI 58 ... m'"),
1358                        }
1359                    }
1360
1361                    // Background Color Handling.
1362                    [49] => self.cursor_attrs.bgcolor = term::Color::Default,
1363                    [n] if 40 <= *n && *n < 48 => match (*n - 40).try_into() {
1364                        Ok(i) => self.cursor_attrs.bgcolor = term::Color::Idx(i),
1365                        Err(e) => warn!(self.logger, "out of bounds bgcolor idx (1): {:?}", e),
1366                    }
1367                    [n] if 100 <= *n && *n < 108 => match (*n - 92).try_into() {
1368                        Ok(i) => self.cursor_attrs.bgcolor = term::Color::Idx(i),
1369                        Err(e) => warn!(self.logger, "out of bounds bgcolor idx (2): {:?}", e),
1370                    }
1371                    param if !param.is_empty() && param[0] == 48 => {
1372                        match parse_extended_color(param, &mut params_iter) {
1373                            Some(color) => self.cursor_attrs.bgcolor = color,
1374                            None => warn!(self.logger, "unhandled incomplete 'CSI 48 ... m'"),
1375                        }
1376                    }
1377
1378                    // Foreground Color Handling.
1379                    [39] => self.cursor_attrs.fgcolor = term::Color::Default,
1380                    [n] if 30 <= *n && *n < 38 => match (*n - 30).try_into() {
1381                        Ok(i) => self.cursor_attrs.fgcolor = term::Color::Idx(i),
1382                        Err(e) => warn!(self.logger, "out of bounds fgcolor idx (1): {:?}", e),
1383                    }
1384                    [n] if 90 <= *n && *n < 98 => match (*n - 82).try_into() {
1385                        Ok(i) => self.cursor_attrs.fgcolor = term::Color::Idx(i),
1386                        Err(e) => warn!(self.logger, "out of bounds fgcolor idx (2): {:?}", e),
1387                    }
1388                    param if !param.is_empty() && param[0] == 38 => {
1389                        match parse_extended_color(param, &mut params_iter) {
1390                            Some(color) => self.cursor_attrs.fgcolor = color,
1391                            None => warn!(self.logger, "unhandled incomplete 'CSI 38 ... m'"),
1392                        }
1393                    }
1394
1395                    _ => warn!(self.logger, "unhandled 'CSI {:?} m'", param),
1396                }
1397            }
1398            'p' => match intermediates {
1399                // DECSTR (DEC Soft Terminal Reset)
1400                [b'!'] => {
1401                    self.tabstops.fill(false);
1402                    let width = self.screen().size.width;
1403                    self.fill_tabstops(0, width);
1404                    self.cursor_style = term::CursorStyle::Default;
1405                    self.cursor_attrs = term::Attrs::default();
1406                    self.cursor_blinking = None;
1407                    self.insert_mode = false;
1408
1409                    warn!(self.logger, "DECSTR only partially handled");
1410                }
1411                // DECRQM (DEC Request Mode Private)
1412                [b'?', b'$'] => {
1413                    // TODO(#4): actuate query state machine.
1414                    //
1415                    // In the future, we'll want to expose an API that
1416                    // allows the embedding application to stream the
1417                    // response of the underlying terminal so that we
1418                    // can sniff its response and figure out what capabilities
1419                    // it supports. This is the key to handling kitty's
1420                    // im-such-a-special-boy escape sequences for example
1421                    // (half the reason to write this crate), but for the
1422                    // moment we just suppress the warning log and convert
1423                    // to a debug log.
1424                    debug!(self.logger, "ignoring DECRQM query: params={:?}", params.iter().collect::<Vec<_>>());
1425                }
1426                _ => warn!(
1427                    self.logger,
1428                    "Unhandled CSI p command: CSI {:?} {:?} p",
1429                    intermediates,
1430                    params.iter().collect::<Vec<&[u16]>>()
1431                ),
1432            },
1433            // DECSCUSR (Set Cursor Style / Shape)
1434            'q' if intermediates == [b' '] => {
1435                let code = param_or(&mut params_iter, 0) as usize;
1436                match term::CursorStyle::try_from(code) {
1437                    Ok(style) => self.cursor_style = style,
1438                    Err(e) => warn!(self.logger, "parsing cursor style: {:?}", e),
1439                }
1440            },
1441            // DECSTBM (Set Scroll Region)
1442            'r' => {
1443                let top = maybe_param(&mut params_iter);
1444                let bottom = maybe_param(&mut params_iter);
1445
1446                let screen = self.screen_mut();
1447                screen.set_scroll_region(match (top, bottom) {
1448                    (None, None) => term::ScrollRegion::TrackSize,
1449                    (Some(t), None) => term::ScrollRegion::Window {
1450                        top: t.saturating_sub(1) as usize,
1451                        bottom: screen.size.height,
1452                    },
1453                    (None, Some(b)) => term::ScrollRegion::Window {
1454                        top: 0,
1455                        bottom: b as usize,
1456                    },
1457                    (Some(t), Some(b)) => term::ScrollRegion::Window {
1458                        top: t.saturating_sub(1) as usize,
1459                        bottom: b as usize,
1460                    }
1461                });
1462            }
1463
1464            _ => {
1465                warn!(self.logger, "unhandled action {}", action);
1466            }
1467        }
1468    }
1469
1470    fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) {
1471        if ignore {
1472            warn!(self.logger, "malformed ESC seq");
1473            return;
1474        }
1475        trace!(self.logger, "esc_dispatch: {}", byte);
1476        self.last_print_char = None;
1477
1478        match (intermediates, byte) {
1479            // save cursor (ESC 7)
1480            ([], b'7') => {
1481                let attrs = self.cursor_attrs.clone();
1482                let screen = self.screen_mut();
1483                let pos = screen.cursor.clone();
1484                screen.saved_cursor = SavedCursor { pos, attrs };
1485            }
1486            // restore cursor (ESC 8)
1487            ([], b'8') => {
1488                let screen = self.screen_mut();
1489                screen.cursor = screen.saved_cursor.pos;
1490                self.cursor_attrs = screen.saved_cursor.attrs.clone();
1491            }
1492            // HTS (Horizontal Tabluation Set, ESC H)
1493            ([], b'H') => {
1494                let col = self.screen().cursor.col;
1495                self.tabstops.set(col, true);
1496            }
1497            // RI (Reverse Index)
1498            ([], b'M') => {
1499                let screen = self.screen_mut();
1500                let (scroll_top, _) =
1501                    screen.scroll_region(false).as_region(&screen.size).row_bounds();
1502
1503                if screen.cursor.row == scroll_top {
1504                    screen.insert_lines(1);
1505                } else if screen.cursor.row > 0 {
1506                    screen.cursor.row -= 1;
1507                }
1508            }
1509            // RIS (Reset to Initial State)
1510            ([], b'c') => {
1511                self.tabstops.fill(false);
1512                let width = self.screen().size.width;
1513                self.fill_tabstops(0, width);
1514                self.cursor_style = term::CursorStyle::Default;
1515                self.cursor_attrs = term::Attrs::default();
1516                self.cursor_blinking = None;
1517                self.insert_mode = false;
1518
1519                warn!(self.logger, "RIS only partially handled");
1520            }
1521
1522            // DECKPAM / DECKPNM (application and numeric keypad mode)
1523            ([], b'=') => self.application_keypad_mode_enabled = true,
1524            ([], b'>') => self.application_keypad_mode_enabled = false,
1525
1526            // Designates US-ASCII or UK-ASCII as a G0-G3 character set. We handle
1527            // utf-8, which is a superset of ascii, so this is a no-op.
1528            ([b'(' | b')' | b'*' | b'+'], b'B' | b'A') => {}
1529
1530            // OSC terminators that get sent to the esc handler as well,
1531            // we can ignore them.
1532            ([], 92) => {}
1533
1534            _ => warn!(self.logger, "unhandled ESC seq ({:?}, {})", intermediates, byte),
1535        }
1536    }
1537
1538    fn terminated(&self) -> bool {
1539        false
1540    }
1541}
1542
1543fn param_or<'params>(params: &mut vte::ParamsIter<'params>, default: u16) -> u16 {
1544    maybe_param(params).unwrap_or(default)
1545}
1546
1547fn maybe_param<'params>(params: &mut vte::ParamsIter<'params>) -> Option<u16> {
1548    match params.next() {
1549        Some([0]) => None,
1550        Some([p]) => Some(*p),
1551        _ => None,
1552    }
1553}
1554
1555fn parse_extended_color<'params>(
1556    first_param: &[u16],
1557    params_iter: &mut vte::ParamsIter<'params>,
1558) -> Option<term::Color> {
1559    if first_param.len() > 1 {
1560        // Colon-delimited subparameters: e.g. [58, 2, r, g, b] or [58, 2,
1561        // space_id, r, g, b]
1562        match first_param[1] {
1563            5 => {
1564                if first_param.len() >= 3 {
1565                    let idx = first_param[2].try_into().ok()?;
1566                    Some(term::Color::Idx(idx))
1567                } else {
1568                    None
1569                }
1570            }
1571            2 => {
1572                if first_param.len() == 5 {
1573                    let r = first_param[2].try_into().ok()?;
1574                    let g = first_param[3].try_into().ok()?;
1575                    let b = first_param[4].try_into().ok()?;
1576                    Some(term::Color::Rgb(r, g, b))
1577                } else if first_param.len() >= 6 {
1578                    // Includes color space ID (e.g. 58:2:0:r:g:b or
1579                    // 58:2::r:g:b)
1580                    let r = first_param[3].try_into().ok()?;
1581                    let g = first_param[4].try_into().ok()?;
1582                    let b = first_param[5].try_into().ok()?;
1583                    Some(term::Color::Rgb(r, g, b))
1584                } else {
1585                    None
1586                }
1587            }
1588            _ => None,
1589        }
1590    } else {
1591        // Semicolon-delimited parameters: e.g. [58], [2], [r], [g], [b]
1592        match params_iter.next() {
1593            Some([5]) => {
1594                let n = param_or(params_iter, 0);
1595                let idx = n.try_into().ok()?;
1596                Some(term::Color::Idx(idx))
1597            }
1598            Some([2]) => {
1599                let r = param_or(params_iter, 0);
1600                let g = param_or(params_iter, 0);
1601                let b = param_or(params_iter, 0);
1602                let r = r.try_into().ok()?;
1603                let g = g.try_into().ok()?;
1604                let b = b.try_into().ok()?;
1605                Some(term::Color::Rgb(r, g, b))
1606            }
1607            _ => None,
1608        }
1609    }
1610}
1611
1612const NONE_VEC: Option<Vec<u8>> = None;