inquire/terminal/
termion.rs

1use core::fmt;
2use std::{
3    fs::File,
4    io::{Result, Write},
5};
6
7use termion::{
8    color::{self, Color},
9    cursor,
10    event::Key,
11    input::{Keys, TermRead},
12    raw::{IntoRawMode, RawTerminal},
13    terminal_size,
14};
15
16use crate::{
17    error::InquireResult,
18    ui::{Attributes, InputReader, Styled},
19};
20
21use super::Terminal;
22
23#[allow(clippy::upper_case_acronyms)]
24enum IO<'a> {
25    TTY(RawTerminal<File>),
26    #[allow(unused)]
27    Custom(&'a mut (dyn Write)),
28}
29
30pub struct TermionKeyReader {
31    keys: Keys<File>,
32}
33
34impl TermionKeyReader {
35    #[allow(unused)]
36    pub fn new() -> InquireResult<Self> {
37        Ok(Self {
38            keys: termion::get_tty()?.keys(),
39        })
40    }
41}
42
43impl InputReader for TermionKeyReader {
44    fn read_key(&mut self) -> InquireResult<crate::ui::Key> {
45        loop {
46            if let Some(key) = self.keys.next() {
47                let key = key?;
48                return Ok(key.into());
49            }
50        }
51    }
52}
53
54pub struct TermionTerminal<'a> {
55    io: IO<'a>,
56}
57
58impl<'a> TermionTerminal<'a> {
59    #[allow(unused)]
60    pub fn new() -> InquireResult<Self> {
61        let tty = termion::get_tty()?;
62        let raw_terminal = tty.into_raw_mode()?;
63        let keys = raw_terminal.try_clone()?.keys();
64
65        Ok(Self {
66            io: IO::TTY(raw_terminal),
67        })
68    }
69
70    /// # Errors
71    ///
72    /// Will return `std::io::Error` if it fails to get terminal size
73    #[cfg(test)]
74    pub fn new_with_writer<W: 'a + Write>(writer: &'a mut W) -> Self {
75        Self {
76            io: IO::Custom(writer),
77        }
78    }
79
80    fn get_writer(&mut self) -> &mut dyn Write {
81        match &mut self.io {
82            IO::TTY(w) => w,
83            IO::Custom(w) => w,
84        }
85    }
86
87    fn set_attributes(&mut self, attributes: Attributes) -> Result<()> {
88        if attributes.contains(Attributes::BOLD) {
89            write!(self.get_writer(), "{}", termion::style::Bold)?;
90        }
91        if attributes.contains(Attributes::ITALIC) {
92            write!(self.get_writer(), "{}", termion::style::Italic)?;
93        }
94
95        Ok(())
96    }
97
98    fn reset_attributes(&mut self) -> Result<()> {
99        write!(self.get_writer(), "{}", termion::style::Reset)
100    }
101
102    fn set_fg_color(&mut self, color: crate::ui::Color) -> Result<()> {
103        write!(self.get_writer(), "{}", color::Fg(color))
104    }
105
106    fn reset_fg_color(&mut self) -> Result<()> {
107        write!(self.get_writer(), "{}", color::Fg(color::Reset))
108    }
109
110    fn set_bg_color(&mut self, color: crate::ui::Color) -> Result<()> {
111        write!(self.get_writer(), "{}", color::Bg(color))
112    }
113
114    fn reset_bg_color(&mut self) -> Result<()> {
115        write!(self.get_writer(), "{}", color::Bg(color::Reset))
116    }
117}
118
119impl<'a> Terminal for TermionTerminal<'a> {
120    fn cursor_up(&mut self, cnt: u16) -> Result<()> {
121        match cnt {
122            0 => Ok(()),
123            cnt => write!(self.get_writer(), "{}", cursor::Up(cnt)),
124        }
125    }
126
127    fn cursor_down(&mut self, cnt: u16) -> Result<()> {
128        match cnt {
129            0 => Ok(()),
130            cnt => write!(self.get_writer(), "{}", cursor::Down(cnt)),
131        }
132    }
133
134    fn cursor_left(&mut self, cnt: u16) -> Result<()> {
135        match cnt {
136            0 => Ok(()),
137            cnt => write!(self.get_writer(), "{}", cursor::Left(cnt)),
138        }
139    }
140
141    fn cursor_right(&mut self, cnt: u16) -> Result<()> {
142        match cnt {
143            0 => Ok(()),
144            cnt => write!(self.get_writer(), "{}", cursor::Right(cnt)),
145        }
146    }
147
148    fn cursor_move_to_column(&mut self, idx: u16) -> Result<()> {
149        write!(self.get_writer(), "\x1b[{}G", idx.saturating_add(1))
150    }
151
152    fn flush(&mut self) -> Result<()> {
153        self.get_writer().flush()
154    }
155
156    fn get_size(&self) -> Result<Option<super::TerminalSize>> {
157        terminal_size().map(|(width, height)| super::TerminalSize::new(width, height))
158    }
159
160    fn write<T: std::fmt::Display>(&mut self, val: T) -> Result<()> {
161        write!(self.get_writer(), "{}", val)
162    }
163
164    fn write_styled<T: std::fmt::Display>(&mut self, val: &Styled<T>) -> Result<()> {
165        if let Some(color) = val.style.fg {
166            self.set_fg_color(color)?;
167        }
168        if let Some(color) = val.style.bg {
169            self.set_bg_color(color)?;
170        }
171        if !val.style.att.is_empty() {
172            self.set_attributes(val.style.att)?;
173        }
174
175        self.write(&val.content)?;
176
177        if val.style.fg.as_ref().is_some() {
178            self.reset_fg_color()?;
179        }
180        if val.style.bg.as_ref().is_some() {
181            self.reset_bg_color()?;
182        }
183        if !val.style.att.is_empty() {
184            self.reset_attributes()?;
185        }
186
187        Ok(())
188    }
189
190    fn clear_line(&mut self) -> Result<()> {
191        write!(self.get_writer(), "{}", termion::clear::CurrentLine)
192    }
193
194    fn clear_until_new_line(&mut self) -> Result<()> {
195        write!(self.get_writer(), "{}", termion::clear::UntilNewline)
196    }
197
198    fn cursor_hide(&mut self) -> Result<()> {
199        write!(self.get_writer(), "{}", cursor::Hide)
200    }
201
202    fn cursor_show(&mut self) -> Result<()> {
203        write!(self.get_writer(), "{}", cursor::Show)
204    }
205}
206
207impl<'a> Drop for TermionTerminal<'a> {
208    fn drop(&mut self) {
209        let _unused = self.flush();
210    }
211}
212
213macro_rules! into_termion_color {
214    ($self:expr, $fn_name:ident, $f:expr) => {{
215        use $crate::ui::Color as C;
216        match $self {
217            C::Black => color::Black.$fn_name($f),
218            C::LightRed => color::LightRed.$fn_name($f),
219            C::DarkRed => color::Red.$fn_name($f),
220            C::LightGreen => color::LightGreen.$fn_name($f),
221            C::DarkGreen => color::Green.$fn_name($f),
222            C::LightYellow => color::LightYellow.$fn_name($f),
223            C::DarkYellow => color::Yellow.$fn_name($f),
224            C::LightBlue => color::LightBlue.$fn_name($f),
225            C::DarkBlue => color::Blue.$fn_name($f),
226            C::LightMagenta => color::LightMagenta.$fn_name($f),
227            C::DarkMagenta => color::Magenta.$fn_name($f),
228            C::LightCyan => color::LightCyan.$fn_name($f),
229            C::DarkCyan => color::Cyan.$fn_name($f),
230            C::White => color::LightWhite.$fn_name($f),
231            C::Grey => color::White.$fn_name($f),
232            C::DarkGrey => color::LightBlack.$fn_name($f),
233            C::Rgb { r, g, b } => color::Rgb(*r, *g, *b).$fn_name($f),
234            C::AnsiValue(b) => color::AnsiValue(*b).$fn_name($f),
235        }
236    }};
237}
238
239impl Color for crate::ui::Color {
240    fn write_fg(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        into_termion_color!(self, write_fg, f)
242    }
243
244    fn write_bg(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        into_termion_color!(self, write_bg, f)
246    }
247}
248
249impl From<Key> for crate::ui::Key {
250    fn from(key: Key) -> Self {
251        use crate::ui::KeyModifiers;
252
253        match key {
254            Key::Esc => Self::Escape,
255            Key::Char('\n' | '\r') => Self::Enter,
256            Key::Char('\t') => Self::Tab,
257            Key::Backspace => Self::Backspace,
258            Key::Delete => Self::Delete(KeyModifiers::empty()),
259            Key::Home => Self::Home,
260            Key::End => Self::End,
261            Key::PageUp => Self::PageUp(KeyModifiers::empty()),
262            Key::PageDown => Self::PageDown(KeyModifiers::empty()),
263            Key::Up => Self::Up(KeyModifiers::empty()),
264            Key::Down => Self::Down(KeyModifiers::empty()),
265            Key::Left => Self::Left(KeyModifiers::empty()),
266            Key::Right => Self::Right(KeyModifiers::empty()),
267            Key::Char(c) => Self::Char(c, KeyModifiers::empty()),
268            Key::Ctrl(c) => Self::Char(c, KeyModifiers::CONTROL),
269            Key::Alt(c) => Self::Char(c, KeyModifiers::ALT),
270            #[allow(deprecated)]
271            _ => Self::Any,
272        }
273    }
274}
275
276#[cfg(test)]
277mod test {
278    use crate::terminal::Terminal;
279    use crate::ui::Color;
280
281    use super::Attributes;
282    use super::TermionTerminal;
283
284    #[test]
285    fn writer() {
286        let mut write: Vec<u8> = Vec::new();
287
288        {
289            let mut terminal = TermionTerminal::new_with_writer(&mut write);
290
291            terminal.write("testing ").unwrap();
292            terminal.write("writing ").unwrap();
293            terminal.flush().unwrap();
294            terminal.write("wow").unwrap();
295        }
296
297        #[cfg(unix)]
298        assert_eq!("testing writing wow", std::str::from_utf8(&write).unwrap());
299    }
300
301    #[test]
302    fn style_management() {
303        let mut write: Vec<u8> = Vec::new();
304
305        {
306            let mut terminal = TermionTerminal::new_with_writer(&mut write);
307
308            terminal.set_attributes(Attributes::BOLD).unwrap();
309            terminal.set_attributes(Attributes::ITALIC).unwrap();
310            terminal.set_attributes(Attributes::BOLD).unwrap();
311            terminal.reset_attributes().unwrap();
312        }
313
314        #[cfg(unix)]
315        assert_eq!(
316            "\x1B[1m\x1B[3m\x1B[1m\x1B[m",
317            std::str::from_utf8(&write).unwrap()
318        );
319    }
320
321    #[test]
322    fn style_management_with_flags() {
323        let mut write: Vec<u8> = Vec::new();
324
325        {
326            let mut terminal = TermionTerminal::new_with_writer(&mut write);
327
328            terminal
329                .set_attributes(Attributes::BOLD | Attributes::ITALIC | Attributes::BOLD)
330                .unwrap();
331            terminal.reset_attributes().unwrap();
332        }
333
334        #[cfg(unix)]
335        assert_eq!("\x1B[1m\x1B[3m\x1B[m", std::str::from_utf8(&write).unwrap());
336    }
337
338    #[test]
339    fn fg_color_management() {
340        let mut write: Vec<u8> = Vec::new();
341
342        {
343            let mut terminal = TermionTerminal::new_with_writer(&mut write);
344
345            terminal.set_fg_color(Color::LightRed).unwrap();
346            terminal.reset_fg_color().unwrap();
347            terminal.set_fg_color(Color::Black).unwrap();
348            terminal.set_fg_color(Color::LightGreen).unwrap();
349        }
350
351        #[cfg(unix)]
352        assert_eq!(
353            "\x1B[38;5;9m\x1B[39m\x1B[38;5;0m\x1B[38;5;10m",
354            std::str::from_utf8(&write).unwrap()
355        );
356    }
357
358    #[test]
359    fn bg_color_management() {
360        let mut write: Vec<u8> = Vec::new();
361
362        {
363            let mut terminal = TermionTerminal::new_with_writer(&mut write);
364
365            terminal.set_bg_color(Color::LightRed).unwrap();
366            terminal.reset_bg_color().unwrap();
367            terminal.set_bg_color(Color::Black).unwrap();
368            terminal.set_bg_color(Color::LightGreen).unwrap();
369        }
370
371        #[cfg(unix)]
372        assert_eq!(
373            "\x1B[48;5;9m\x1B[49m\x1B[48;5;0m\x1B[48;5;10m",
374            std::str::from_utf8(&write).unwrap()
375        );
376    }
377}