virtual_tty/
lib.rs

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        // Use the new tokenized parser
55        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                // Fallback to legacy parsing for compatibility
64                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                // Validate command before executing
85                if command.validate().is_ok() {
86                    self.execute_ansi_command(&command, state);
87                }
88                // If validation fails, silently ignore the command
89            }
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                        // Simple tab handling - advance to next tab stop (8 chars)
105                        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                        // Bell character - typically ignored in terminal emulation
122                    }
123                    ControlChar::VerticalTab => {
124                        // Vertical tab - move to next line
125                        if state.cursor.newline(self.height) {
126                            state.buffer.scroll_up();
127                        }
128                    }
129                    ControlChar::FormFeed => {
130                        // Form feed - clear screen and move to top
131                        state.buffer.clear();
132                        state.cursor.set_position(0, 0, self.height, self.width);
133                    }
134                }
135            }
136            Token::Invalid(_) => {
137                // Ignore invalid tokens for now
138            }
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                // Start of escape sequence
148                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                // Carriage return
155                state.cursor.carriage_return();
156            } else if ch == '\n' {
157                // Newline
158                if state.cursor.newline(self.height) {
159                    state.buffer.scroll_up();
160                }
161            } else if ch == '\x08' {
162                // Backspace
163                state.cursor.backspace();
164            } else {
165                // Regular character
166                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                // SGR (Select Graphic Rendition) - ignore for now
239            }
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    // =============================================================================
357    // STDERR TESTS - Mirror of stdout tests but using stderr_write()
358    // =============================================================================
359
360    #[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"); // Move up 1 line
438        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}