inquire/terminal/
crossterm.rs

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}