inquire/terminal/
crossterm.rs

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