1use std::sync::{Arc, Mutex};
2
3mod ansi;
4mod buffer;
5mod cursor;
6mod errors;
7mod state;
8
9use ansi::{parse_escape_sequence, AnsiCommand, AnsiParser, ClearMode, ControlChar, Token};
10use state::TtyState;
11
12pub struct VirtualTty {
13 state: Arc<Mutex<TtyState>>,
14 width: usize,
15 height: usize,
16}
17
18impl VirtualTty {
19 pub fn new(width: usize, height: usize) -> Self {
20 let state = TtyState::new(width, height);
21
22 Self {
23 state: Arc::new(Mutex::new(state)),
24 width,
25 height,
26 }
27 }
28
29 pub fn get_width(&self) -> usize {
30 self.width
31 }
32
33 pub fn get_height(&self) -> usize {
34 self.height
35 }
36
37 pub fn get_size(&self) -> (usize, usize) {
38 (self.width, self.height)
39 }
40
41 pub fn stdout_write(&mut self, data: &str) {
42 self.write_internal(data);
43 }
44
45 pub fn stderr_write(&mut self, data: &str) {
46 self.write_internal(data);
47 }
48
49 pub fn send_input(&mut self, input: &str) {
50 self.write_internal(input);
51 }
52
53 fn write_internal(&mut self, data: &str) {
54 match AnsiParser::parse(data) {
56 Ok(tokens) => {
57 let mut state = self.state.lock().unwrap();
58 for token in tokens {
59 self.process_token(token, &mut state);
60 }
61 }
62 Err(_) => {
63 self.write_internal_legacy(data);
65 }
66 }
67 }
68
69 fn process_token(&self, token: Token, state: &mut TtyState) {
70 match token {
71 Token::Text(text) => {
72 for ch in text.chars() {
73 let cursor_row = state.cursor.row;
74 let cursor_col = state.cursor.col;
75 if cursor_row < self.height && cursor_col < self.width {
76 state.buffer.set_char(cursor_row, cursor_col, ch);
77 if state.cursor.advance(self.width, self.height) {
78 state.buffer.scroll_up();
79 }
80 }
81 }
82 }
83 Token::Command(command) => {
84 if command.validate().is_ok() {
86 self.execute_ansi_command(&command, state);
87 }
88 }
90 Token::ControlChar(ctrl_char) => {
91 match ctrl_char {
92 ControlChar::LineFeed => {
93 if state.cursor.newline(self.height) {
94 state.buffer.scroll_up();
95 }
96 }
97 ControlChar::CarriageReturn => {
98 state.cursor.carriage_return();
99 }
100 ControlChar::Backspace => {
101 state.cursor.backspace();
102 }
103 ControlChar::Tab => {
104 let tab_width = 8;
106 let cursor_col = state.cursor.col;
107 let next_tab_stop = ((cursor_col / tab_width) + 1) * tab_width;
108 let spaces_to_add = next_tab_stop - cursor_col;
109 for _ in 0..spaces_to_add {
110 let cursor_row = state.cursor.row;
111 let cursor_col = state.cursor.col;
112 if cursor_row < self.height && cursor_col < self.width {
113 state.buffer.set_char(cursor_row, cursor_col, ' ');
114 if state.cursor.advance(self.width, self.height) {
115 state.buffer.scroll_up();
116 }
117 }
118 }
119 }
120 ControlChar::Bell => {
121 }
123 ControlChar::VerticalTab => {
124 if state.cursor.newline(self.height) {
126 state.buffer.scroll_up();
127 }
128 }
129 ControlChar::FormFeed => {
130 state.buffer.clear();
132 state.cursor.set_position(0, 0, self.height, self.width);
133 }
134 }
135 }
136 Token::Invalid(_) => {
137 }
139 }
140 }
141
142 fn write_internal_legacy(&mut self, data: &str) {
143 let mut state = self.state.lock().unwrap();
144 let mut chars = data.chars();
145 while let Some(ch) = chars.next() {
146 if ch == '\x1b' {
147 if chars.next() == Some('[') {
149 if let Some(command) = parse_escape_sequence(&mut chars) {
150 self.execute_ansi_command(&command, &mut state);
151 }
152 }
153 } else if ch == '\r' {
154 state.cursor.carriage_return();
156 } else if ch == '\n' {
157 if state.cursor.newline(self.height) {
159 state.buffer.scroll_up();
160 }
161 } else if ch == '\x08' {
162 state.cursor.backspace();
164 } else {
165 let cursor_row = state.cursor.row;
167 let cursor_col = state.cursor.col;
168 if cursor_row < self.height && cursor_col < self.width {
169 state.buffer.set_char(cursor_row, cursor_col, ch);
170 if state.cursor.advance(self.width, self.height) {
171 state.buffer.scroll_up();
172 }
173 }
174 }
175 }
176 }
177
178 fn execute_ansi_command(&self, command: &AnsiCommand, state: &mut TtyState) {
179 match command {
180 AnsiCommand::CursorUp(n) => {
181 state.cursor.move_up(*n);
182 }
183 AnsiCommand::CursorDown(n) => {
184 state.cursor.move_down(*n, self.height);
185 }
186 AnsiCommand::CursorForward(n) => {
187 state.cursor.move_forward(*n, self.width);
188 }
189 AnsiCommand::CursorBack(n) => {
190 state.cursor.move_back(*n);
191 }
192 AnsiCommand::CursorPosition { row, col } => {
193 state
194 .cursor
195 .set_position(*row, *col, self.height, self.width);
196 }
197 AnsiCommand::ClearScreen(clear_mode) => match clear_mode {
198 ClearMode::Entire => {
199 state.buffer.clear();
200 state.cursor.set_position(0, 0, self.height, self.width);
201 }
202 ClearMode::ToBeginning => {
203 let cursor_row = state.cursor.row;
204 let cursor_col = state.cursor.col;
205 state
206 .buffer
207 .clear_from_beginning_to_cursor(cursor_row, cursor_col);
208 }
209 ClearMode::ToEnd => {
210 let cursor_row = state.cursor.row;
211 let cursor_col = state.cursor.col;
212 state
213 .buffer
214 .clear_from_cursor_to_end(cursor_row, cursor_col);
215 }
216 },
217 AnsiCommand::ClearLine(clear_mode) => match clear_mode {
218 ClearMode::Entire => {
219 let cursor_row = state.cursor.row;
220 state.buffer.clear_entire_line(cursor_row);
221 }
222 ClearMode::ToBeginning => {
223 let cursor_row = state.cursor.row;
224 let cursor_col = state.cursor.col;
225 state
226 .buffer
227 .clear_line_from_beginning_to_cursor(cursor_row, cursor_col);
228 }
229 ClearMode::ToEnd => {
230 let cursor_row = state.cursor.row;
231 let cursor_col = state.cursor.col;
232 state
233 .buffer
234 .clear_line_from_cursor_to_end(cursor_row, cursor_col);
235 }
236 },
237 AnsiCommand::SetGraphicsRendition => {
238 }
240 }
241 }
242
243 pub fn get_snapshot(&self) -> String {
244 let state = self.state.lock().unwrap();
245 state.get_snapshot()
246 }
247
248 pub fn clear(&mut self) {
249 let mut state = self.state.lock().unwrap();
250 state.clear(self.width, self.height);
251 }
252
253 pub fn get_cursor_position(&self) -> (usize, usize) {
254 let state = self.state.lock().unwrap();
255 state.get_cursor_position()
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_new() {
265 let tty = VirtualTty::new(80, 24);
266 assert_eq!(tty.get_width(), 80);
267 assert_eq!(tty.get_height(), 24);
268 assert_eq!(tty.get_size(), (80, 24));
269 }
270
271 #[test]
272 fn test_basic_write() {
273 let mut tty = VirtualTty::new(10, 3);
274 tty.stdout_write("Hello");
275 let snapshot = tty.get_snapshot();
276 insta::assert_snapshot!(snapshot, @r"
277 Hello \n
278 \n
279 \n
280 ");
281 }
282
283 #[test]
284 fn test_newline() {
285 let mut tty = VirtualTty::new(10, 3);
286 tty.stdout_write("Line1\nLine2");
287 let snapshot = tty.get_snapshot();
288 insta::assert_snapshot!(snapshot, @r"
289 Line1 \n
290 Line2 \n
291 \n
292 ");
293 }
294
295 #[test]
296 fn test_line_wrap() {
297 let mut tty = VirtualTty::new(5, 3);
298 tty.stdout_write("HelloWorld");
299 let snapshot = tty.get_snapshot();
300 insta::assert_snapshot!(snapshot, @r"
301 Hello\n
302 World\n
303 \n
304 ");
305 }
306
307 #[test]
308 fn test_clear_screen() {
309 let mut tty = VirtualTty::new(10, 3);
310 tty.stdout_write("Hello\nWorld");
311 tty.stdout_write("\x1b[2J");
312 let snapshot = tty.get_snapshot();
313 insta::assert_snapshot!(snapshot, @r"
314 \n
315 \n
316 \n
317 ");
318 }
319
320 #[test]
321 fn test_stderr() {
322 let mut tty = VirtualTty::new(10, 3);
323 tty.stderr_write("Error!");
324 let snapshot = tty.get_snapshot();
325 insta::assert_snapshot!(snapshot, @r"
326 Error! \n
327 \n
328 \n
329 ");
330 }
331
332 #[test]
333 fn test_scroll() {
334 let mut tty = VirtualTty::new(10, 2);
335 tty.stdout_write("Line1\nLine2\nLine3");
336 let snapshot = tty.get_snapshot();
337 insta::assert_snapshot!(snapshot, @r"
338 Line2 \n
339 Line3 \n
340 ");
341 }
342
343 #[test]
344 fn test_clear() {
345 let mut tty = VirtualTty::new(10, 3);
346 tty.stdout_write("Hello\nWorld");
347 tty.clear();
348 let snapshot = tty.get_snapshot();
349 insta::assert_snapshot!(snapshot, @r"
350 \n
351 \n
352 \n
353 ");
354 }
355
356 #[test]
361 fn test_stderr_basic_write() {
362 let mut tty = VirtualTty::new(10, 3);
363 tty.stderr_write("Hello");
364 let snapshot = tty.get_snapshot();
365 insta::assert_snapshot!(snapshot, @r"
366 Hello \n
367 \n
368 \n
369 ");
370 }
371
372 #[test]
373 fn test_stderr_newline() {
374 let mut tty = VirtualTty::new(10, 3);
375 tty.stderr_write("Line1\nLine2");
376 let snapshot = tty.get_snapshot();
377 insta::assert_snapshot!(snapshot, @r"
378 Line1 \n
379 Line2 \n
380 \n
381 ");
382 }
383
384 #[test]
385 fn test_stderr_line_wrap() {
386 let mut tty = VirtualTty::new(5, 3);
387 tty.stderr_write("HelloWorld");
388 let snapshot = tty.get_snapshot();
389 insta::assert_snapshot!(snapshot, @r"
390 Hello\n
391 World\n
392 \n
393 ");
394 }
395
396 #[test]
397 fn test_stderr_clear_screen() {
398 let mut tty = VirtualTty::new(10, 3);
399 tty.stderr_write("Hello\nWorld");
400 tty.stderr_write("\x1b[2J");
401 let snapshot = tty.get_snapshot();
402 insta::assert_snapshot!(snapshot, @r"
403 \n
404 \n
405 \n
406 ");
407 }
408
409 #[test]
410 fn test_stderr_scroll() {
411 let mut tty = VirtualTty::new(10, 2);
412 tty.stderr_write("Line1\nLine2\nLine3");
413 let snapshot = tty.get_snapshot();
414 insta::assert_snapshot!(snapshot, @r"
415 Line2 \n
416 Line3 \n
417 ");
418 }
419
420 #[test]
421 fn test_mixed_stdout_stderr() {
422 let mut tty = VirtualTty::new(15, 3);
423 tty.stdout_write("Hello");
424 tty.stderr_write(" World");
425 let snapshot = tty.get_snapshot();
426 insta::assert_snapshot!(snapshot, @r"
427 Hello World \n
428 \n
429 \n
430 ");
431 }
432
433 #[test]
434 fn test_stderr_with_ansi_escape() {
435 let mut tty = VirtualTty::new(10, 3);
436 tty.stderr_write("Hello");
437 tty.stderr_write("\x1b[1A"); tty.stderr_write("X");
439 let snapshot = tty.get_snapshot();
440 insta::assert_snapshot!(snapshot, @r"
441 HelloX \n
442 \n
443 \n
444 ");
445 }
446}