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