rskit_cli/prompt/terminal/
rich.rs1use std::io::{self, IsTerminal, Write};
15
16use crossterm::cursor::MoveToPreviousLine;
17use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, read};
18use crossterm::terminal::{Clear, ClearType, disable_raw_mode, enable_raw_mode};
19use crossterm::{execute, queue};
20use rskit_errors::{AppError, AppResult};
21
22use super::{Capabilities, Terminal};
23use crate::prompt::key::Key;
24
25pub struct RichTerminal {
29 writer: io::Stderr,
30 raw: bool,
31}
32
33impl RichTerminal {
34 pub fn stderr() -> AppResult<Self> {
42 let writer = io::stderr();
43 if !writer.is_terminal() {
44 return Err(AppError::invalid_input(
45 "prompt",
46 "rich terminal requires an interactive stderr",
47 ));
48 }
49 if !io::stdin().is_terminal() {
50 return Err(AppError::invalid_input(
51 "prompt",
52 "rich terminal requires an interactive stdin for key input",
53 ));
54 }
55 Ok(Self { writer, raw: false })
56 }
57}
58
59impl Drop for RichTerminal {
60 fn drop(&mut self) {
61 if self.raw {
62 let _ = disable_raw_mode();
63 }
64 }
65}
66
67const fn map_key(event: KeyEvent) -> Key {
68 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
69 match event.code {
70 KeyCode::Char('c' | 'd') if ctrl => Key::Interrupt,
71 KeyCode::Char(' ') => Key::Space,
72 KeyCode::Char(c) => Key::Char(c),
73 KeyCode::Enter => Key::Enter,
74 KeyCode::Backspace => Key::Backspace,
75 KeyCode::Esc => Key::Escape,
76 KeyCode::Tab => Key::Tab,
77 KeyCode::Up => Key::Up,
78 KeyCode::Down => Key::Down,
79 KeyCode::Left => Key::Left,
80 KeyCode::Right => Key::Right,
81 KeyCode::Home => Key::Home,
82 KeyCode::End => Key::End,
83 _ => Key::Unknown,
84 }
85}
86
87impl Terminal for RichTerminal {
88 fn capabilities(&self) -> Capabilities {
89 Capabilities::key_driven()
90 }
91
92 fn read_line(&mut self) -> AppResult<Option<String>> {
93 Err(AppError::invalid_input(
94 "prompt",
95 "rich terminal reads keys, not lines",
96 ))
97 }
98
99 fn read_key(&mut self) -> AppResult<Key> {
100 loop {
101 match read().map_err(AppError::internal)? {
102 Event::Key(event) if event.kind == KeyEventKind::Press => {
103 return Ok(map_key(event));
104 }
105 _ => {}
106 }
107 }
108 }
109
110 fn write(&mut self, text: &str) -> AppResult<()> {
111 write!(self.writer, "{text}").map_err(AppError::internal)
112 }
113
114 fn write_line(&mut self, text: &str) -> AppResult<()> {
115 let newline = if self.raw { "\r\n" } else { "\n" };
119 write!(self.writer, "{text}{newline}").map_err(AppError::internal)
120 }
121
122 fn flush(&mut self) -> AppResult<()> {
123 self.writer.flush().map_err(AppError::internal)
124 }
125
126 fn clear_last_lines(&mut self, count: u16) -> AppResult<()> {
127 if count == 0 {
128 return Ok(());
129 }
130 queue!(
131 self.writer,
132 MoveToPreviousLine(count),
133 Clear(ClearType::FromCursorDown)
134 )
135 .map_err(AppError::internal)?;
136 self.flush()
137 }
138
139 fn begin_interactive(&mut self) -> AppResult<()> {
140 if !self.raw {
141 enable_raw_mode().map_err(AppError::internal)?;
142 self.raw = true;
143 }
144 Ok(())
145 }
146
147 fn end_interactive(&mut self) -> AppResult<()> {
148 if !self.raw {
152 return Ok(());
153 }
154 disable_raw_mode().map_err(AppError::internal)?;
155 self.raw = false;
156 execute!(self.writer, Clear(ClearType::CurrentLine)).map_err(AppError::internal)
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::{Key, KeyCode, KeyEvent, KeyModifiers, RichTerminal, map_key};
163
164 #[test]
165 fn requires_a_terminal_stderr() {
166 use std::io::{IsTerminal, stderr, stdin};
171
172 let result = RichTerminal::stderr();
173 if stdin().is_terminal() && stderr().is_terminal() {
174 assert!(result.is_ok());
175 } else {
176 assert!(result.is_err());
177 }
178 }
179
180 #[test]
181 fn maps_control_keys_to_interrupt() {
182 let ctrl_c = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
183 assert_eq!(map_key(ctrl_c), Key::Interrupt);
184 let plain_c = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE);
185 assert_eq!(map_key(plain_c), Key::Char('c'));
186 }
187
188 #[test]
189 fn maps_space_bar_to_space_not_char() {
190 let space = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE);
195 assert_eq!(map_key(space), Key::Space);
196 }
197
198 #[test]
199 fn maps_navigation_keys() {
200 assert_eq!(map_key(KeyEvent::from(KeyCode::Up)), Key::Up);
201 assert_eq!(map_key(KeyEvent::from(KeyCode::Enter)), Key::Enter);
202 assert_eq!(map_key(KeyEvent::from(KeyCode::Backspace)), Key::Backspace);
203 assert_eq!(map_key(KeyEvent::from(KeyCode::Esc)), Key::Escape);
204 assert_eq!(map_key(KeyEvent::from(KeyCode::F(5))), Key::Unknown);
205 }
206}