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 #[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}