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