1use std::io::{stderr, Result, Stderr, Write};
2
3use crossterm::{
4 cursor,
5 event::{self, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
6 queue,
7 style::{Attribute, Color, Print, SetAttribute, SetBackgroundColor, SetForegroundColor},
8 terminal::{self, ClearType},
9 Command,
10};
11
12use crate::{
13 error::InquireResult,
14 ui::{Attributes, InputReader, Key, Styled},
15};
16
17use super::Terminal;
18
19enum IO {
20 Std(Stderr),
21 #[allow(unused)]
22 Test(Vec<u8>),
23}
24
25pub struct CrosstermTerminal {
26 io: IO,
27}
28
29pub struct CrosstermKeyReader;
30
31impl CrosstermKeyReader {
32 pub fn new() -> Self {
33 Self
34 }
35}
36
37impl InputReader for CrosstermKeyReader {
38 fn read_key(&mut self) -> InquireResult<Key> {
39 loop {
40 if let event::Event::Key(key_event) = event::read()? {
41 if KeyEventKind::Press == key_event.kind {
42 return Ok(key_event.into());
43 }
44 }
45 }
46 }
47}
48
49impl CrosstermTerminal {
50 pub fn new() -> InquireResult<Self> {
51 terminal::enable_raw_mode()?;
52
53 Ok(Self {
54 io: IO::Std(stderr()),
55 })
56 }
57
58 fn get_writer(&mut self) -> &mut dyn Write {
59 match &mut self.io {
60 IO::Std(w) => w,
61 IO::Test(w) => w,
62 }
63 }
64
65 fn write_command<C: Command>(&mut self, command: C) -> Result<()> {
66 queue!(&mut self.get_writer(), command)
67 }
68
69 fn set_attributes(&mut self, attributes: Attributes) -> Result<()> {
70 if attributes.contains(Attributes::BOLD) {
71 self.write_command(SetAttribute(Attribute::Bold))?;
72 }
73 if attributes.contains(Attributes::ITALIC) {
74 self.write_command(SetAttribute(Attribute::Italic))?;
75 }
76
77 Ok(())
78 }
79
80 fn reset_attributes(&mut self) -> Result<()> {
81 self.write_command(SetAttribute(Attribute::Reset))
82 }
83
84 fn set_fg_color(&mut self, color: crate::ui::Color) -> Result<()> {
85 self.write_command(SetForegroundColor(color.into()))
86 }
87
88 fn reset_fg_color(&mut self) -> Result<()> {
89 self.write_command(SetForegroundColor(Color::Reset))
90 }
91
92 fn set_bg_color(&mut self, color: crate::ui::Color) -> Result<()> {
93 self.write_command(SetBackgroundColor(color.into()))
94 }
95
96 fn reset_bg_color(&mut self) -> Result<()> {
97 self.write_command(SetBackgroundColor(Color::Reset))
98 }
99}
100
101impl Terminal for CrosstermTerminal {
102 fn cursor_up(&mut self, cnt: u16) -> Result<()> {
103 match cnt {
104 0 => Ok(()),
105 cnt => self.write_command(cursor::MoveUp(cnt)),
106 }
107 }
108
109 fn cursor_down(&mut self, cnt: u16) -> Result<()> {
110 match cnt {
111 0 => Ok(()),
112 cnt => self.write_command(cursor::MoveDown(cnt)),
113 }
114 }
115
116 fn cursor_left(&mut self, cnt: u16) -> Result<()> {
117 match cnt {
118 0 => Ok(()),
119 cnt => self.write_command(cursor::MoveLeft(cnt)),
120 }
121 }
122
123 fn cursor_right(&mut self, cnt: u16) -> Result<()> {
124 match cnt {
125 0 => Ok(()),
126 cnt => self.write_command(cursor::MoveRight(cnt)),
127 }
128 }
129
130 fn cursor_move_to_column(&mut self, idx: u16) -> Result<()> {
131 self.write_command(cursor::MoveToColumn(idx))
132 }
133
134 fn flush(&mut self) -> Result<()> {
135 self.get_writer().flush()
136 }
137
138 fn get_size(&self) -> Result<Option<super::TerminalSize>> {
139 terminal::size().map(|(width, height)| super::TerminalSize::new(width, height))
140 }
141
142 fn write<T: std::fmt::Display>(&mut self, val: T) -> Result<()> {
143 self.write_command(Print(val))
144 }
145
146 fn write_styled<T: std::fmt::Display>(&mut self, val: &Styled<T>) -> Result<()> {
147 if let Some(color) = val.style.fg {
148 self.set_fg_color(color)?;
149 }
150 if let Some(color) = val.style.bg {
151 self.set_bg_color(color)?;
152 }
153 if !val.style.att.is_empty() {
154 self.set_attributes(val.style.att)?;
155 }
156
157 self.write(&val.content)?;
158
159 if val.style.fg.as_ref().is_some() {
160 self.reset_fg_color()?;
161 }
162 if val.style.bg.as_ref().is_some() {
163 self.reset_bg_color()?;
164 }
165 if !val.style.att.is_empty() {
166 self.reset_attributes()?;
167 }
168
169 Ok(())
170 }
171
172 fn clear_line(&mut self) -> Result<()> {
173 self.write_command(terminal::Clear(ClearType::CurrentLine))
174 }
175
176 fn clear_until_new_line(&mut self) -> Result<()> {
177 self.write_command(terminal::Clear(ClearType::UntilNewLine))
178 }
179
180 fn cursor_hide(&mut self) -> Result<()> {
181 self.write_command(cursor::Hide)
182 }
183
184 fn cursor_show(&mut self) -> Result<()> {
185 self.write_command(cursor::Show)
186 }
187}
188
189impl Drop for CrosstermTerminal {
190 fn drop(&mut self) {
191 let _unused = self.flush();
192 let _unused = match self.io {
193 IO::Std(_) => terminal::disable_raw_mode(),
194 IO::Test(_) => Ok(()),
195 };
196 }
197}
198
199impl From<crate::ui::Color> for Color {
200 fn from(c: crate::ui::Color) -> Self {
201 use crate::ui::Color as C;
202 match c {
203 C::Black => Color::Black,
204 C::LightRed => Color::Red,
205 C::DarkRed => Color::DarkRed,
206 C::LightGreen => Color::Green,
207 C::DarkGreen => Color::DarkGreen,
208 C::LightYellow => Color::Yellow,
209 C::DarkYellow => Color::DarkYellow,
210 C::LightBlue => Color::Blue,
211 C::DarkBlue => Color::DarkBlue,
212 C::LightMagenta => Color::Magenta,
213 C::DarkMagenta => Color::DarkMagenta,
214 C::LightCyan => Color::Cyan,
215 C::DarkCyan => Color::DarkCyan,
216 C::White => Color::White,
217 C::Grey => Color::Grey,
218 C::DarkGrey => Color::DarkGrey,
219 C::Rgb { r, g, b } => Color::Rgb { r, g, b },
220 C::AnsiValue(b) => Color::AnsiValue(b),
221 }
222 }
223}
224
225impl From<KeyModifiers> for crate::ui::KeyModifiers {
226 fn from(m: KeyModifiers) -> Self {
227 let mut modifiers = Self::empty();
228
229 if m.contains(KeyModifiers::NONE) {
230 modifiers |= crate::ui::KeyModifiers::NONE;
231 }
232 if m.contains(KeyModifiers::ALT) {
233 modifiers |= crate::ui::KeyModifiers::ALT;
234 }
235 if m.contains(KeyModifiers::CONTROL) {
236 modifiers |= crate::ui::KeyModifiers::CONTROL;
237 }
238 if m.contains(KeyModifiers::SHIFT) {
239 modifiers |= crate::ui::KeyModifiers::SHIFT;
240 }
241 if m.contains(KeyModifiers::SUPER) {
242 modifiers |= crate::ui::KeyModifiers::SUPER;
243 }
244 if m.contains(KeyModifiers::HYPER) {
245 modifiers |= crate::ui::KeyModifiers::HYPER;
246 }
247 if m.contains(KeyModifiers::META) {
248 modifiers |= crate::ui::KeyModifiers::META;
249 }
250
251 modifiers
252 }
253}
254
255impl From<KeyEvent> for Key {
256 fn from(event: KeyEvent) -> Self {
257 match event {
258 KeyEvent {
259 code: KeyCode::Esc, ..
260 } => Self::Escape,
261 KeyEvent {
262 code: KeyCode::Enter | KeyCode::Char('\n' | '\r'),
263 ..
264 } => Self::Enter,
265 KeyEvent {
266 code: KeyCode::Tab | KeyCode::Char('\t'),
267 ..
268 } => Self::Tab,
269 KeyEvent {
270 code: KeyCode::Backspace,
271 ..
272 } => Self::Backspace,
273 KeyEvent {
274 code: KeyCode::Delete,
275 modifiers: m,
276 ..
277 } => Self::Delete(m.into()),
278 KeyEvent {
279 code: KeyCode::Home,
280 ..
281 } => Self::Home,
282 KeyEvent {
283 code: KeyCode::End, ..
284 } => Self::End,
285 KeyEvent {
286 code: KeyCode::PageUp,
287 modifiers: m,
288 ..
289 } => Self::PageUp(m.into()),
290 KeyEvent {
291 code: KeyCode::PageDown,
292 modifiers: m,
293 ..
294 } => Self::PageDown(m.into()),
295 KeyEvent {
296 code: KeyCode::Up,
297 modifiers: m,
298 ..
299 } => Self::Up(m.into()),
300 KeyEvent {
301 code: KeyCode::Down,
302 modifiers: m,
303 ..
304 } => Self::Down(m.into()),
305 KeyEvent {
306 code: KeyCode::Left,
307 modifiers: m,
308 ..
309 } => Self::Left(m.into()),
310 KeyEvent {
311 code: KeyCode::Right,
312 modifiers: m,
313 ..
314 } => Self::Right(m.into()),
315 KeyEvent {
316 code: KeyCode::Char(c),
317 modifiers: m,
318 ..
319 } => Self::Char(c, m.into()),
320 #[allow(deprecated)]
321 _ => Self::Any,
322 }
323 }
324}
325
326#[cfg(test)]
327mod test {
328 use crate::terminal::Terminal;
329 use crate::ui::Color;
330
331 use super::Attributes;
332 use super::CrosstermTerminal;
333 use super::IO;
334
335 impl CrosstermTerminal {
336 pub fn new_in_memory_output() -> Self {
337 Self {
338 io: IO::Test(Vec::new()),
339 }
340 }
341
342 pub fn get_buffer_content(&mut self) -> Vec<u8> {
343 match &mut self.io {
344 IO::Std(_) => panic!("Cannot get write buffer from standard output"),
345 IO::Test(w) => {
346 let mut buffer = Vec::new();
347 std::mem::swap(&mut buffer, w);
348 buffer
349 }
350 }
351 }
352 }
353
354 #[test]
355 fn writer() {
356 let mut terminal = CrosstermTerminal::new_in_memory_output();
357
358 terminal.write("testing ").unwrap();
359 terminal.write("writing ").unwrap();
360 terminal.flush().unwrap();
361 terminal.write("wow").unwrap();
362
363 #[cfg(unix)]
364 assert_eq!(
365 "testing writing wow",
366 std::str::from_utf8(&terminal.get_buffer_content()).unwrap()
367 );
368 }
369
370 #[test]
371 fn style_management() {
372 let mut terminal = CrosstermTerminal::new_in_memory_output();
373
374 terminal.set_attributes(Attributes::BOLD).unwrap();
375 terminal.set_attributes(Attributes::ITALIC).unwrap();
376 terminal.set_attributes(Attributes::BOLD).unwrap();
377 terminal.reset_attributes().unwrap();
378
379 #[cfg(unix)]
380 assert_eq!(
381 "\x1B[1m\x1B[3m\x1B[1m\x1B[0m",
382 std::str::from_utf8(&terminal.get_buffer_content()).unwrap()
383 );
384 }
385
386 #[test]
387 fn style_management_with_flags() {
388 let mut terminal = CrosstermTerminal::new_in_memory_output();
389
390 terminal
391 .set_attributes(Attributes::BOLD | Attributes::ITALIC | Attributes::BOLD)
392 .unwrap();
393 terminal.reset_attributes().unwrap();
394
395 #[cfg(unix)]
396 assert_eq!(
397 "\x1B[1m\x1B[3m\x1B[0m",
398 std::str::from_utf8(&terminal.get_buffer_content()).unwrap()
399 );
400 }
401
402 #[test]
403 fn fg_color_management() {
404 let mut terminal = CrosstermTerminal::new_in_memory_output();
405
406 terminal.set_fg_color(Color::LightRed).unwrap();
407 terminal.reset_fg_color().unwrap();
408 terminal.set_fg_color(Color::Black).unwrap();
409 terminal.set_fg_color(Color::LightGreen).unwrap();
410
411 #[cfg(unix)]
412 assert_eq!(
413 "\x1B[38;5;9m\x1B[39m\x1B[38;5;0m\x1B[38;5;10m",
414 std::str::from_utf8(&terminal.get_buffer_content()).unwrap()
415 );
416 }
417
418 #[test]
419 fn bg_color_management() {
420 let mut terminal = CrosstermTerminal::new_in_memory_output();
421
422 terminal.set_bg_color(Color::LightRed).unwrap();
423 terminal.reset_bg_color().unwrap();
424 terminal.set_bg_color(Color::Black).unwrap();
425 terminal.set_bg_color(Color::LightGreen).unwrap();
426
427 #[cfg(unix)]
428 assert_eq!(
429 "\x1B[48;5;9m\x1B[49m\x1B[48;5;0m\x1B[48;5;10m",
430 std::str::from_utf8(&terminal.get_buffer_content()).unwrap()
431 );
432 }
433}