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        UnderlineStyle,
23    },
24};
25
26use smallvec::SmallVec;
27use tracing::{debug, warn, trace};
28
29mod altscreen;
30mod cell;
31mod line;
32mod screen;
33mod scrollback;
34
35#[cfg(not(feature = "internal-test"))]
36mod term;
37
38#[cfg(feature = "internal-test")]
39pub mod term;
40
41/// A representation of a terminal.
42pub struct Term {
43    parser: vte::Parser,
44    state: State,
45}
46
47impl Term {
48    /// Create a new terminal with the given width and height.
49    ///
50    /// Note that width will only be used when generated output
51    /// to determine where wrapping should be place.
52    ///
53    /// scrollback_lines must be at least size.height. If it is
54    /// less than size.height, it will be automatically adjusted
55    /// to be equal to size.height.
56    pub fn new(scrollback_lines: usize, size: Size) -> Self {
57        Term { parser: vte::Parser::new(), state: State::new(scrollback_lines, size) }
58    }
59
60    /// Get the current terminal size.
61    pub fn size(&self) -> Size {
62        self.state.screen().size
63    }
64
65    /// Set the terminal size.
66    ///
67    /// This will implicitly size up the scrollback_lines if
68    /// it is currently less than size.height.
69    pub fn resize(&mut self, size: Size) {
70        if size.height > self.scrollback_lines() {
71            self.set_scrollback_lines(size.height);
72        }
73
74        self.state.scrollback.resize(size);
75        self.state.altscreen.resize(size);
76    }
77
78    /// Get the current number of lines of stored scrollback.
79    pub fn scrollback_lines(&self) -> usize {
80        self.state.scrollback.scrollback_lines().expect("scrollback screen to have lines")
81    }
82
83    /// Set the number of lines of scrollback to store. This will drop
84    /// data when resizing down. When resizing up, no new memory is allocated,
85    /// capacity is simply expanded.
86    ///
87    /// If the given value is less than size().height, it will be overridden
88    /// to match the current height. You cannot store less scrollback than
89    /// there are lines in the visible screen region.
90    pub fn set_scrollback_lines(&mut self, scrollback_lines: usize) {
91        self.state.scrollback.set_scrollback_lines(scrollback_lines);
92    }
93
94    /// Process the given chunk of input. This should be the data read off
95    /// a pty running a shell.
96    pub fn process(&mut self, buf: &[u8]) {
97        self.parser.advance(&mut self.state, buf);
98    }
99
100    /// Get the current contents of the terminal encoded via terminal
101    /// escape sequences. The contents buffer will be prefixed with
102    /// a reset code, so inputing this to any terminal emulator will
103    /// reset the emulator to the contents of this Term instance.
104    pub fn contents(&self, dump_region: ContentRegion) -> Vec<u8> {
105        let mut buf = vec![];
106        term::control_codes().clear_attrs.term_input_into(&mut buf);
107        term::ControlCodes::cursor_position(1, 1).term_input_into(&mut buf);
108        term::control_codes().clear_screen.term_input_into(&mut buf);
109        self.state.dump_contents_into(&mut buf, dump_region);
110
111        buf
112    }
113}
114
115/// A section of the screen to dump.
116#[derive(Debug, Eq, PartialEq, Clone)]
117pub enum ContentRegion {
118    /// The whole terminal state, including all scrollback data.
119    All,
120    /// Only the visible lines.
121    Screen,
122    /// The bottom N lines, including (N - height) lines of scrollback.
123    BottomLines(usize),
124}
125
126impl std::fmt::Display for Term {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        self.state.fmt(f)
129    }
130}
131
132/// The size of the terminal.
133#[derive(Debug, Clone, Copy, Eq, PartialEq)]
134pub struct Size {
135    pub width: usize,
136    pub height: usize,
137}
138
139/// The complete terminal state. An internal implementation detail.
140struct State {
141    /// The state for the normal terminal screen.
142    scrollback: Screen,
143    /// The state for the alternate screen.
144    altscreen: Screen,
145    /// The currently active screen mode.
146    screen_mode: ScreenMode,
147    /// The current cursor attrs. These are shared between the scrollback
148    /// and alt screens, which is why they are stored here rather than
149    /// with the curors themsevles.
150    cursor_attrs: term::Attrs,
151    /// The terminal title, as set by `OSC 0` and `OSC 2`.
152    title: Option<SmallVec<[u8; 8]>>,
153    /// The terminal icon name, as set by `OSC 0` and `OSC 1`.
154    icon_name: Option<SmallVec<[u8; 8]>>,
155    /// The terminal working directory (some terminal emulators use this
156    /// to know what directory to start new shells in).
157    working_dir: Option<WorkingDir>,
158    /// A table mapping color index to a particular color spec.
159    /// This is set by OSC 4. We use a tree for deterministic output
160    /// to make testing easier. A hash would work just as well.
161    palette_overrides: BTreeMap<usize, Vec<u8>>,
162    /// Color overrides for things like foreground and background.
163    /// These slots extend from OSC 10 to OSC 19.
164    functional_colors: [Option<Vec<u8>>; 10],
165    /// Tracks if the cursor is currently hidden. Controlled
166    /// via the `CSI ? 25 {h,l}` codes.
167    cursor_hidden: bool,
168    /// Tracks application keypad mode state. Controlled via
169    /// `CSI ? 1 {h,l}`.
170    application_keypad_mode_enabled: bool,
171    /// Tracks paste mode. Controlled via `CSI ? 2004 {h,l}`.
172    in_paste_mode: bool,
173}
174
175struct WorkingDir {
176    host: SmallVec<[u8; 8]>,
177    dir: SmallVec<[u8; 8]>,
178}
179
180impl std::fmt::Display for State {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        match self.screen_mode {
183            ScreenMode::Scrollback => {
184                writeln!(f, "Screen Mode: Scrollback")?;
185                write!(f, "{}", self.scrollback)?;
186            }
187            ScreenMode::Alt => {
188                writeln!(f, "Screen Mode: AltScreen")?;
189                write!(f, "{}", self.altscreen)?;
190            }
191        }
192
193        Ok(())
194    }
195}
196
197impl State {
198    fn new(scrollback_lines: usize, size: Size) -> Self {
199        State {
200            scrollback: Screen::scrollback(scrollback_lines, size),
201            altscreen: Screen::alt(size),
202            screen_mode: ScreenMode::Scrollback,
203            cursor_attrs: term::Attrs::default(),
204            title: None,
205            icon_name: None,
206            working_dir: None,
207            palette_overrides: BTreeMap::new(),
208            functional_colors: [NONE_VEC; 10],
209            cursor_hidden: false,
210            application_keypad_mode_enabled: false,
211            in_paste_mode: false,
212        }
213    }
214
215    fn screen_mut(&mut self) -> &mut Screen {
216        match self.screen_mode {
217            ScreenMode::Scrollback => &mut self.scrollback,
218            ScreenMode::Alt => &mut self.altscreen,
219        }
220    }
221
222    fn screen(&self) -> &Screen {
223        match self.screen_mode {
224            ScreenMode::Scrollback => &self.scrollback,
225            ScreenMode::Alt => &self.altscreen,
226        }
227    }
228
229    fn dump_contents_into(&self, buf: &mut Vec<u8>, dump_region: ContentRegion) {
230        match self.screen_mode {
231            ScreenMode::Scrollback => self.scrollback.dump_contents_into(buf, dump_region),
232            ScreenMode::Alt => self.altscreen.dump_contents_into(buf, dump_region),
233        }
234
235        let controls = term::control_codes();
236
237        // restore cursor attributes (the screen will have already restored our
238        // position).
239        controls.clear_attrs.term_input_into(buf);
240        let codes = term::Attrs::default().transition_to(&self.cursor_attrs);
241        for c in codes.into_iter() {
242            c.term_input_into(buf);
243        }
244
245        // Restore the title / icon name. Most terminals treat theses as the
246        // same thing these days, but we'll go the extra mile and differentiate
247        // rather than just always sending `OSC 0 ; <title> ST` in case there is
248        // a terminal that actually makes a distinction.
249        match (&self.title, &self.icon_name) {
250            (Some(title), Some(icon_name)) if title == icon_name => {
251                ControlCodes::set_title_and_icon_name(title.clone()).term_input_into(buf)
252            }
253            (Some(title), Some(icon_name)) => {
254                ControlCodes::set_title(title.clone()).term_input_into(buf);
255                ControlCodes::set_icon_name(icon_name.clone()).term_input_into(buf);
256            }
257            (Some(title), None) => {
258                ControlCodes::set_title(title.clone()).term_input_into(buf);
259            }
260            (None, Some(icon_name)) => {
261                ControlCodes::set_icon_name(icon_name.clone()).term_input_into(buf);
262            }
263            (None, None) => {}
264        }
265
266        if let Some(working_dir) = &self.working_dir {
267            ControlCodes::set_working_dir(working_dir.host.clone(), working_dir.dir.clone())
268                .term_input_into(buf);
269        }
270
271        if !self.palette_overrides.is_empty() {
272            ControlCodes::set_color_indices(
273                self.palette_overrides
274                    .iter()
275                    .map(|(idx, color_spec)| (*idx, SmallVec::from(color_spec.as_slice()))),
276            )
277            .term_input_into(buf);
278        }
279
280        if self.cursor_hidden {
281            controls.hide_cursor.term_input_into(buf);
282        }
283        if self.application_keypad_mode_enabled {
284            controls.enable_application_keypad_mode.term_input_into(buf);
285        }
286        if self.in_paste_mode {
287            controls.enable_paste_mode.term_input_into(buf);
288        }
289
290        // Generate fused functional color commands from any runs in the
291        // functional colors table.
292        let mut functional_color_idx = 0;
293        while functional_color_idx < self.functional_colors.len() {
294            if let Some(color_spec) = &self.functional_colors[functional_color_idx] {
295                let start_idx = functional_color_idx;
296                let mut color_specs = vec![color_spec.as_slice()];
297
298                functional_color_idx += 1;
299                while functional_color_idx < self.functional_colors.len() {
300                    if let Some(s) = &self.functional_colors[functional_color_idx] {
301                        color_specs.push(s.as_slice());
302                    } else {
303                        break;
304                    }
305                    functional_color_idx += 1;
306                }
307
308                ControlCodes::set_functional_color(start_idx, color_specs).term_input_into(buf);
309            }
310
311            functional_color_idx += 1;
312        }
313    }
314
315    /// Set a run within the functional colors table starting at the given
316    /// index. This implements OSC 10 through OSC 19.
317    fn set_functional_color<'a, I>(&mut self, mut idx: usize, mut params_iter: I)
318    where
319        I: Iterator<Item = &'a &'a [u8]>,
320    {
321        while let Some(color_spec) = params_iter.next() {
322            if idx >= self.functional_colors.len() {
323                return;
324            }
325
326            if *color_spec != [b'?'] {
327                self.functional_colors[idx] = Some(Vec::from(*color_spec));
328            }
329
330            idx += 1;
331        }
332    }
333}
334
335/// Indicates which screen mode is active.
336enum ScreenMode {
337    Scrollback,
338    Alt,
339}
340
341impl vte::Perform for State {
342    fn print(&mut self, c: char) {
343        let attrs = self.cursor_attrs.clone();
344        let screen = self.screen_mut();
345        screen.snap_to_bottom();
346        if let Err(e) = screen.write_at_cursor(Cell::new(c, attrs)) {
347            warn!("writing char at cursor: {e:?}");
348        }
349    }
350
351    fn execute(&mut self, byte: u8) {
352        match byte {
353            b'\n' => self.screen_mut().cursor.row += 1,
354            b'\r' => self.screen_mut().cursor.col = 0,
355            _ => {
356                warn!("execute: unhandled byte {}", byte);
357            }
358        }
359    }
360
361    fn hook(&mut self, _params: &vte::Params, intermediates: &[u8], ignore: bool, action: char) {
362        debug!("unhandled hook{}: {intermediates:?} {action}",
363            if ignore { " (ignored)" } else { "" });
364    }
365
366    fn put(&mut self, byte: u8) {
367        trace!("unhandled put: {byte}");
368    }
369
370    fn unhook(&mut self) {
371        debug!("unhandled unhook");
372    }
373
374    // OSC commands are of the form
375    // `OSC <p1> ; <p2> ... <pn> <terminator>` where
376    // `OSC` is always `ESC]`, the params are byte sequences seperated by
377    // semicolons, and the terminator is either `BEL` (0x7) or
378    // `ST` (`ESC\`, 0x1b 0x5c). Modern applications use ST for the most
379    // part, but some older applications will send BEL. We should be able
380    // to just ignore the _bell_terminated flag and treat commands the
381    // same regardless of the terminator they have.
382    #[rustfmt::skip]
383    fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
384        let mut params_iter = params.iter();
385        match params_iter.next() {
386            // Title manipulation
387            Some([b'0']) => if let Some(title) = params_iter.next() {
388                self.title = Some(title.to_vec().into());
389                self.icon_name = Some(title.to_vec().into());
390            } else {
391                warn!("OSC 0 with no title param");
392            },
393            Some([b'1']) => if let Some(icon_name) = params_iter.next() {
394                self.icon_name = Some(icon_name.to_vec().into());
395            } else {
396                warn!("OSC 1 with no icon_name param");
397            },
398            Some([b'2']) => if let Some(title) = params_iter.next() {
399                self.title = Some(title.to_vec().into());
400            } else {
401                warn!("OSC 2 with no title param");
402            },
403
404            // Color Palette
405            Some([b'4']) => while let (Some(idx), Some(color_spec)) = (params_iter.next(), params_iter.next()) {
406                if *color_spec == [b'?'] {
407                    // If the program is querying for a color, we just ignore
408                    // that control code. The real terminal is responsible for
409                    // responding.
410                    continue;
411                }
412
413                match std::str::from_utf8(idx) {
414                    Ok(s) => match s.parse::<usize>() {
415                        Ok(i) => {
416                            self.palette_overrides.insert(i, color_spec.to_vec());
417                        },
418                        Err(e) => warn!("OSC 4: idx is an invalid number '{s}': {e}"),
419                    },
420                    Err(e) => warn!("OSC 4: invalid idx '{idx:?}': {e}"),
421                }
422            },
423            Some([b'1', b'0', b'4']) => while let Some(idx) = params_iter.next() {
424                match std::str::from_utf8(idx) {
425                    Ok(s) => match s.parse::<usize>() {
426                        Ok(i) => {
427                            self.palette_overrides.remove(&i);
428                        },
429                        Err(e) => warn!("OSC 104: idx is an invalid number '{s}': {e}"),
430                    },
431                    Err(e) => warn!("OSC 104: invalid idx '{idx:?}': {e}"),
432                }
433            },
434
435            // Working dir
436            Some([b'7']) => if let (Some(host), Some(dir)) = (params_iter.next(), params_iter.next()) {
437                self.working_dir = Some(WorkingDir {
438                    host: host.to_vec().into(),
439                    dir: dir.to_vec().into(),
440                });
441            } else {
442                warn!("OSC 7 with fewer than 2 params");
443            },
444
445            // Links. Depending on params, OSC 8 both starts and ends links.
446            Some([b'8']) => if let (Some(params), Some(url)) = (params_iter.next(), params_iter.next()) {
447                if params.is_empty() && url.is_empty() {
448                    self.cursor_attrs.link_target = None;
449                } else {
450                    self.cursor_attrs.link_target = Some(LinkTarget {
451                        params: SmallVec::from_slice(params),
452                        url: SmallVec::from_slice(url),
453                    });
454                }
455            } else {
456                self.cursor_attrs.link_target = None;
457            },
458
459            // Functional colors (foreground, background and whatnot).
460            Some([b'1', x]) if b'0' <= *x && *x <= b'9' =>
461                self.set_functional_color((*x - b'0') as usize, params_iter),
462
463            Some([b'5', b'2']) => debug!("ignoring OSC 52 (clipboard)"),
464            Some([b'9']) => debug!("ignoring OSC 9 (desktop notification)"),
465            Some([b'7', b'7', b'7']) => debug!("ignoring OSC 777"),
466            Some([b'1', b'3', b'3']) => debug!("ignoring OSC 133 (iterm2 marks)"),
467
468            _ => warn!("unhandled 'OSC {:?} {}'", params, if bell_terminated {
469                "BEL"
470            } else {
471                "ST"
472            }),
473        }
474    }
475
476    // Handle escape codes beginning with the CSI indicator ('\x1b[').
477    //
478    // rustfmt has insane ideas about match arm formatting and there is
479    // apparently no way to make it do the reasonable thing of preserving
480    // horizontal whitespace by placing loops directly in match arm statement
481    // position.
482    #[rustfmt::skip]
483    fn csi_dispatch(
484        &mut self,
485        params: &vte::Params,
486        intermediates: &[u8],
487        ignore: bool,
488        action: char,
489    ) {
490        if ignore {
491            warn!("malformed CSI seq");
492            return;
493        }
494
495        let mut params_iter = params.iter();
496
497        match action {
498            // CUU (Cursor Up)
499            'A' => {
500                let n = param_or(&mut params_iter, 1) as usize;
501                let screen = self.screen_mut();
502                screen.cursor.row = screen.cursor.row.saturating_sub(n);
503                screen.clamp();
504            }
505            // CUD (Cursor Down)
506            'B' => {
507                let n = param_or(&mut params_iter, 1) as usize;
508                let screen = self.screen_mut();
509                screen.cursor.row += n;
510                screen.clamp();
511            }
512            // CUF (Cursor Forward)
513            'C' => {
514                let n = param_or(&mut params_iter, 1) as usize;
515                let screen = self.screen_mut();
516                screen.cursor.col += n;
517                screen.clamp();
518            }
519            // CUF (Cursor Backwards)
520            'D' => {
521                let n = param_or(&mut params_iter, 1) as usize;
522                let screen = self.screen_mut();
523                screen.cursor.col = screen.cursor.col.saturating_sub(n);
524                screen.clamp();
525            }
526            // CNL (Cursor Next Line)
527            'E' => {
528                let n = param_or(&mut params_iter, 1) as usize;
529                let screen = self.screen_mut();
530                screen.cursor.row += n;
531                screen.cursor.col = 0;
532                screen.clamp();
533            }
534            // CPL (Cursor Prev Line)
535            'F' => {
536                let n = param_or(&mut params_iter, 1) as usize;
537                let screen = self.screen_mut();
538                screen.cursor.row = screen.cursor.row.saturating_sub(n);
539                screen.cursor.col = 0;
540                screen.clamp();
541            }
542            // CHA (Cursor Horizontal Absolute)
543            'G' => {
544                let n = param_or(&mut params_iter, 1) as usize;
545                let n = n.saturating_sub(1); // translate to 0 indexing
546
547                let screen = self.screen_mut();
548                screen.cursor.col = n;
549                screen.clamp();
550            }
551            // CUP (Cursor Set Position)
552            'H' => {
553                // parse the params and adjust 1 indexing to 0 indexing
554                let row = param_or(&mut params_iter, 1) as usize;
555                let col = param_or(&mut params_iter, 1) as usize;
556                let screen = self.screen_mut();
557                screen.set_cursor(term::Pos { row, col });
558                screen.clamp();
559            }
560            // ED (Erase in Display)
561            'J' => while let Some(code) = params_iter.next() {
562                match code {
563                    [] | [0] => self.screen_mut().erase_to_end(),
564                    [1] => self.screen_mut().erase_from_start(),
565                    [2] => self.screen_mut().erase(false),
566                    [3] => self.screen_mut().erase(true),
567                    _ => warn!("unhandled 'CSI {code:?} J'"),
568                }
569            }
570            // EL (Erase in Line)
571            'K' => while let Some(code) = params_iter.next() {
572                match code {
573                    [] | [0] => {
574                        let screen = self.screen_mut();
575                        let col = screen.cursor.col;
576                        if let Some(l) = screen.get_line_mut() {
577                            l.erase(line::Section::ToEnd(col));
578                        }
579                    }
580                    [1] => {
581                        let screen = self.screen_mut();
582                        let col = screen.cursor.col;
583                        if let Some(l) = screen.get_line_mut() {
584                            l.erase(line::Section::StartTo(col));
585                        }
586                    }
587                    [2] => if let Some(l) = self.screen_mut().get_line_mut() {
588                        l.erase(line::Section::Whole);
589                    }
590                    _ => warn!("unhandled 'CSI {code:?} K'"),
591                }
592            }
593            // IL (Insert Line)
594            'L' => {
595                let n = param_or(&mut params_iter, 1) as usize;
596                self.screen_mut().insert_lines(n);
597            }
598            // DL (Delete Line)
599            'M' => {
600                let n = param_or(&mut params_iter, 1) as usize;
601                self.screen_mut().delete_lines(n);
602            }
603            // SU (Scroll Up)
604            'S' => {
605                let n = param_or(&mut params_iter, 1) as usize;
606                self.screen_mut().scroll_up(n as usize);
607            }
608            // SD (Scroll Down)
609            'T' => {
610                let n = param_or(&mut params_iter, 1) as usize;
611                self.screen_mut().scroll_down(n as usize);
612            }
613
614            // ICH (Insert Character)
615            '@' => {
616                let n = param_or(&mut params_iter, 1) as usize;
617
618                let screen = self.screen_mut();
619                let width = screen.size.width;
620                let col = screen.cursor.col;
621                if let Some(l) = screen.get_line_mut() {
622                    l.insert_character(width, col, n);
623                }
624            }
625            // DCH (Delete Character)
626            'P' => {
627                let n = param_or(&mut params_iter, 1) as usize;
628
629                let attrs = self.cursor_attrs.clone();
630
631                let screen = self.screen_mut();
632                let width = screen.size.width;
633                let col = screen.cursor.col;
634                if let Some(l) = screen.get_line_mut() {
635                    l.delete_character(width, col, &attrs, n);
636                }
637            }
638
639            // SCP (Save Cursor Position)
640            's' => {
641                let screen = self.screen_mut();
642                let cursor = screen.cursor.clone();
643                screen.saved_cursor.pos = cursor;
644            }
645            // RCP (Restore Cursor Position)
646            'u' => {
647                let screen = self.screen_mut();
648                screen.cursor = screen.saved_cursor.pos;
649                screen.clamp();
650            }
651
652            'h' => match intermediates {
653                [b'?'] => while let Some(code) = params_iter.next() {
654                    match code {
655                        [1] => self.application_keypad_mode_enabled = true,
656                        [6] => self.screen_mut().set_origin_mode(OriginMode::ScrollRegion),
657                        [25] => self.cursor_hidden = false,
658                        // enable alt screen
659                        [1049] => {
660                            // The alt-screen gets reset upon entry, so we need to
661                            // clobber it here.
662                            self.altscreen = Screen::alt(self.altscreen.size);
663                            self.screen_mode = ScreenMode::Alt;
664                        }
665                        [2004] => self.in_paste_mode = true,
666
667                        _ => {
668                            warn!(
669                                "Unhandled CSI l command: CSI {:?} {:?} l",
670                                intermediates,
671                                params.iter().collect::<Vec<&[u16]>>()
672                            );
673                            return;
674                        }
675                    }
676                }
677                _ => warn!(
678                    "Unhandled CSI h command: CSI {:?} {:?} h",
679                    intermediates,
680                    params.iter().collect::<Vec<&[u16]>>()
681                ),
682            }
683            'l' => match intermediates {
684                [b'?'] => while let Some(code) = params_iter.next() {
685                    match code {
686                        [1] => self.application_keypad_mode_enabled = false,
687                        [6] => self.screen_mut().set_origin_mode(OriginMode::Term),
688                        [25] => self.cursor_hidden = true,
689                        [1049] => self.screen_mode = ScreenMode::Scrollback,
690                        [2004] => self.in_paste_mode = false,
691                        _ => {
692                            warn!(
693                                "Unhandled CSI l command: CSI {:?} {:?} l",
694                                intermediates,
695                                params.iter().collect::<Vec<&[u16]>>()
696                            );
697                            return;
698                        }
699                    }
700                }
701                _ => warn!(
702                    "Unhandled CSI l command: CSI {:?} {:?} l",
703                    intermediates,
704                    params.iter().collect::<Vec<&[u16]>>()
705                ),
706            },
707            // DSR (Device Status Report)
708            'n' => while let Some(param) = params_iter.next() {
709                match param {
710                    // TODO: We might want to store this to assert against the
711                    // terminal output stream once we start scanning that.
712                    // We'll need to implement terminal output stream scanning
713                    // in order to properly handle kitty extensions at some
714                    // point (since we need to know if the real terminal
715                    // responded with a code indicating that it supported the
716                    // extensions in order to determine how we should interpret
717                    // control codes).
718                    [6] => debug!("ignoring DSR (CSI 6 n), that's the real terminal's job"),
719                    _ => {}
720                }
721            },
722
723            // cell attribute manipulation
724            'm' => while let Some(param) = params_iter.next() {
725                match param {
726                    [] | [0] => self.cursor_attrs = term::Attrs::default(),
727
728                    // Underline Handling
729                    // TODO: there are lots of other underline styles. To fix,
730                    // we need to update attrs.
731                    //
732                    // Kitty extensions:
733                    //      CSI 4 : 3 m => curly
734                    //      CSI 4 : 2 m => double
735                    //
736                    // Other:
737                    //      CSI 58 ; 2 ; r ; g ; b m => RGB colored underline
738                    [4] => self.cursor_attrs.underline = Some(UnderlineStyle::Single),
739                    [21] => self.cursor_attrs.underline = Some(UnderlineStyle::Double),
740                    [24] => self.cursor_attrs.underline = None,
741
742                    // Font Weight Handling.
743                    [1] => self.cursor_attrs.font_weight = Some(FontWeight::Bold),
744                    [2] => self.cursor_attrs.font_weight = Some(FontWeight::Faint),
745                    [22] => self.cursor_attrs.font_weight = None,
746
747                    // Italic Handling.
748                    [3] => self.cursor_attrs.italic = true,
749                    [23] => self.cursor_attrs.italic = false,
750
751                    // Inverse Handling.
752                    [7] => self.cursor_attrs.inverse = true,
753                    [27] => self.cursor_attrs.inverse = false,
754
755                    // Blink Handling
756                    [5] => self.cursor_attrs.blink = Some(BlinkStyle::Slow),
757                    [6] => self.cursor_attrs.blink = Some(BlinkStyle::Rapid),
758                    [25] => self.cursor_attrs.blink = None,
759
760                    // Conceal Handling
761                    [8] => self.cursor_attrs.conceal = true,
762                    [28] => self.cursor_attrs.conceal = false,
763
764                    // Strikethrough Handling.
765                    [9] => self.cursor_attrs.strikethrough = true,
766                    [29] => self.cursor_attrs.strikethrough = false,
767
768                    // Frame Handling.
769                    [51] => self.cursor_attrs.framed = Some(FrameStyle::Frame),
770                    [52] => self.cursor_attrs.framed = Some(FrameStyle::Circle),
771                    [54] => self.cursor_attrs.framed = None,
772
773                    // Overline Handling.
774                    [53] => self.cursor_attrs.overline = true,
775                    [55] => self.cursor_attrs.overline = false,
776
777                    // Background Color Handling.
778                    [49] => self.cursor_attrs.bgcolor = term::Color::Default,
779                    [n] if 40 <= *n && *n < 48 => match (*n - 40).try_into() {
780                        Ok(i) => self.cursor_attrs.bgcolor = term::Color::Idx(i),
781                        Err(e) => warn!("out of bounds bgcolor idx (1): {e:?}"),
782                    }
783                    [n] if 100 <= *n && *n < 108 => match (*n - 92).try_into() {
784                        Ok(i) => self.cursor_attrs.bgcolor = term::Color::Idx(i),
785                        Err(e) => warn!("out of bounds bgcolor idx (2): {e:?}"),
786                    }
787                    [48] => match params_iter.next() {
788                        Some([5]) => {
789                            let n = param_or(&mut params_iter, 0);
790                            match n.try_into() {
791                                Ok(i) => self.cursor_attrs.bgcolor = term::Color::Idx(i),
792                                Err(e) => warn!("out of bounds bgcolor idx (3): {e:?}"),
793                            }
794                        },
795                        Some([2]) => {
796                            // N.B. apparently some very old termianls have a "space id"
797                            // param before the three color params. It might make sense
798                            // to fully slurp the params here and if there are 4 provided
799                            // drop the first to avoid shifting the rgb. I'm guessing this
800                            // is so rare as to not matter though.
801                            let r = param_or(&mut params_iter, 0);
802                            let g = param_or(&mut params_iter, 0);
803                            let b = param_or(&mut params_iter, 0);
804                            if let (Ok(r), Ok(g), Ok(b)) = (r.try_into(), g.try_into(), b.try_into()) {
805                                self.cursor_attrs.bgcolor = term::Color::Rgb(r, g, b);
806                            } else {
807                                warn!("out of bounds color codes for CSI 48 2 ... m");
808                            }
809                        },
810                        _ => warn!("unhandled incomplete 'CSI 48 ... m'"),
811                    },
812
813                    // Foreground Color Handling.
814                    [39] => self.cursor_attrs.fgcolor = term::Color::Default,
815                    [n] if 30 <= *n && *n < 38 => match (*n - 30).try_into() {
816                        Ok(i) => self.cursor_attrs.fgcolor = term::Color::Idx(i),
817                        Err(e) => warn!("out of bounds fgcolor idx (1): {e:?}"),
818                    }
819                    [n] if 90 <= *n && *n < 98 => match (*n - 82).try_into() {
820                        Ok(i) => self.cursor_attrs.fgcolor = term::Color::Idx(i),
821                        Err(e) => warn!("out of bounds fgcolor idx (2): {e:?}"),
822                    }
823                    [38] => match params_iter.next() {
824                        Some([5]) => {
825
826                            let n = param_or(&mut params_iter, 0);
827                            match n.try_into() {
828                                Ok(i) => self.cursor_attrs.fgcolor = term::Color::Idx(i),
829                                Err(e) => warn!("out of bounds fgcolor idx (3): {e:?}"),
830                            }
831                        },
832                        Some([2]) => {
833                            // N.B. apparently some very old termianls have a "space id"
834                            // param before the three color params. It might make sense
835                            // to fully slurp the params here and if there are 4 provided
836                            // drop the first to avoid shifting the rgb. I'm guessing this
837                            // is so rare as to not matter though.
838                            let r = param_or(&mut params_iter, 0);
839                            let g = param_or(&mut params_iter, 0);
840                            let b = param_or(&mut params_iter, 0);
841                            if let (Ok(r), Ok(g), Ok(b)) = (r.try_into(), g.try_into(), b.try_into()) {
842                                self.cursor_attrs.fgcolor = term::Color::Rgb(r, g, b);
843                            } else {
844                                warn!("out of bounds color codes for CSI 38 2 ... m");
845                            }
846                        },
847                        _ => warn!("unhandled incomplete 'CSI 38 ... m'"),
848                    },
849
850                    _ => warn!("unhandled 'CSI {param:?} m'"),
851                }
852            }
853            // DECSTBM (Set Scroll Region)
854            'r' => {
855                let top = maybe_param(&mut params_iter);
856                let bottom = maybe_param(&mut params_iter);
857
858                let screen = self.screen_mut();
859                screen.set_scroll_region(match (top, bottom) {
860                    (None, None) => term::ScrollRegion::TrackSize,
861                    (Some(t), None) => term::ScrollRegion::Window {
862                        top: t.saturating_sub(1) as usize,
863                        bottom: screen.size.height,
864                    },
865                    (None, Some(b)) => term::ScrollRegion::Window {
866                        top: 0,
867                        bottom: b as usize,
868                    },
869                    (Some(t), Some(b)) => term::ScrollRegion::Window {
870                        top: t.saturating_sub(1) as usize,
871                        bottom: b as usize,
872                    }
873                });
874            }
875
876            _ => {
877                warn!("unhandled action {}", action);
878            }
879        }
880    }
881
882    fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) {
883        if ignore {
884            warn!("malformed ESC seq");
885            return;
886        }
887
888        match (intermediates, byte) {
889            // save cursor (ESC 7)
890            ([], b'7') => {
891                let attrs = self.cursor_attrs.clone();
892                let screen = self.screen_mut();
893                let pos = screen.cursor.clone();
894                screen.saved_cursor = SavedCursor { pos, attrs };
895            }
896            // restore cursor (ESC 8)
897            ([], b'8') => {
898                let screen = self.screen_mut();
899                screen.cursor = screen.saved_cursor.pos;
900                self.cursor_attrs = screen.saved_cursor.attrs.clone();
901            }
902
903            _ => warn!("unhandled ESC seq ({intermediates:?}, {byte})"),
904        }
905    }
906
907    fn terminated(&self) -> bool {
908        false
909    }
910}
911
912fn param_or<'params>(params: &mut vte::ParamsIter<'params>, default: u16) -> u16 {
913    maybe_param(params).unwrap_or(default)
914}
915
916fn maybe_param<'params>(params: &mut vte::ParamsIter<'params>) -> Option<u16> {
917    match params.next() {
918        Some([0]) => None,
919        Some([p]) => Some(*p),
920        _ => None,
921    }
922}
923
924const NONE_VEC: Option<Vec<u8>> = None;