Skip to main content

shell_tunnel/output/
screen.rs

1//! Virtual terminal screen using vt100.
2
3use vt100::Parser;
4
5/// Virtual terminal screen for processing interactive output.
6///
7/// This maintains a virtual screen buffer that accurately tracks
8/// cursor position, text content, and screen state for applications
9/// like vim, top, etc.
10pub struct VirtualScreen {
11    parser: Parser,
12}
13
14impl VirtualScreen {
15    /// Create a new virtual screen with default dimensions (80x24).
16    pub fn new() -> Self {
17        Self::with_size(80, 24)
18    }
19
20    /// Create a virtual screen with custom dimensions.
21    pub fn with_size(cols: u16, rows: u16) -> Self {
22        Self {
23            parser: Parser::new(rows, cols, 0),
24        }
25    }
26
27    /// Process input bytes and update screen state.
28    pub fn process(&mut self, input: &[u8]) {
29        self.parser.process(input);
30    }
31
32    /// Get the current screen contents as plain text.
33    ///
34    /// Returns each row as a string, trimmed of trailing whitespace.
35    pub fn contents(&self) -> String {
36        self.parser.screen().contents()
37    }
38
39    /// Get screen contents as lines.
40    pub fn lines(&self) -> Vec<String> {
41        let screen = self.parser.screen();
42        let mut lines = Vec::new();
43
44        for row in 0..screen.size().0 {
45            let mut line = String::new();
46            for col in 0..screen.size().1 {
47                if let Some(cell) = screen.cell(row, col) {
48                    line.push(cell.contents().chars().next().unwrap_or(' '));
49                }
50            }
51            lines.push(line.trim_end().to_string());
52        }
53
54        lines
55    }
56
57    /// Get non-empty lines only.
58    pub fn non_empty_lines(&self) -> Vec<String> {
59        self.lines().into_iter().filter(|l| !l.is_empty()).collect()
60    }
61
62    /// Get the current cursor position (row, col).
63    pub fn cursor_position(&self) -> (u16, u16) {
64        self.parser.screen().cursor_position()
65    }
66
67    /// Get screen dimensions (rows, cols).
68    pub fn size(&self) -> (u16, u16) {
69        self.parser.screen().size()
70    }
71
72    /// Check if screen has any content.
73    pub fn is_empty(&self) -> bool {
74        self.contents().trim().is_empty()
75    }
76
77    /// Clear the screen (reset to initial state).
78    pub fn clear(&mut self) {
79        let (rows, cols) = self.size();
80        self.parser = Parser::new(rows, cols, 0);
81    }
82}
83
84impl Default for VirtualScreen {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_new_screen() {
96        let screen = VirtualScreen::new();
97        assert_eq!(screen.size(), (24, 80));
98        assert!(screen.is_empty());
99    }
100
101    #[test]
102    fn test_custom_size() {
103        let screen = VirtualScreen::with_size(120, 40);
104        assert_eq!(screen.size(), (40, 120));
105    }
106
107    #[test]
108    fn test_process_text() {
109        let mut screen = VirtualScreen::new();
110        screen.process(b"Hello, World!");
111
112        let contents = screen.contents();
113        assert!(contents.contains("Hello, World!"));
114    }
115
116    #[test]
117    fn test_process_with_newlines() {
118        let mut screen = VirtualScreen::new();
119        screen.process(b"Line 1\r\nLine 2\r\nLine 3");
120
121        let lines = screen.non_empty_lines();
122        assert!(lines.len() >= 3);
123        assert!(lines[0].contains("Line 1"));
124    }
125
126    #[test]
127    fn test_cursor_position() {
128        let mut screen = VirtualScreen::new();
129        screen.process(b"test");
130
131        let (row, col) = screen.cursor_position();
132        assert_eq!(row, 0);
133        assert_eq!(col, 4); // After "test"
134    }
135
136    #[test]
137    fn test_process_ansi_colors() {
138        let mut screen = VirtualScreen::new();
139        screen.process(b"\x1b[31mRed Text\x1b[0m");
140
141        let contents = screen.contents();
142        assert!(contents.contains("Red Text"));
143    }
144
145    #[test]
146    fn test_clear_screen_sequence() {
147        let mut screen = VirtualScreen::new();
148        screen.process(b"Initial content");
149        screen.process(b"\x1b[2J\x1b[HCleared");
150
151        let contents = screen.contents();
152        assert!(contents.contains("Cleared"));
153    }
154
155    #[test]
156    fn test_clear_method() {
157        let mut screen = VirtualScreen::new();
158        screen.process(b"Some content");
159        screen.clear();
160
161        assert!(screen.is_empty());
162    }
163
164    #[test]
165    fn test_lines_trimmed() {
166        let mut screen = VirtualScreen::new();
167        screen.process(b"  text with spaces  ");
168
169        let lines = screen.lines();
170        // First line should have the text (right-trimmed)
171        assert!(!lines.is_empty());
172    }
173}