Skip to main content

shpool_vterm/
lib.rs

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