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