Skip to main content

telex/
terminal_state.rs

1use crate::buffer::Cell;
2use crossterm::style::Color;
3use portable_pty::{native_pty_system, CommandBuilder, PtySize};
4use std::cell::RefCell;
5use std::io::{Read, Write};
6use std::rc::Rc;
7use std::sync::mpsc::{self, Receiver};
8use std::thread;
9use vte::{Params, Perform};
10
11/// A 2D grid of terminal cells with cursor tracking.
12#[derive(Clone)]
13pub struct TerminalBuffer {
14    cells: Vec<Vec<Cell>>,
15    rows: usize,
16    cols: usize,
17    cursor_row: usize,
18    cursor_col: usize,
19    cursor_visible: bool,
20    // Current SGR state for new characters
21    current_fg: Color,
22    current_bg: Color,
23    current_bold: bool,
24    current_italic: bool,
25    current_underline: bool,
26    current_dim: bool,
27}
28
29impl TerminalBuffer {
30    pub fn new(rows: usize, cols: usize) -> Self {
31        let cells = vec![vec![Cell::default(); cols]; rows];
32        Self {
33            cells,
34            rows,
35            cols,
36            cursor_row: 0,
37            cursor_col: 0,
38            cursor_visible: true,
39            current_fg: Color::Reset,
40            current_bg: Color::Reset,
41            current_bold: false,
42            current_italic: false,
43            current_underline: false,
44            current_dim: false,
45        }
46    }
47
48    pub fn rows(&self) -> usize {
49        self.rows
50    }
51
52    pub fn cols(&self) -> usize {
53        self.cols
54    }
55
56    pub fn cursor_row(&self) -> usize {
57        self.cursor_row
58    }
59
60    pub fn cursor_col(&self) -> usize {
61        self.cursor_col
62    }
63
64    pub fn cursor_visible(&self) -> bool {
65        self.cursor_visible
66    }
67
68    pub fn cells(&self) -> &Vec<Vec<Cell>> {
69        &self.cells
70    }
71
72    pub fn get_cell(&self, row: usize, col: usize) -> Option<&Cell> {
73        self.cells.get(row).and_then(|r| r.get(col))
74    }
75
76    pub fn set_cell(&mut self, row: usize, col: usize, cell: Cell) {
77        if row < self.rows && col < self.cols {
78            self.cells[row][col] = cell;
79        }
80    }
81
82    pub fn write_char(&mut self, ch: char) {
83        if self.cursor_row < self.rows && self.cursor_col < self.cols {
84            let cell = Cell::styled(
85                ch,
86                self.current_fg,
87                self.current_bg,
88                self.current_bold,
89                self.current_italic,
90                self.current_underline,
91                self.current_dim,
92            );
93            self.cells[self.cursor_row][self.cursor_col] = cell;
94            self.cursor_col += 1;
95
96            // Wrap to next line if we exceed columns
97            if self.cursor_col >= self.cols {
98                self.cursor_col = 0;
99                self.cursor_row += 1;
100                // Scroll if we exceed rows
101                if self.cursor_row >= self.rows {
102                    self.scroll_up();
103                    self.cursor_row = self.rows - 1;
104                }
105            }
106        }
107    }
108
109    pub fn move_cursor(&mut self, row: usize, col: usize) {
110        self.cursor_row = row.min(self.rows.saturating_sub(1));
111        self.cursor_col = col.min(self.cols.saturating_sub(1));
112    }
113
114    pub fn move_cursor_relative(&mut self, row_delta: isize, col_delta: isize) {
115        let new_row = (self.cursor_row as isize + row_delta).max(0) as usize;
116        let new_col = (self.cursor_col as isize + col_delta).max(0) as usize;
117        self.move_cursor(new_row, new_col);
118    }
119
120    pub fn set_cursor_visible(&mut self, visible: bool) {
121        self.cursor_visible = visible;
122    }
123
124    pub fn clear_screen(&mut self) {
125        for row in &mut self.cells {
126            for cell in row {
127                *cell = Cell::default();
128            }
129        }
130    }
131
132    pub fn clear_line(&mut self, row: usize) {
133        if row < self.rows {
134            for col in 0..self.cols {
135                self.cells[row][col] = Cell::default();
136            }
137        }
138    }
139
140    pub fn clear_line_from_cursor(&mut self) {
141        let row = self.cursor_row;
142        if row < self.rows {
143            for col in self.cursor_col..self.cols {
144                self.cells[row][col] = Cell::default();
145            }
146        }
147    }
148
149    pub fn clear_line_to_cursor(&mut self) {
150        let row = self.cursor_row;
151        if row < self.rows {
152            for col in 0..=self.cursor_col.min(self.cols - 1) {
153                self.cells[row][col] = Cell::default();
154            }
155        }
156    }
157
158    pub fn scroll_up(&mut self) {
159        self.cells.remove(0);
160        self.cells.push(vec![Cell::default(); self.cols]);
161    }
162
163    pub fn carriage_return(&mut self) {
164        self.cursor_col = 0;
165    }
166
167    pub fn newline(&mut self) {
168        self.cursor_row += 1;
169        if self.cursor_row >= self.rows {
170            self.scroll_up();
171            self.cursor_row = self.rows - 1;
172        }
173    }
174
175    pub fn tab(&mut self) {
176        // Tab stops at multiples of 8
177        let next_stop = ((self.cursor_col / 8) + 1) * 8;
178        self.cursor_col = next_stop.min(self.cols - 1);
179    }
180
181    pub fn backspace(&mut self) {
182        if self.cursor_col > 0 {
183            self.cursor_col -= 1;
184        }
185    }
186
187    pub fn set_sgr(&mut self, fg: Color, bg: Color, bold: bool, italic: bool, underline: bool, dim: bool) {
188        self.current_fg = fg;
189        self.current_bg = bg;
190        self.current_bold = bold;
191        self.current_italic = italic;
192        self.current_underline = underline;
193        self.current_dim = dim;
194    }
195
196    pub fn reset_sgr(&mut self) {
197        self.current_fg = Color::Reset;
198        self.current_bg = Color::Reset;
199        self.current_bold = false;
200        self.current_italic = false;
201        self.current_underline = false;
202        self.current_dim = false;
203    }
204}
205
206/// Messages sent from PTY reader thread to main thread.
207enum PtyMessage {
208    Output(Vec<u8>),
209    Exited,
210}
211
212/// Handle to a running PTY process.
213#[derive(Clone)]
214pub struct TerminalHandle {
215    inner: Rc<RefCell<TerminalHandleInner>>,
216}
217
218struct TerminalHandleInner {
219    buffer: TerminalBuffer,
220    parser: vte::Parser,
221    pty_pair: Option<PtyPair>,
222    output_rx: Option<Receiver<PtyMessage>>,
223    is_started: bool,
224    is_exited: bool,
225}
226
227struct PtyPair {
228    writer: Box<dyn Write + Send>,
229    _reader_thread: thread::JoinHandle<()>,
230}
231
232impl TerminalHandle {
233    pub fn new(rows: usize, cols: usize) -> Self {
234        Self {
235            inner: Rc::new(RefCell::new(TerminalHandleInner {
236                buffer: TerminalBuffer::new(rows, cols),
237                parser: vte::Parser::new(),
238                pty_pair: None,
239                output_rx: None,
240                is_started: false,
241                is_exited: false,
242            })),
243        }
244    }
245
246    pub fn is_started(&self) -> bool {
247        self.inner.borrow().is_started
248    }
249
250    pub fn is_exited(&self) -> bool {
251        self.inner.borrow().is_exited
252    }
253
254    pub fn spawn(&self, command: &str, args: &[&str], cols: usize, rows: usize) -> Result<(), String> {
255        let mut inner = self.inner.borrow_mut();
256
257        if inner.is_started {
258            return Err("Terminal already started".to_string());
259        }
260
261        let pty_system = native_pty_system();
262
263        let pty_pair = pty_system
264            .openpty(PtySize {
265                rows: rows as u16,
266                cols: cols as u16,
267                pixel_width: 0,
268                pixel_height: 0,
269            })
270            .map_err(|e| format!("Failed to open PTY: {}", e))?;
271
272        let mut cmd = CommandBuilder::new(command);
273        for arg in args {
274            cmd.arg(arg);
275        }
276
277        let mut child = pty_pair
278            .slave
279            .spawn_command(cmd)
280            .map_err(|e| format!("Failed to spawn command: {}", e))?;
281
282        // Spawn reader thread
283        let (output_tx, output_rx) = mpsc::channel();
284        let mut reader = pty_pair
285            .master
286            .try_clone_reader()
287            .map_err(|e| format!("Failed to clone reader: {}", e))?;
288
289        let reader_thread = thread::spawn(move || {
290            let mut buf = [0u8; 4096];
291            loop {
292                match reader.read(&mut buf) {
293                    Ok(0) => {
294                        // EOF - process exited
295                        let _ = output_tx.send(PtyMessage::Exited);
296                        break;
297                    }
298                    Ok(n) => {
299                        if output_tx.send(PtyMessage::Output(buf[..n].to_vec())).is_err() {
300                            // Main thread dropped the receiver
301                            break;
302                        }
303                    }
304                    Err(_) => {
305                        let _ = output_tx.send(PtyMessage::Exited);
306                        break;
307                    }
308                }
309            }
310            // Reap the child process
311            let _ = child.wait();
312        });
313
314        let writer = pty_pair
315            .master
316            .take_writer()
317            .map_err(|e| format!("Failed to take writer: {}", e))?;
318
319        inner.pty_pair = Some(PtyPair {
320            writer,
321            _reader_thread: reader_thread,
322        });
323        inner.output_rx = Some(output_rx);
324        inner.is_started = true;
325        inner.buffer = TerminalBuffer::new(rows, cols);
326
327        Ok(())
328    }
329
330    pub fn poll(&self) {
331        // Collect messages first without holding mutable borrow
332        let messages: Vec<PtyMessage> = {
333            let inner = self.inner.borrow();
334
335            if !inner.is_started || inner.is_exited {
336                return;
337            }
338
339            let Some(ref rx) = inner.output_rx else {
340                return;
341            };
342
343            // Drain all available messages (non-blocking)
344            let mut msgs = Vec::new();
345            while let Ok(msg) = rx.try_recv() {
346                msgs.push(msg);
347            }
348            msgs
349        };
350
351        // Now process messages with mutable borrow
352        for msg in messages {
353            match msg {
354                PtyMessage::Output(data) => {
355                    let mut inner = self.inner.borrow_mut();
356                    // Process the data through the parser
357                    // We need to temporarily access both buffer and parser
358                    // Split the borrow by using raw pointers (safe because we know the lifetimes)
359                    let buffer_ptr = &mut inner.buffer as *mut TerminalBuffer;
360                    let parser_ptr = &mut inner.parser as *mut vte::Parser;
361
362                    unsafe {
363                        let mut performer = TerminalPerformer {
364                            buffer: &mut *buffer_ptr,
365                        };
366                        (*parser_ptr).advance(&mut performer, &data);
367                    }
368                }
369                PtyMessage::Exited => {
370                    let mut inner = self.inner.borrow_mut();
371                    inner.is_exited = true;
372                    return;
373                }
374            }
375        }
376    }
377
378    pub fn send_input(&self, data: &[u8]) -> Result<(), String> {
379        let mut inner = self.inner.borrow_mut();
380
381        if !inner.is_started || inner.is_exited {
382            return Err("Terminal not running".to_string());
383        }
384
385        if let Some(ref mut pty_pair) = inner.pty_pair {
386            pty_pair.writer.write_all(data)
387                .map_err(|e| format!("Failed to write to PTY: {}", e))?;
388            pty_pair.writer.flush()
389                .map_err(|e| format!("Failed to flush PTY: {}", e))?;
390        }
391
392        Ok(())
393    }
394
395    pub fn get_buffer(&self) -> TerminalBuffer {
396        self.inner.borrow().buffer.clone()
397    }
398
399    pub fn resize(&self, rows: usize, cols: usize) -> Result<(), String> {
400        let mut inner = self.inner.borrow_mut();
401
402        if !inner.is_started {
403            return Ok(());
404        }
405
406        if let Some(ref mut _pty_pair) = inner.pty_pair {
407            // Note: portable-pty doesn't expose resize on the writer, so we skip it for now
408            // In a full implementation, we'd need to store the master PTY handle
409        }
410
411        inner.buffer = TerminalBuffer::new(rows, cols);
412        Ok(())
413    }
414}
415
416/// Implements vte::Perform to handle ANSI escape sequences.
417struct TerminalPerformer<'a> {
418    buffer: &'a mut TerminalBuffer,
419}
420
421impl<'a> Perform for TerminalPerformer<'a> {
422    fn print(&mut self, c: char) {
423        self.buffer.write_char(c);
424    }
425
426    fn execute(&mut self, byte: u8) {
427        match byte {
428            b'\n' => {
429                // Line feed
430                self.buffer.newline();
431            }
432            b'\r' => {
433                // Carriage return
434                self.buffer.carriage_return();
435            }
436            b'\t' => {
437                // Tab
438                self.buffer.tab();
439            }
440            0x08 => {
441                // Backspace
442                self.buffer.backspace();
443            }
444            0x07 => {
445                // Bell - ignore for now
446            }
447            _ => {
448                // Ignore other control characters
449            }
450        }
451    }
452
453    fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _c: char) {
454        // DCS sequences - not needed for MVP
455    }
456
457    fn put(&mut self, _byte: u8) {
458        // DCS sequences - not needed for MVP
459    }
460
461    fn unhook(&mut self) {
462        // DCS sequences - not needed for MVP
463    }
464
465    fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {
466        // OSC sequences (like setting title) - not needed for MVP
467    }
468
469    fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, c: char) {
470        match c {
471            'H' | 'f' => {
472                // Cursor Position (CUP): ESC[{row};{col}H
473                let row = params.iter().next().and_then(|p| p.first().copied()).unwrap_or(1);
474                let col = params.iter().nth(1).and_then(|p| p.first().copied()).unwrap_or(1);
475                self.buffer.move_cursor(
476                    (row as usize).saturating_sub(1),
477                    (col as usize).saturating_sub(1),
478                );
479            }
480            'A' => {
481                // Cursor Up (CUU): ESC[{n}A
482                let n = params.iter().next().and_then(|p| p.first().copied()).unwrap_or(1);
483                self.buffer.move_cursor_relative(-(n as isize), 0);
484            }
485            'B' => {
486                // Cursor Down (CUD): ESC[{n}B
487                let n = params.iter().next().and_then(|p| p.first().copied()).unwrap_or(1);
488                self.buffer.move_cursor_relative(n as isize, 0);
489            }
490            'C' => {
491                // Cursor Forward (CUF): ESC[{n}C
492                let n = params.iter().next().and_then(|p| p.first().copied()).unwrap_or(1);
493                self.buffer.move_cursor_relative(0, n as isize);
494            }
495            'D' => {
496                // Cursor Back (CUB): ESC[{n}D
497                let n = params.iter().next().and_then(|p| p.first().copied()).unwrap_or(1);
498                self.buffer.move_cursor_relative(0, -(n as isize));
499            }
500            'J' => {
501                // Erase in Display (ED): ESC[{n}J
502                let n = params.iter().next().and_then(|p| p.first().copied()).unwrap_or(0);
503                match n {
504                    0 => {
505                        // Clear from cursor to end of screen
506                        self.buffer.clear_line_from_cursor();
507                        let row = self.buffer.cursor_row();
508                        for r in (row + 1)..self.buffer.rows() {
509                            self.buffer.clear_line(r);
510                        }
511                    }
512                    1 => {
513                        // Clear from cursor to beginning of screen
514                        self.buffer.clear_line_to_cursor();
515                        let row = self.buffer.cursor_row();
516                        for r in 0..row {
517                            self.buffer.clear_line(r);
518                        }
519                    }
520                    2 => {
521                        // Clear entire screen
522                        self.buffer.clear_screen();
523                    }
524                    _ => {}
525                }
526            }
527            'K' => {
528                // Erase in Line (EL): ESC[{n}K
529                let n = params.iter().next().and_then(|p| p.first().copied()).unwrap_or(0);
530                match n {
531                    0 => self.buffer.clear_line_from_cursor(),
532                    1 => self.buffer.clear_line_to_cursor(),
533                    2 => self.buffer.clear_line(self.buffer.cursor_row()),
534                    _ => {}
535                }
536            }
537            'm' => {
538                // SGR - Select Graphic Rendition: ESC[{n}m
539                let mut fg = self.buffer.current_fg;
540                let mut bg = self.buffer.current_bg;
541                let mut bold = self.buffer.current_bold;
542                let mut italic = self.buffer.current_italic;
543                let mut underline = self.buffer.current_underline;
544                let mut dim = self.buffer.current_dim;
545
546                let mut iter = params.iter();
547                while let Some(param) = iter.next() {
548                    let n = param.first().copied().unwrap_or(0);
549                    match n {
550                        0 => {
551                            // Reset
552                            fg = Color::Reset;
553                            bg = Color::Reset;
554                            bold = false;
555                            italic = false;
556                            underline = false;
557                            dim = false;
558                        }
559                        1 => bold = true,
560                        2 => dim = true,
561                        3 => italic = true,
562                        4 => underline = true,
563                        22 => {
564                            bold = false;
565                            dim = false;
566                        }
567                        23 => italic = false,
568                        24 => underline = false,
569                        // Foreground colors (30-37)
570                        30 => fg = Color::Black,
571                        31 => fg = Color::DarkRed,
572                        32 => fg = Color::DarkGreen,
573                        33 => fg = Color::DarkYellow,
574                        34 => fg = Color::DarkBlue,
575                        35 => fg = Color::DarkMagenta,
576                        36 => fg = Color::DarkCyan,
577                        37 => fg = Color::Grey,
578                        38 => {
579                            // 256-color or RGB
580                            if let Some(next_param) = iter.next() {
581                                let mode = next_param.first().copied().unwrap_or(0);
582                                if mode == 5 {
583                                    // 256-color: ESC[38;5;{n}m
584                                    if let Some(color_param) = iter.next() {
585                                        let color_idx = color_param.first().copied().unwrap_or(0);
586                                        fg = Color::AnsiValue(color_idx as u8);
587                                    }
588                                } else if mode == 2 {
589                                    // RGB: ESC[38;2;{r};{g};{b}m
590                                    let r = iter.next().and_then(|p| p.first().copied()).unwrap_or(0) as u8;
591                                    let g = iter.next().and_then(|p| p.first().copied()).unwrap_or(0) as u8;
592                                    let b = iter.next().and_then(|p| p.first().copied()).unwrap_or(0) as u8;
593                                    fg = Color::Rgb { r, g, b };
594                                }
595                            }
596                        }
597                        39 => fg = Color::Reset,
598                        // Background colors (40-47)
599                        40 => bg = Color::Black,
600                        41 => bg = Color::DarkRed,
601                        42 => bg = Color::DarkGreen,
602                        43 => bg = Color::DarkYellow,
603                        44 => bg = Color::DarkBlue,
604                        45 => bg = Color::DarkMagenta,
605                        46 => bg = Color::DarkCyan,
606                        47 => bg = Color::Grey,
607                        48 => {
608                            // 256-color or RGB
609                            if let Some(next_param) = iter.next() {
610                                let mode = next_param.first().copied().unwrap_or(0);
611                                if mode == 5 {
612                                    // 256-color: ESC[48;5;{n}m
613                                    if let Some(color_param) = iter.next() {
614                                        let color_idx = color_param.first().copied().unwrap_or(0);
615                                        bg = Color::AnsiValue(color_idx as u8);
616                                    }
617                                } else if mode == 2 {
618                                    // RGB: ESC[48;2;{r};{g};{b}m
619                                    let r = iter.next().and_then(|p| p.first().copied()).unwrap_or(0) as u8;
620                                    let g = iter.next().and_then(|p| p.first().copied()).unwrap_or(0) as u8;
621                                    let b = iter.next().and_then(|p| p.first().copied()).unwrap_or(0) as u8;
622                                    bg = Color::Rgb { r, g, b };
623                                }
624                            }
625                        }
626                        49 => bg = Color::Reset,
627                        // Bright foreground colors (90-97)
628                        90 => fg = Color::DarkGrey,
629                        91 => fg = Color::Red,
630                        92 => fg = Color::Green,
631                        93 => fg = Color::Yellow,
632                        94 => fg = Color::Blue,
633                        95 => fg = Color::Magenta,
634                        96 => fg = Color::Cyan,
635                        97 => fg = Color::White,
636                        // Bright background colors (100-107)
637                        100 => bg = Color::DarkGrey,
638                        101 => bg = Color::Red,
639                        102 => bg = Color::Green,
640                        103 => bg = Color::Yellow,
641                        104 => bg = Color::Blue,
642                        105 => bg = Color::Magenta,
643                        106 => bg = Color::Cyan,
644                        107 => bg = Color::White,
645                        _ => {}
646                    }
647                }
648
649                self.buffer.set_sgr(fg, bg, bold, italic, underline, dim);
650            }
651            'h' => {
652                // Set Mode
653                if let Some(param) = params.iter().next() {
654                    let n = param.first().copied().unwrap_or(0);
655                    if n == 25 {
656                        // Show cursor
657                        self.buffer.set_cursor_visible(true);
658                    }
659                }
660            }
661            'l' => {
662                // Reset Mode
663                if let Some(param) = params.iter().next() {
664                    let n = param.first().copied().unwrap_or(0);
665                    if n == 25 {
666                        // Hide cursor
667                        self.buffer.set_cursor_visible(false);
668                    }
669                }
670            }
671            _ => {
672                // Ignore other CSI sequences
673            }
674        }
675    }
676
677    fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {
678        // ESC sequences - not needed for MVP
679    }
680}