agent_tui/terminal/
vterm.rs1use 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}