agent_tui/
terminal.rs

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