Skip to main content

vtcode_ghostty_core/
terminal.rs

1mod edit;
2mod modes;
3mod report;
4
5use crate::cell::Cell;
6use crate::cursor::Cursor;
7use crate::mode::TerminalModes;
8use crate::parser::{CsiSequence, ParserState};
9use crate::region::Region;
10use crate::screen::{Screen, ScreenKind, default_tab_stops, plain_text_for_screen};
11use crate::style::Style;
12
13/// A pure-Rust VT terminal emulator with incremental byte processing.
14///
15/// Feed bytes via [`write`](Terminal::write), then query the screen state via
16/// accessors like [`plain_text`](Terminal::plain_text) or [`cell`](Terminal::cell).
17pub struct Terminal {
18    cols: usize,
19    rows: usize,
20    primary: Screen,
21    alternate: Screen,
22    active: ScreenKind,
23    current_style: Style,
24    state: ParserState,
25    csi_buffer: String,
26    csi_intermediate: u8,
27    osc_buffer: Vec<u8>,
28    utf8_buffer: Vec<u8>,
29    utf8_remaining: usize,
30    output: Vec<u8>,
31    clipboard: Vec<String>,
32    bell_count: usize,
33    title: Option<String>,
34    modes: TerminalModes,
35    tab_stops: Vec<bool>,
36    last_printed: Option<char>,
37    scroll_region: Region,
38    max_scrollback: usize,
39}
40
41impl Terminal {
42    /// Create a new terminal with the given dimensions.
43    pub fn new(cols: usize, rows: usize) -> Self {
44        let style = Style::default();
45        let scroll_region = Region {
46            top: 0,
47            bottom: rows.saturating_sub(1),
48        };
49        Self {
50            cols,
51            rows,
52            primary: Screen::new(cols, rows, style),
53            alternate: Screen::new(cols, rows, style),
54            active: ScreenKind::Primary,
55            current_style: style,
56            state: ParserState::Ground,
57            csi_buffer: String::new(),
58            csi_intermediate: 0,
59            osc_buffer: Vec::new(),
60            utf8_buffer: Vec::new(),
61            utf8_remaining: 0,
62            output: Vec::new(),
63            clipboard: Vec::new(),
64            bell_count: 0,
65            title: None,
66            modes: TerminalModes::default(),
67            tab_stops: default_tab_stops(cols),
68            last_printed: None,
69            scroll_region,
70            max_scrollback: 1000,
71        }
72    }
73
74    /// Set the maximum number of scrollback lines.
75    pub fn set_max_scrollback(&mut self, max: usize) {
76        self.max_scrollback = max;
77    }
78
79    /// Process a byte stream incrementally.
80    pub fn write(&mut self, bytes: &[u8]) {
81        for &byte in bytes {
82            self.advance(byte);
83        }
84    }
85
86    /// Take accumulated response bytes (DSR replies, DA responses).
87    pub fn take_output(&mut self) -> Vec<u8> {
88        std::mem::take(&mut self.output)
89    }
90
91    /// Take accumulated clipboard writes.
92    pub fn take_clipboard(&mut self) -> Vec<String> {
93        std::mem::take(&mut self.clipboard)
94    }
95
96    /// Resize the terminal.
97    pub fn resize(&mut self, cols: usize, rows: usize) {
98        if cols == self.cols && rows == self.rows {
99            return;
100        }
101        let style = self.current_style;
102        self.primary.resize(self.cols, self.rows, cols, rows, style);
103        self.alternate
104            .resize(self.cols, self.rows, cols, rows, style);
105        self.cols = cols;
106        self.rows = rows;
107        self.scroll_region = Region {
108            top: 0,
109            bottom: rows.saturating_sub(1),
110        };
111        self.tab_stops = default_tab_stops(cols);
112    }
113
114    // -- Accessors --
115
116    pub fn cols(&self) -> usize {
117        self.cols
118    }
119
120    pub fn rows(&self) -> usize {
121        self.rows
122    }
123
124    pub fn active_screen(&self) -> ScreenKind {
125        self.active
126    }
127
128    pub fn cursor(&self) -> Cursor {
129        self.screen().cursor
130    }
131
132    pub fn current_style(&self) -> &Style {
133        &self.current_style
134    }
135
136    pub fn bell_count(&self) -> usize {
137        self.bell_count
138    }
139
140    pub fn title(&self) -> Option<&str> {
141        self.title.as_deref()
142    }
143
144    /// Get the cell at the given position.
145    pub fn cell(&self, col: usize, row: usize) -> Option<&Cell> {
146        if col >= self.cols || row >= self.rows {
147            return None;
148        }
149        let screen = self.screen();
150        let idx = Screen::index(self.cols, col, row);
151        screen.grid.get(idx)
152    }
153
154    /// Get the full grid as a flat slice.
155    pub fn grid(&self) -> &[Cell] {
156        &self.screen().grid
157    }
158
159    /// Number of scrollback lines available.
160    pub fn scrollback_len(&self) -> usize {
161        self.screen().scrollback.len()
162    }
163
164    /// Get a scrollback row by index (0 = oldest), trimmed of trailing blanks.
165    pub fn scrollback_row(&self, index: usize) -> Option<String> {
166        self.screen().scrollback.get(index).map(|row| {
167            let end = row
168                .iter()
169                .rposition(|cell| !cell.is_blank())
170                .map_or(0, |idx| idx + 1);
171            let mut out = String::new();
172            for cell in &row[..end] {
173                if !cell.is_wide_continuation() {
174                    out.push(cell.ch());
175                }
176            }
177            out
178        })
179    }
180
181    /// Extract plain text from the visible screen.
182    pub fn plain_text(&self) -> String {
183        plain_text_for_screen(self.screen(), self.cols, self.rows)
184    }
185
186    /// Extract plain text including scrollback.
187    pub fn screen_dump(&self) -> String {
188        let screen = self.screen();
189        let mut out = String::new();
190
191        for row in &screen.scrollback {
192            if !out.is_empty() {
193                out.push('\n');
194            }
195            for cell in row {
196                if !cell.is_wide_continuation() {
197                    out.push(cell.ch());
198                }
199            }
200        }
201
202        let visible = plain_text_for_screen(screen, self.cols, self.rows);
203        if !visible.is_empty() {
204            if !out.is_empty() {
205                out.push('\n');
206            }
207            out.push_str(&visible);
208        }
209
210        out
211    }
212
213    // -- Mode accessors --
214
215    pub fn wraparound(&self) -> bool {
216        self.modes.wraparound
217    }
218    pub fn cursor_visible(&self) -> bool {
219        self.modes.cursor_visible
220    }
221    pub fn cursor_shape(&self) -> crate::mode::CursorShape {
222        self.modes.cursor_shape
223    }
224    pub fn application_cursor_keys(&self) -> bool {
225        self.modes.application_cursor_keys
226    }
227    pub fn bracketed_paste(&self) -> bool {
228        self.modes.bracketed_paste
229    }
230    pub fn focus_reporting(&self) -> bool {
231        self.modes.focus_reporting
232    }
233    pub fn mouse_tracking(&self) -> Option<crate::mode::MouseTracking> {
234        self.modes.mouse_tracking
235    }
236    pub fn sgr_mouse(&self) -> bool {
237        self.modes.sgr_mouse
238    }
239    pub fn scroll_region(&self) -> (usize, usize) {
240        (self.scroll_region.top, self.scroll_region.bottom)
241    }
242
243    // -- Internal --
244
245    fn screen(&self) -> &Screen {
246        match self.active {
247            ScreenKind::Primary => &self.primary,
248            ScreenKind::Alternate => &self.alternate,
249        }
250    }
251
252    fn screen_mut(&mut self) -> &mut Screen {
253        match self.active {
254            ScreenKind::Primary => &mut self.primary,
255            ScreenKind::Alternate => &mut self.alternate,
256        }
257    }
258
259    fn advance(&mut self, byte: u8) {
260        match self.state {
261            ParserState::Ground => self.advance_ground(byte),
262            ParserState::Escape => self.advance_escape(byte),
263            ParserState::Csi => self.advance_csi(byte),
264            ParserState::Osc => self.advance_osc(byte),
265        }
266    }
267
268    fn advance_ground(&mut self, byte: u8) {
269        match byte {
270            // C0 controls
271            0x07 => self.bell_count += 1,
272            0x08 => self.backspace(),
273            0x09 => self.horizontal_tab(),
274            0x0A | 0x0B | 0x0C => self.linefeed(),
275            0x0D => self.carriage_return(),
276            0x1B => {
277                self.state = ParserState::Escape;
278            }
279            // Printable ASCII
280            0x20..=0x7E => {
281                let ch = byte as char;
282                self.print_char(ch);
283            }
284            // UTF-8 multi-byte sequences
285            0xC0..=0xDF => {
286                self.utf8_buffer.clear();
287                self.utf8_buffer.push(byte);
288                self.utf8_remaining = 1;
289            }
290            0xE0..=0xEF => {
291                self.utf8_buffer.clear();
292                self.utf8_buffer.push(byte);
293                self.utf8_remaining = 2;
294            }
295            0xF0..=0xF7 => {
296                self.utf8_buffer.clear();
297                self.utf8_buffer.push(byte);
298                self.utf8_remaining = 3;
299            }
300            // UTF-8 continuation byte (if we're accumulating)
301            0x80..=0xBF if self.utf8_remaining > 0 => {
302                self.advance_utf8(byte);
303            }
304            _ => {} // Ignore other control bytes
305        }
306    }
307
308    fn advance_escape(&mut self, byte: u8) {
309        match byte {
310            b'[' => {
311                self.csi_buffer.clear();
312                self.state = ParserState::Csi;
313                return; // Stay in Csi state for next byte
314            }
315            b']' => {
316                self.osc_buffer.clear();
317                self.state = ParserState::Osc;
318                return; // Stay in Osc state for next byte
319            }
320            b'7' => self.save_cursor(),
321            b'8' => self.restore_cursor(),
322            b'M' => self.reverse_index(),
323            b'c' => self.full_reset(),
324            b'H' => self.set_tab_stop(),
325            b'(' | b')' => {} // Character set designation -- ignored
326            _ => {}
327        }
328        self.state = ParserState::Ground;
329    }
330
331    fn advance_csi(&mut self, byte: u8) {
332        match byte {
333            // Parameter bytes (0x30-0x3F) -- accumulate
334            0x30..=0x3F => {
335                self.csi_buffer.push(byte as char);
336            }
337            // Intermediate bytes (0x20-0x2F) -- store separately
338            0x20..=0x2F => {
339                self.csi_intermediate = byte;
340            }
341            // Final byte (0x40-0x7E) -- dispatch
342            0x40..=0x7E => {
343                let raw = self.csi_buffer.clone();
344                let intermediate = self.csi_intermediate;
345                let csi = CsiSequence::parse(&raw);
346                self.dispatch_csi(byte, &csi, intermediate);
347                self.csi_buffer.clear();
348                self.csi_intermediate = 0;
349                self.state = ParserState::Ground;
350            }
351            // Abort on other bytes
352            _ => {
353                self.csi_buffer.clear();
354                self.csi_intermediate = 0;
355                self.state = ParserState::Ground;
356            }
357        }
358    }
359
360    fn advance_osc(&mut self, byte: u8) {
361        match byte {
362            0x07 => {
363                // BEL terminates OSC
364                self.finish_osc();
365                self.state = ParserState::Ground;
366            }
367            0x1B => {
368                // ESC might be start of ST (ESC \)
369                self.osc_buffer.push(byte);
370            }
371            b'\\' if self.osc_buffer.last() == Some(&0x1B) => {
372                // ESC \ = ST (String Terminator)
373                self.osc_buffer.pop(); // Remove the ESC
374                self.finish_osc();
375                self.state = ParserState::Ground;
376            }
377            _ => {
378                self.osc_buffer.push(byte);
379            }
380        }
381    }
382
383    fn dispatch_csi(&mut self, final_byte: u8, csi: &CsiSequence, intermediate: u8) {
384        match final_byte {
385            // Cursor movement
386            b'A' => self.cursor_up(csi.param_or(0, 1)),
387            b'B' => self.cursor_down(csi.param_or(0, 1)),
388            b'C' => self.cursor_right(csi.param_or(0, 1)),
389            b'D' => self.cursor_left(csi.param_or(0, 1)),
390            b'E' => self.cursor_next_line(csi.param_or(0, 1)),
391            b'F' => self.cursor_previous_line(csi.param_or(0, 1)),
392            b'G' | b'`' => {
393                let col = csi.one_based_to_zero(0).min(self.cols.saturating_sub(1));
394                self.set_cursor(col, self.cursor().row);
395            }
396            b'H' | b'f' => {
397                let row = csi.one_based_to_zero(0).min(self.rows.saturating_sub(1));
398                let col = csi.one_based_to_zero(1).min(self.cols.saturating_sub(1));
399                self.set_cursor(col, row);
400            }
401            b'd' => {
402                let row = csi.one_based_to_zero(0).min(self.rows.saturating_sub(1));
403                self.set_cursor(self.cursor().col, row);
404            }
405            // Erasure
406            b'J' => self.erase_display(csi.param_or(0, 0)),
407            b'K' => self.erase_line(csi.param_or(0, 0)),
408            b'X' => self.erase_chars(csi.param_or(0, 1)),
409            // Insert/Delete
410            b'@' => self.insert_blank_chars(csi.param_or(0, 1)),
411            b'P' => self.delete_chars(csi.param_or(0, 1)),
412            b'L' => self.insert_lines(csi.param_or(0, 1)),
413            b'M' => self.delete_lines(csi.param_or(0, 1)),
414            // Scrolling
415            b'S' => self.scroll_up_n(csi.param_or(0, 1)),
416            b'T' => self.scroll_down_n(csi.param_or(0, 1)),
417            // SGR
418            b'm' => self.select_graphic_rendition(&csi.params),
419            // Modes
420            b'h' => self.set_private_modes(&csi.params, true),
421            b'l' => self.set_private_modes(&csi.params, false),
422            // Cursor shape (DECSCUSR)
423            b'q' => {
424                // q with space intermediate = cursor shape (DECSCUSR)
425                if intermediate == b' ' {
426                    self.set_cursor_shape(csi.param_or(0, 0));
427                }
428            }
429            // Device reports
430            b'n' => self.device_status_report(csi.private, &csi.params),
431            b'c' => self.device_attributes(&csi.raw),
432            // Scroll region
433            b'r' => self.set_scroll_region(&csi.params),
434            // Tabs
435            b'I' => self.horizontal_tab_n(csi.param_or(0, 1)),
436            b'Z' => self.horizontal_tab_back_n(csi.param_or(0, 1)),
437            b'g' => self.clear_tabs(&csi.params),
438            b'b' => self.repeat_preceding_char(csi.param_or(0, 1)),
439            // Ignore unrecognized sequences
440            _ => {}
441        }
442    }
443
444    fn finish_osc(&mut self) {
445        let raw = &self.osc_buffer;
446        // OSC sequences are: Ps ; Pt ST
447        // Split on first semicolon
448        if let Some(semi_pos) = raw.iter().position(|&b| b == b';') {
449            let ps = &raw[..semi_pos];
450            let pt = &raw[semi_pos + 1..];
451
452            match ps {
453                b"0" | b"2" => {
454                    // Set window title
455                    if let Ok(title) = std::str::from_utf8(pt) {
456                        self.title = Some(title.to_string());
457                    }
458                }
459                b"52" => {
460                    // Clipboard write (base64 encoded)
461                    // Skip the selection character (e.g., 'c')
462                    let data_start = if pt.first().map_or(false, |b| b.is_ascii_alphabetic()) {
463                        &pt[1..]
464                    } else {
465                        pt
466                    };
467                    // We don't decode base64 here -- just store as-is for now
468                    if let Ok(text) = std::str::from_utf8(data_start) {
469                        self.clipboard.push(text.to_string());
470                    }
471                }
472                _ => {}
473            }
474        }
475    }
476
477    fn full_reset(&mut self) {
478        let style = Style::default();
479        self.current_style = style;
480        self.primary.reset(self.cols, self.rows, style);
481        self.alternate.reset(self.cols, self.rows, style);
482        self.active = ScreenKind::Primary;
483        self.modes = TerminalModes::default();
484        self.tab_stops = default_tab_stops(self.cols);
485        self.scroll_region = Region {
486            top: 0,
487            bottom: self.rows.saturating_sub(1),
488        };
489        self.state = ParserState::Ground;
490        self.csi_buffer.clear();
491        self.osc_buffer.clear();
492        self.utf8_buffer.clear();
493        self.utf8_remaining = 0;
494        self.last_printed = None;
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    fn term(cols: usize, rows: usize) -> Terminal {
503        Terminal::new(cols, rows)
504    }
505
506    #[test]
507    fn plain_ascii() {
508        let mut t = term(10, 2);
509        t.write(b"hello");
510        assert_eq!(t.plain_text(), "hello");
511    }
512
513    #[test]
514    fn newline() {
515        let mut t = term(10, 3);
516        t.write(b"line1\nline2");
517        assert_eq!(t.plain_text(), "line1\nline2");
518    }
519
520    #[test]
521    fn carriage_return_overwrite() {
522        let mut t = term(10, 1);
523        t.write(b"abc\rXY");
524        assert_eq!(t.plain_text(), "XYc");
525    }
526
527    #[test]
528    fn cursor_movement() {
529        let mut t = term(10, 3);
530        t.write(b"a\x1B[2;3Hb");
531        assert_eq!(t.cursor().row, 1);
532        // Cursor advances past the written character (col 2 -> 3)
533        assert_eq!(t.cursor().col, 3);
534    }
535
536    #[test]
537    fn erase_display() {
538        let mut t = term(10, 2);
539        t.write(b"hello\x1B[2J");
540        assert_eq!(t.plain_text(), "");
541    }
542
543    #[test]
544    fn sgr_bold_and_color() {
545        let mut t = term(10, 1);
546        t.write(b"\x1B[1;31mX");
547        let cell = t.cell(0, 0).unwrap();
548        assert!(cell.style().bold);
549    }
550
551    #[test]
552    fn alternate_screen() {
553        let mut t = term(10, 2);
554        t.write(b"primary");
555        t.write(b"\x1B[?1049h"); // Switch to alt screen
556        t.write(b"alternate");
557        assert_eq!(t.plain_text(), "alternate");
558        assert_eq!(t.active_screen(), ScreenKind::Alternate);
559        t.write(b"\x1B[?1049l"); // Back to primary
560        assert_eq!(t.plain_text(), "primary");
561    }
562
563    #[test]
564    fn scrollback_simple() {
565        let mut t = term(10, 2);
566        t.set_max_scrollback(10);
567        t.write(b"line1\nline2");
568        // Both lines fit on screen -- no scroll needed
569        assert_eq!(t.scrollback_len(), 0);
570        assert_eq!(t.plain_text(), "line1\nline2");
571    }
572
573    #[test]
574    fn scrollback() {
575        let mut t = term(10, 2);
576        t.set_max_scrollback(10);
577        t.write(b"line1\nline2\nline3");
578        // One scroll: line1 moves to scrollback, line2 and line3 are on screen
579        assert_eq!(t.scrollback_len(), 1);
580        assert_eq!(t.scrollback_row(0), Some("line1".to_string()));
581        assert_eq!(t.plain_text(), "line2\nline3");
582    }
583
584    #[test]
585    fn window_title() {
586        let mut t = term(10, 2);
587        t.write(b"\x1B]0;My Title\x07");
588        assert_eq!(t.title(), Some("My Title"));
589    }
590
591    #[test]
592    fn wide_characters() {
593        let mut t = term(10, 1);
594        t.write("你".as_bytes());
595        assert_eq!(t.cursor().col, 2); // CJK takes 2 columns
596    }
597
598    #[test]
599    fn cursor_shape() {
600        let mut t = term(10, 1);
601        t.write(b"\x1B[5 q");
602        assert_eq!(t.cursor_shape(), crate::mode::CursorShape::Bar);
603    }
604
605    #[test]
606    fn resize() {
607        let mut t = term(10, 2);
608        t.write(b"hello");
609        t.resize(20, 5);
610        assert_eq!(t.cols(), 20);
611        assert_eq!(t.rows(), 5);
612        assert!(t.plain_text().contains("hello"));
613    }
614}