agent_tui/terminal/
vterm.rs

1use std::sync::Arc;
2use std::sync::Mutex;
3
4use vt100::Parser;
5
6use crate::common::mutex_lock_or_recover;
7use crate::core::{CellStyle, Color, ScreenGrid};
8
9#[derive(Debug, Clone)]
10pub struct Cell {
11    pub char: char,
12    pub style: CellStyle,
13}
14
15#[derive(Debug, Clone)]
16pub struct ScreenBuffer {
17    pub cells: Vec<Vec<Cell>>,
18}
19
20impl ScreenGrid for ScreenBuffer {
21    fn rows(&self) -> usize {
22        self.cells.len()
23    }
24
25    fn cols(&self) -> usize {
26        self.cells.first().map(|r| r.len()).unwrap_or(0)
27    }
28
29    fn cell(&self, row: usize, col: usize) -> Option<(char, CellStyle)> {
30        self.cells
31            .get(row)
32            .and_then(|r| r.get(col))
33            .map(|c| (c.char, c.style.clone()))
34    }
35}
36
37pub use crate::core::CursorPosition;
38
39pub struct VirtualTerminal {
40    parser: Arc<Mutex<Parser>>,
41    cols: u16,
42    rows: u16,
43}
44
45const MAX_SCROLLBACK: usize = 1000;
46
47impl VirtualTerminal {
48    pub fn new(cols: u16, rows: u16) -> Self {
49        let parser = Parser::new(rows, cols, MAX_SCROLLBACK);
50        Self {
51            parser: Arc::new(Mutex::new(parser)),
52            cols,
53            rows,
54        }
55    }
56
57    pub fn process(&self, data: &[u8]) {
58        let mut parser = mutex_lock_or_recover(&self.parser);
59        parser.process(data);
60    }
61
62    pub fn screen_text(&self) -> String {
63        let parser = mutex_lock_or_recover(&self.parser);
64        let screen = parser.screen();
65
66        let mut lines = Vec::new();
67        for row in 0..screen.size().0 {
68            let mut line = String::new();
69            for col in 0..screen.size().1 {
70                let cell = screen.cell(row, col);
71                if let Some(cell) = cell {
72                    line.push(cell.contents().chars().next().unwrap_or(' '));
73                } else {
74                    line.push(' ');
75                }
76            }
77
78            let trimmed = line.trim_end();
79            lines.push(trimmed.to_string());
80        }
81
82        while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
83            lines.pop();
84        }
85
86        lines.join("\n")
87    }
88
89    pub fn screen_buffer(&self) -> ScreenBuffer {
90        let parser = mutex_lock_or_recover(&self.parser);
91        let screen = parser.screen();
92
93        let mut cells = Vec::new();
94        for row in 0..screen.size().0 {
95            let mut row_cells = Vec::new();
96            for col in 0..screen.size().1 {
97                let cell = screen.cell(row, col);
98                let (char, style) = if let Some(cell) = cell {
99                    let c = cell.contents().chars().next().unwrap_or(' ');
100                    let s = CellStyle {
101                        bold: cell.bold(),
102                        underline: cell.underline(),
103                        inverse: cell.inverse(),
104                        fg_color: convert_color(cell.fgcolor()),
105                        bg_color: convert_color(cell.bgcolor()),
106                    };
107                    (c, s)
108                } else {
109                    (' ', CellStyle::default())
110                };
111                row_cells.push(Cell { char, style });
112            }
113            cells.push(row_cells);
114        }
115
116        ScreenBuffer { cells }
117    }
118
119    pub fn cursor(&self) -> CursorPosition {
120        let parser = mutex_lock_or_recover(&self.parser);
121        let screen = parser.screen();
122        let (row, col) = screen.cursor_position();
123
124        CursorPosition {
125            row,
126            col,
127            visible: !screen.hide_cursor(),
128        }
129    }
130
131    pub fn resize(&mut self, cols: u16, rows: u16) {
132        let mut parser = mutex_lock_or_recover(&self.parser);
133        parser.screen_mut().set_size(rows, cols);
134        self.cols = cols;
135        self.rows = rows;
136    }
137
138    pub fn size(&self) -> (u16, u16) {
139        (self.cols, self.rows)
140    }
141
142    pub fn clear(&mut self) {
143        let rows = self.rows;
144        let cols = self.cols;
145        let mut parser = mutex_lock_or_recover(&self.parser);
146        parser.screen_mut().set_size(rows, cols);
147    }
148}
149
150fn convert_color(color: vt100::Color) -> Option<Color> {
151    match color {
152        vt100::Color::Default => Some(Color::Default),
153        vt100::Color::Idx(idx) => Some(Color::Indexed(idx)),
154        vt100::Color::Rgb(r, g, b) => Some(Color::Rgb(r, g, b)),
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_basic_terminal() {
164        let term = VirtualTerminal::new(80, 24);
165        term.process(b"Hello, World!");
166        let text = term.screen_text();
167        assert!(text.contains("Hello, World!"));
168    }
169
170    #[test]
171    fn test_cursor_position() {
172        let term = VirtualTerminal::new(80, 24);
173        term.process(b"ABC");
174        let cursor = term.cursor();
175        assert_eq!(cursor.col, 3);
176        assert_eq!(cursor.row, 0);
177    }
178
179    #[test]
180    fn test_screen_buffer() {
181        let term = VirtualTerminal::new(80, 24);
182        term.process(b"\x1b[1mBold\x1b[0m Normal");
183        let buffer = term.screen_buffer();
184
185        assert!(buffer.cells[0][0].style.bold);
186        assert_eq!(buffer.cells[0][0].char, 'B');
187    }
188}