1use crate::sync_utils::mutex_lock_or_recover;
7use std::sync::{Arc, Mutex};
8use vt100::Parser;
9
10#[derive(Debug, Clone, Default)]
12pub struct CellStyle {
13 pub bold: bool,
14 pub italic: bool,
15 pub underline: bool,
16 pub inverse: bool,
17 pub fg_color: Option<Color>,
18 pub bg_color: Option<Color>,
19}
20
21#[derive(Debug, Clone)]
22pub enum Color {
23 Default,
24 Indexed(u8),
25 Rgb(u8, u8, u8),
26}
27
28#[derive(Debug, Clone)]
30pub struct Cell {
31 pub char: char,
32 pub style: CellStyle,
33}
34
35#[derive(Debug, Clone)]
37pub struct ScreenBuffer {
38 pub cells: Vec<Vec<Cell>>,
39 pub cols: u16,
40 pub rows: u16,
41}
42
43#[derive(Debug, Clone)]
45pub struct CursorPosition {
46 pub row: u16,
47 pub col: u16,
48 pub visible: bool,
49}
50
51pub struct VirtualTerminal {
53 parser: Arc<Mutex<Parser>>,
54 cols: u16,
55 rows: u16,
56}
57
58impl VirtualTerminal {
59 pub fn new(cols: u16, rows: u16) -> Self {
61 let parser = Parser::new(rows, cols, 0);
62 Self {
63 parser: Arc::new(Mutex::new(parser)),
64 cols,
65 rows,
66 }
67 }
68
69 pub fn process(&self, data: &[u8]) {
71 let mut parser = mutex_lock_or_recover(&self.parser);
72 parser.process(data);
73 }
74
75 pub fn screen_text(&self) -> String {
77 let parser = mutex_lock_or_recover(&self.parser);
78 let screen = parser.screen();
79
80 let mut lines = Vec::new();
81 for row in 0..screen.size().0 {
82 let mut line = String::new();
83 for col in 0..screen.size().1 {
84 let cell = screen.cell(row, col);
85 if let Some(cell) = cell {
86 line.push(cell.contents().chars().next().unwrap_or(' '));
87 } else {
88 line.push(' ');
89 }
90 }
91 let trimmed = line.trim_end();
93 lines.push(trimmed.to_string());
94 }
95
96 while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
98 lines.pop();
99 }
100
101 lines.join("\n")
102 }
103
104 pub fn screen_buffer(&self) -> ScreenBuffer {
106 let parser = mutex_lock_or_recover(&self.parser);
107 let screen = parser.screen();
108
109 let mut cells = Vec::new();
110 for row in 0..screen.size().0 {
111 let mut row_cells = Vec::new();
112 for col in 0..screen.size().1 {
113 let cell = screen.cell(row, col);
114 let (char, style) = if let Some(cell) = cell {
115 let c = cell.contents().chars().next().unwrap_or(' ');
116 let s = CellStyle {
117 bold: cell.bold(),
118 italic: cell.italic(),
119 underline: cell.underline(),
120 inverse: cell.inverse(),
121 fg_color: convert_color(cell.fgcolor()),
122 bg_color: convert_color(cell.bgcolor()),
123 };
124 (c, s)
125 } else {
126 (' ', CellStyle::default())
127 };
128 row_cells.push(Cell { char, style });
129 }
130 cells.push(row_cells);
131 }
132
133 ScreenBuffer {
134 cells,
135 cols: self.cols,
136 rows: self.rows,
137 }
138 }
139
140 pub fn cursor(&self) -> CursorPosition {
142 let parser = mutex_lock_or_recover(&self.parser);
143 let screen = parser.screen();
144 let (row, col) = screen.cursor_position();
145
146 CursorPosition {
147 row,
148 col,
149 visible: !screen.hide_cursor(),
150 }
151 }
152
153 pub fn resize(&mut self, cols: u16, rows: u16) {
155 let mut parser = mutex_lock_or_recover(&self.parser);
156 parser.set_size(rows, cols);
157 self.cols = cols;
158 self.rows = rows;
159 }
160
161 pub fn size(&self) -> (u16, u16) {
163 (self.cols, self.rows)
164 }
165
166 pub fn clear(&mut self) {
168 let rows = self.rows;
169 let cols = self.cols;
170 let mut parser = mutex_lock_or_recover(&self.parser);
171 parser.set_size(rows, cols);
172 }
173
174 pub fn is_cell_inverse(&self, row: u16, col: u16) -> bool {
176 let parser = mutex_lock_or_recover(&self.parser);
177 let screen = parser.screen();
178 screen.cell(row, col).map(|c| c.inverse()).unwrap_or(false)
179 }
180
181 pub fn is_cell_bold(&self, row: u16, col: u16) -> bool {
183 let parser = mutex_lock_or_recover(&self.parser);
184 let screen = parser.screen();
185 screen.cell(row, col).map(|c| c.bold()).unwrap_or(false)
186 }
187
188 pub fn cell_style(&self, row: u16, col: u16) -> Option<CellStyle> {
190 let parser = mutex_lock_or_recover(&self.parser);
191 let screen = parser.screen();
192 screen.cell(row, col).map(|cell| CellStyle {
193 bold: cell.bold(),
194 italic: cell.italic(),
195 underline: cell.underline(),
196 inverse: cell.inverse(),
197 fg_color: convert_color(cell.fgcolor()),
198 bg_color: convert_color(cell.bgcolor()),
199 })
200 }
201
202 pub fn get_styled_range(
204 &self,
205 row: u16,
206 start_col: u16,
207 end_col: u16,
208 ) -> Vec<(char, CellStyle)> {
209 let parser = mutex_lock_or_recover(&self.parser);
210 let screen = parser.screen();
211
212 let mut result = Vec::new();
213 for col in start_col..end_col {
214 if let Some(cell) = screen.cell(row, col) {
215 let c = cell.contents().chars().next().unwrap_or(' ');
216 let style = CellStyle {
217 bold: cell.bold(),
218 italic: cell.italic(),
219 underline: cell.underline(),
220 inverse: cell.inverse(),
221 fg_color: convert_color(cell.fgcolor()),
222 bg_color: convert_color(cell.bgcolor()),
223 };
224 result.push((c, style));
225 } else {
226 result.push((' ', CellStyle::default()));
227 }
228 }
229 result
230 }
231}
232
233fn convert_color(color: vt100::Color) -> Option<Color> {
234 match color {
235 vt100::Color::Default => Some(Color::Default),
236 vt100::Color::Idx(idx) => Some(Color::Indexed(idx)),
237 vt100::Color::Rgb(r, g, b) => Some(Color::Rgb(r, g, b)),
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_basic_terminal() {
247 let term = VirtualTerminal::new(80, 24);
248 term.process(b"Hello, World!");
249 let text = term.screen_text();
250 assert!(text.contains("Hello, World!"));
251 }
252
253 #[test]
254 fn test_cursor_position() {
255 let term = VirtualTerminal::new(80, 24);
256 term.process(b"ABC");
257 let cursor = term.cursor();
258 assert_eq!(cursor.col, 3);
259 assert_eq!(cursor.row, 0);
260 }
261
262 #[test]
263 fn test_screen_buffer() {
264 let term = VirtualTerminal::new(80, 24);
265 term.process(b"\x1b[1mBold\x1b[0m Normal");
266 let buffer = term.screen_buffer();
267
268 assert!(buffer.cells[0][0].style.bold);
270 assert_eq!(buffer.cells[0][0].char, 'B');
271 }
272}