cli_prompts/engine/
crossterm.rs

1use std::io::{Result, Write};
2
3use crossterm::{
4    cursor::{position, MoveTo, MoveToPreviousLine},
5    event::{read, Event},
6    execute, queue,
7    style::{Attribute, Attributes, Color as Cc, Colors, Print, SetAttributes, SetColors},
8    terminal::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, Clear, ClearType},
9};
10
11use crate::{
12    input::Key,
13    style::{Color, Formatting, FormattingOption}
14};
15
16use super::{CommandBuffer, Engine};
17
18struct RawMode(bool);
19
20/// Terminal handing backend implemented with the [crossterm](https://docs.rs/crossterm/latest/crossterm/) crate
21pub struct CrosstermEngine<W: Write> {
22    buffer: W,
23    raw_mode: RawMode,
24    previous_line_count: u16,
25}
26
27/// Command buffer for the `CrosstermEngine`
28pub struct CrosstermCommandBuffer<W: Write> {
29    commands: Vec<Box<dyn Command<W>>>,
30    lines_count: u16,
31}
32
33impl<W: Write> CrosstermEngine<W> {
34    pub fn new(buffer: W) -> Self {
35        CrosstermEngine {
36            buffer,
37            raw_mode: RawMode::ensure(),
38            previous_line_count: 1,
39        }
40    }
41}
42
43impl<W: Write> Engine for CrosstermEngine<W> {
44    type Buffer = CrosstermCommandBuffer<W>;
45
46    fn get_command_buffer(&self) -> Self::Buffer {
47        CrosstermCommandBuffer::new()
48    }
49
50    fn render(&mut self, render_commands: &Self::Buffer) -> Result<()> {
51        for _ in 0..self.previous_line_count - 1 {
52            queue!(self.buffer, MoveToPreviousLine(1))?;
53        }
54
55        queue!(self.buffer, MoveTo(0, position()?.1))?;
56
57        for cmd in &render_commands.commands {
58            cmd.execute(&mut self.buffer)?;
59        }
60
61        queue!(self.buffer, Clear(ClearType::FromCursorDown))?;
62
63        self.previous_line_count = render_commands.lines_count;
64        self.buffer.flush()
65    }
66
67    fn finish_rendering(&mut self) -> Result<()> {
68        execute!(self.buffer, Print("\r\n"))
69    }
70
71    fn read_key(&self) -> Result<Key> {
72        loop {
73            match read() {
74                Ok(evt) => {
75                    if let Event::Key(key) = evt {
76                        return Ok(key.code.into());
77                    } else {
78                        continue;
79                    }
80                }
81                Err(error) => return Err(error),
82            }
83        }
84    }
85}
86
87impl<W: Write> CrosstermCommandBuffer<W> {
88    fn new() -> Self {
89        CrosstermCommandBuffer {
90            commands: vec![],
91            lines_count: 1,
92        }
93    }
94}
95
96impl<W: Write> CommandBuffer for CrosstermCommandBuffer<W> {
97    fn new_line(&mut self) {
98        self.commands.push(Box::new(NewLineCommand));
99        self.lines_count += 1;
100    }
101
102    fn print(&mut self, text: &str) {
103        self.commands.push(Box::new(PrintCommand(text.to_owned())));
104    }
105
106    fn set_formatting(&mut self, formatting: &Formatting) {
107        self.commands
108            .push(Box::new(SetFormattingCommand(formatting.to_owned())));
109    }
110
111    fn reset_formatting(&mut self) {
112        self.commands
113            .push(Box::new(SetFormattingCommand(Formatting::reset())));
114    }
115}
116
117impl<W: Write> super::Clear for CrosstermCommandBuffer<W> {
118    fn clear(&mut self) {
119        self.commands.clear();
120        self.lines_count = 1;
121    }
122}
123
124impl RawMode {
125    pub fn ensure() -> Self {
126        let is_raw = is_raw_mode_enabled().unwrap_or(false);
127        if !is_raw {
128            enable_raw_mode().unwrap_or_default();
129        }
130
131        Self(is_raw)
132    }
133}
134
135impl Drop for RawMode {
136    fn drop(&mut self) {
137        if !self.0 {
138            disable_raw_mode().unwrap_or_default();
139        }
140    }
141}
142
143struct NewLineCommand;
144struct PrintCommand(String);
145struct SetFormattingCommand(Formatting);
146
147trait Command<W: Write> {
148    fn execute(&self, buffer: &mut W) -> Result<()>;
149}
150
151impl<W: Write> Command<W> for PrintCommand {
152    fn execute(&self, buffer: &mut W) -> Result<()> {
153        queue!(buffer, Print(&self.0), Clear(ClearType::UntilNewLine))
154    }
155}
156
157impl<W: Write> Command<W> for NewLineCommand {
158    fn execute(&self, buffer: &mut W) -> Result<()> {
159        queue!(buffer, Print("\r\n"))
160    }
161}
162
163impl<W: Write> Command<W> for SetFormattingCommand {
164    fn execute(&self, buffer: &mut W) -> Result<()> {
165        let colors = Colors {
166            foreground: self.0.foreground_color.map(|c| c.into()),
167            background: self.0.background_color.map(|c| c.into()),
168        };
169
170        let attributes_vec: Vec<Attribute> =
171            self.0.text_formatting.iter().map(|&f| f.into()).collect();
172        let attributes_ref: &[Attribute] = &attributes_vec;
173        let attributes: Attributes = attributes_ref.into();
174
175        queue!(buffer, SetColors(colors), SetAttributes(attributes))
176    }
177}
178
179impl From<FormattingOption> for crossterm::style::Attribute {
180    fn from(value: FormattingOption) -> Self {
181        match value {
182            FormattingOption::Reset => Attribute::Reset,
183            FormattingOption::Bold => Attribute::Bold,
184            FormattingOption::Italic => Attribute::Italic,
185            FormattingOption::Underline => Attribute::Underlined,
186            FormattingOption::CrossedOut => Attribute::CrossedOut,
187        }
188    }
189}
190
191impl From<crossterm::event::KeyCode> for Key {
192    fn from(key_code: crossterm::event::KeyCode) -> Self {
193        match key_code {
194            crossterm::event::KeyCode::Backspace => Key::Backspace,
195            crossterm::event::KeyCode::Enter => Key::Enter,
196            crossterm::event::KeyCode::Left => Key::Left,
197            crossterm::event::KeyCode::Right => Key::Right,
198            crossterm::event::KeyCode::Up => Key::Up,
199            crossterm::event::KeyCode::Down => Key::Down,
200            crossterm::event::KeyCode::Home => Key::Home,
201            crossterm::event::KeyCode::End => Key::End,
202            crossterm::event::KeyCode::PageUp => Key::PageUp,
203            crossterm::event::KeyCode::PageDown => Key::PageDown,
204            crossterm::event::KeyCode::Tab => Key::Tab,
205            crossterm::event::KeyCode::BackTab => Key::BackTab,
206            crossterm::event::KeyCode::Delete => Key::Delete,
207            crossterm::event::KeyCode::Insert => Key::Insert,
208            crossterm::event::KeyCode::F(func) => Key::F(func),
209            crossterm::event::KeyCode::Char(c) => Key::Char(c),
210            crossterm::event::KeyCode::Null => Key::Esc,
211            crossterm::event::KeyCode::Esc => Key::Esc,
212        }
213    }
214}
215
216impl From<Color> for Cc {
217    fn from(value: Color) -> Self {
218        match value {
219            Color::Reset => Cc::Reset,
220            Color::Black => Cc::Black,
221            Color::DarkGrey => Cc::DarkGrey,
222            Color::Red => Cc::Red,
223            Color::DarkRed => Cc::DarkRed,
224            Color::Green => Cc::Green,
225            Color::DarkGreen => Cc::DarkGreen,
226            Color::Yellow => Cc::Yellow,
227            Color::DarkYellow => Cc::DarkYellow,
228            Color::Blue => Cc::Blue,
229            Color::DarkBlue => Cc::DarkBlue,
230            Color::Magenta => Cc::Magenta,
231            Color::DarkMagenta => Cc::DarkMagenta,
232            Color::Cyan => Cc::Cyan,
233            Color::DarkCyan => Cc::DarkCyan,
234            Color::White => Cc::White,
235            Color::Grey => Cc::Grey,
236            Color::Rgb { r, g, b } => Cc::Rgb { r, g, b },
237            Color::AnsiValue(c) => Cc::AnsiValue(c),
238        }
239    }
240}