1use std::io;
2
3macro_rules! try_io {
4 ($expr:expr) => {
5 match $expr {
6 | Ok(v) => v,
7 | Err(e) => return Err(e),
8 }
9 };
10}
11
12pub trait Terminal {
18 fn start(&mut self) -> io::Result<()>;
20 fn stop(&mut self) -> io::Result<()>;
22 fn write(&mut self, data: &str) -> io::Result<()>;
24 fn size(&self) -> io::Result<(u16, u16)>;
26 fn move_cursor(&mut self, row: u16, col: u16) -> io::Result<()>;
28 fn hide_cursor(&mut self) -> io::Result<()>;
30 fn show_cursor(&mut self) -> io::Result<()>;
32}
33
34pub struct TestTerminal {
39 cols: u16,
40 rows: u16,
41 buffer: Vec<String>,
42 cursor_moves: Vec<(u16, u16)>,
43 cursor_hidden: bool,
44}
45
46impl TestTerminal {
47 pub fn new(cols: u16, rows: u16) -> Self {
49 Self {
50 cols,
51 rows,
52 buffer: Vec::new(),
53 cursor_moves: Vec::new(),
54 cursor_hidden: false,
55 }
56 }
57
58 pub fn written(&self) -> &Vec<String> {
60 &self.buffer
61 }
62
63 pub fn cursor_moves(&self) -> &Vec<(u16, u16)> {
65 &self.cursor_moves
66 }
67
68 pub fn is_cursor_hidden(&self) -> bool {
70 self.cursor_hidden
71 }
72}
73
74impl Terminal for TestTerminal {
75 fn start(&mut self) -> io::Result<()> {
76 Ok(())
77 }
78
79 fn stop(&mut self) -> io::Result<()> {
80 Ok(())
81 }
82
83 fn write(&mut self, data: &str) -> io::Result<()> {
84 self.buffer.push(data.to_string());
85 Ok(())
86 }
87
88 fn size(&self) -> io::Result<(u16, u16)> {
89 Ok((self.cols, self.rows))
90 }
91
92 fn move_cursor(&mut self, row: u16, col: u16) -> io::Result<()> {
93 self.cursor_moves.push((row, col));
94 Ok(())
95 }
96
97 fn hide_cursor(&mut self) -> io::Result<()> {
98 self.cursor_hidden = true;
99 Ok(())
100 }
101
102 fn show_cursor(&mut self) -> io::Result<()> {
103 self.cursor_hidden = false;
104 Ok(())
105 }
106}
107
108pub struct ProcessTerminal {
116 stdout: Box<dyn std::io::Write>,
117 is_tty: bool,
118}
119
120impl Default for ProcessTerminal {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126impl ProcessTerminal {
127 pub fn new() -> Self {
129 Self {
130 stdout: Box::new(std::io::stdout()),
131 is_tty: true,
132 }
133 }
134
135 #[cfg(test)]
137 pub fn new_test() -> Self {
138 Self {
139 stdout: Box::new(Vec::new()),
140 is_tty: false,
141 }
142 }
143}
144
145impl Terminal for ProcessTerminal {
146 fn start(&mut self) -> io::Result<()> {
147 if self.is_tty {
148 try_io!(crossterm::terminal::enable_raw_mode());
149 }
150 try_io!(crossterm::execute!(
151 self.stdout,
152 crossterm::terminal::EnterAlternateScreen,
153 crossterm::cursor::Hide
154 ));
155 Ok(())
156 }
157
158 fn stop(&mut self) -> io::Result<()> {
159 try_io!(crossterm::execute!(
160 self.stdout,
161 crossterm::cursor::Show,
162 crossterm::terminal::LeaveAlternateScreen
163 ));
164 if self.is_tty {
165 try_io!(crossterm::terminal::disable_raw_mode());
166 }
167 Ok(())
168 }
169
170 fn write(&mut self, data: &str) -> io::Result<()> {
171 use std::io::Write;
172 try_io!(self.stdout.write_all(data.as_bytes()));
173 try_io!(self.stdout.flush());
174 Ok(())
175 }
176
177 fn size(&self) -> io::Result<(u16, u16)> {
178 if self.is_tty {
179 crossterm::terminal::size()
180 } else {
181 Ok((80, 24))
182 }
183 }
184
185 fn move_cursor(&mut self, row: u16, col: u16) -> io::Result<()> {
186 try_io!(crossterm::execute!(
187 self.stdout,
188 crossterm::cursor::MoveTo(col, row)
189 ));
190 Ok(())
191 }
192
193 fn hide_cursor(&mut self) -> io::Result<()> {
194 try_io!(crossterm::execute!(self.stdout, crossterm::cursor::Hide));
195 Ok(())
196 }
197
198 fn show_cursor(&mut self) -> io::Result<()> {
199 try_io!(crossterm::execute!(self.stdout, crossterm::cursor::Show));
200 Ok(())
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn process_terminal_all_methods() {
210 let mut term = ProcessTerminal::new_test();
211 term.start().unwrap();
212 term.write("hello").unwrap();
213 assert_eq!(term.size().unwrap(), (80, 24));
214 term.move_cursor(5, 10).unwrap();
215 term.hide_cursor().unwrap();
216 term.show_cursor().unwrap();
217 term.stop().unwrap();
218 }
219
220 #[test]
221 fn process_terminal_new_does_not_panic() {
222 let _term = ProcessTerminal::new();
223 }
224}