1use std::io;
2
3pub trait Terminal {
9 fn start(&mut self) -> io::Result<()>;
11 fn stop(&mut self) -> io::Result<()>;
13 fn write(&mut self, data: &str) -> io::Result<()>;
15 fn size(&self) -> io::Result<(u16, u16)>;
17 fn move_cursor(&mut self, row: u16, col: u16) -> io::Result<()>;
19 fn hide_cursor(&mut self) -> io::Result<()>;
21 fn show_cursor(&mut self) -> io::Result<()>;
23}
24
25pub struct TestTerminal {
30 cols: u16,
31 rows: u16,
32 buffer: Vec<String>,
33 cursor_moves: Vec<(u16, u16)>,
34 cursor_hidden: bool,
35}
36
37impl TestTerminal {
38 pub fn new(cols: u16, rows: u16) -> Self {
40 Self {
41 cols,
42 rows,
43 buffer: Vec::new(),
44 cursor_moves: Vec::new(),
45 cursor_hidden: false,
46 }
47 }
48
49 pub fn written(&self) -> &Vec<String> {
51 &self.buffer
52 }
53
54 pub fn cursor_moves(&self) -> &Vec<(u16, u16)> {
56 &self.cursor_moves
57 }
58
59 pub fn is_cursor_hidden(&self) -> bool {
61 self.cursor_hidden
62 }
63}
64
65impl Terminal for TestTerminal {
66 fn start(&mut self) -> io::Result<()> {
67 Ok(())
68 }
69
70 fn stop(&mut self) -> io::Result<()> {
71 Ok(())
72 }
73
74 fn write(&mut self, data: &str) -> io::Result<()> {
75 self.buffer.push(data.to_string());
76 Ok(())
77 }
78
79 fn size(&self) -> io::Result<(u16, u16)> {
80 Ok((self.cols, self.rows))
81 }
82
83 fn move_cursor(&mut self, row: u16, col: u16) -> io::Result<()> {
84 self.cursor_moves.push((row, col));
85 Ok(())
86 }
87
88 fn hide_cursor(&mut self) -> io::Result<()> {
89 self.cursor_hidden = true;
90 Ok(())
91 }
92
93 fn show_cursor(&mut self) -> io::Result<()> {
94 self.cursor_hidden = false;
95 Ok(())
96 }
97}
98
99pub struct ProcessTerminal {
107 stdout: Box<dyn std::io::Write>,
108 is_tty: bool,
109}
110
111impl ProcessTerminal {
112 pub fn new() -> Self {
114 Self {
115 stdout: Box::new(std::io::stdout()),
116 is_tty: true,
117 }
118 }
119
120 #[cfg(test)]
122 pub fn new_test() -> Self {
123 Self {
124 stdout: Box::new(Vec::new()),
125 is_tty: false,
126 }
127 }
128}
129
130impl Terminal for ProcessTerminal {
131 fn start(&mut self) -> io::Result<()> {
132 if self.is_tty {
133 crossterm::terminal::enable_raw_mode()?;
134 }
135 crossterm::execute!(
136 self.stdout,
137 crossterm::terminal::EnterAlternateScreen,
138 crossterm::cursor::Hide
139 )?;
140 Ok(())
141 }
142
143 fn stop(&mut self) -> io::Result<()> {
144 crossterm::execute!(
145 self.stdout,
146 crossterm::cursor::Show,
147 crossterm::terminal::LeaveAlternateScreen
148 )?;
149 if self.is_tty {
150 crossterm::terminal::disable_raw_mode()?;
151 }
152 Ok(())
153 }
154
155 fn write(&mut self, data: &str) -> io::Result<()> {
156 use std::io::Write;
157 self.stdout.write_all(data.as_bytes())?;
158 self.stdout.flush()?;
159 Ok(())
160 }
161
162 fn size(&self) -> io::Result<(u16, u16)> {
163 if self.is_tty {
164 crossterm::terminal::size()
165 } else {
166 Ok((80, 24))
167 }
168 }
169
170 fn move_cursor(&mut self, row: u16, col: u16) -> io::Result<()> {
171 crossterm::execute!(self.stdout, crossterm::cursor::MoveTo(col, row))?;
172 Ok(())
173 }
174
175 fn hide_cursor(&mut self) -> io::Result<()> {
176 crossterm::execute!(self.stdout, crossterm::cursor::Hide)?;
177 Ok(())
178 }
179
180 fn show_cursor(&mut self) -> io::Result<()> {
181 crossterm::execute!(self.stdout, crossterm::cursor::Show)?;
182 Ok(())
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn process_terminal_all_methods() {
192 let mut term = ProcessTerminal::new_test();
193 term.start().unwrap();
194 term.write("hello").unwrap();
195 assert_eq!(term.size().unwrap(), (80, 24));
196 term.move_cursor(5, 10).unwrap();
197 term.hide_cursor().unwrap();
198 term.show_cursor().unwrap();
199 term.stop().unwrap();
200 }
201
202 #[test]
203 fn process_terminal_new_does_not_panic() {
204 let _term = ProcessTerminal::new();
205 }
206}