1use async_channel::{Receiver, Sender, TryRecvError};
20use async_trait::async_trait;
21use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
22use crossterm::tty::IsTty;
23use crossterm::{QueueableCommand, cursor, style, terminal};
24use endbasic_std::Signal;
25use endbasic_std::console::graphics::InputOps;
26use endbasic_std::console::{
27 CharsXY, ClearType, Console, Key, get_env_var_as_u16, read_key_from_stdin, remove_control_chars,
28};
29use endbasic_std::sound::BEEP_TONE;
30use futures_util::StreamExt;
31use std::cmp::Ordering;
32use std::collections::VecDeque;
33use std::io::{self, StdoutLock, Write};
34use std::thread;
35
36fn parse_key_event(ev: KeyEvent) -> Option<Key> {
38 if ev.kind != KeyEventKind::Press {
39 return None;
40 }
41
42 let key = match ev.code {
43 KeyCode::Backspace => Key::Backspace,
44 KeyCode::Delete => Key::Delete,
45 KeyCode::End => Key::End,
46 KeyCode::Esc => Key::Escape,
47 KeyCode::Home => Key::Home,
48 KeyCode::Tab => Key::Tab,
49 KeyCode::Up => Key::ArrowUp,
50 KeyCode::Down => Key::ArrowDown,
51 KeyCode::Left => Key::ArrowLeft,
52 KeyCode::Right => Key::ArrowRight,
53 KeyCode::PageDown => Key::PageDown,
54 KeyCode::PageUp => Key::PageUp,
55 KeyCode::Char('a') if ev.modifiers == KeyModifiers::CONTROL => Key::Home,
56 KeyCode::Char('b') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowLeft,
57 KeyCode::Char('c') if ev.modifiers == KeyModifiers::CONTROL => Key::Interrupt,
58 KeyCode::Char('d') if ev.modifiers == KeyModifiers::CONTROL => Key::EofOrDelete,
59 KeyCode::Char('e') if ev.modifiers == KeyModifiers::CONTROL => Key::End,
60 KeyCode::Char('f') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowRight,
61 KeyCode::Char('j') if ev.modifiers == KeyModifiers::CONTROL => Key::NewLine,
62 KeyCode::Char('m') if ev.modifiers == KeyModifiers::CONTROL => Key::NewLine,
63 KeyCode::Char('n') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowDown,
64 KeyCode::Char('p') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowUp,
65 KeyCode::Char(ch) => Key::Char(ch),
66 KeyCode::Enter => Key::NewLine,
67 _ => Key::Unknown,
68 };
69 Some(key)
70}
71
72pub struct TerminalConsole {
74 is_tty: bool,
77
78 fg_color: Option<u8>,
80
81 bg_color: Option<u8>,
83
84 cursor_visible: bool,
86
87 alt_active: bool,
89
90 sync_enabled: bool,
92
93 on_key_rx: Receiver<Key>,
95}
96
97impl Drop for TerminalConsole {
98 fn drop(&mut self) {
99 self.on_key_rx.close();
100 if self.is_tty {
101 terminal::disable_raw_mode().unwrap();
102 }
103 }
104}
105
106impl TerminalConsole {
107 pub fn from_stdio(signals_tx: Sender<Signal>) -> io::Result<Self> {
112 let (terminal, _on_key_tx) = Self::from_stdio_with_injector(signals_tx)?;
113 Ok(terminal)
114 }
115
116 pub fn from_stdio_with_injector(signals_tx: Sender<Signal>) -> io::Result<(Self, Sender<Key>)> {
124 let (on_key_tx, on_key_rx) = async_channel::unbounded();
125
126 let is_tty = io::stdin().is_tty() && io::stdout().is_tty();
127
128 if is_tty {
129 terminal::enable_raw_mode()?;
130 tokio::task::spawn(TerminalConsole::raw_key_handler(
131 EventStream::new(),
132 on_key_tx.clone(),
133 signals_tx,
134 ));
135 } else {
136 tokio::task::spawn(TerminalConsole::stdio_key_handler(on_key_tx.clone()));
137 }
138
139 Ok((
140 Self {
141 is_tty,
142 fg_color: None,
143 bg_color: None,
144 cursor_visible: true,
145 alt_active: false,
146 sync_enabled: true,
147 on_key_rx,
148 },
149 on_key_tx,
150 ))
151 }
152
153 async fn raw_key_handler<S>(mut events: S, on_key_tx: Sender<Key>, signals_tx: Sender<Signal>)
156 where
157 S: futures_util::stream::Stream<Item = io::Result<Event>> + Unpin,
158 {
159 loop {
160 let key = tokio::select! {
161 _ = on_key_tx.closed() => break,
162 maybe_event = events.next() => match maybe_event {
163 Some(Ok(Event::Key(ev))) => match parse_key_event(ev) {
164 Some(key) => key,
165 None => continue,
166 },
167 Some(Ok(_)) => continue,
168 Some(Err(_)) => Key::Unknown,
169 None => break,
170 }
171 };
172
173 if key == Key::Interrupt {
174 if signals_tx.send(Signal::Break).await.is_err() {
180 break;
181 }
182 }
183
184 if on_key_tx.send(key).await.is_err() {
186 break;
187 }
188 }
189 }
190
191 async fn stdio_key_handler(on_key_tx: Sender<Key>) {
194 let mut buffer = VecDeque::default();
201
202 let mut done = false;
203 while !done {
204 if on_key_tx.is_closed() {
205 break;
206 }
207
208 let key = match read_key_from_stdin(&mut buffer) {
209 Ok(key) => key,
210 Err(_) => {
211 Key::Unknown
213 }
214 };
215
216 done = key == Key::EofOrDelete;
217
218 if on_key_tx.send(key).await.is_err() {
222 break;
223 }
224 }
225
226 on_key_tx.close();
227 }
228
229 fn maybe_flush(&self, mut lock: StdoutLock<'_>) -> io::Result<()> {
231 if self.sync_enabled { lock.flush() } else { Ok(()) }
232 }
233}
234
235#[async_trait(?Send)]
236impl InputOps for TerminalConsole {
237 async fn poll_key(&mut self) -> io::Result<Option<Key>> {
238 match self.on_key_rx.try_recv() {
239 Ok(k) => Ok(Some(k)),
240 Err(TryRecvError::Empty) => Ok(None),
241 Err(TryRecvError::Closed) => Ok(Some(Key::EofOrDelete)),
242 }
243 }
244
245 async fn read_key(&mut self) -> io::Result<Key> {
246 match self.on_key_rx.recv().await {
247 Ok(k) => Ok(k),
248 Err(_) => Ok(Key::EofOrDelete),
249 }
250 }
251}
252
253#[async_trait(?Send)]
254impl Console for TerminalConsole {
255 fn clear(&mut self, how: ClearType) -> io::Result<()> {
256 let how = match how {
257 ClearType::All => terminal::ClearType::All,
258 ClearType::CurrentLine => terminal::ClearType::CurrentLine,
259 ClearType::PreviousChar => {
260 let stdout = io::stdout();
261 let mut stdout = stdout.lock();
262 stdout.write_all(b"\x08 \x08")?;
263 return self.maybe_flush(stdout);
264 }
265 ClearType::UntilNewLine => terminal::ClearType::UntilNewLine,
266 };
267 let stdout = io::stdout();
268 let mut stdout = stdout.lock();
269 stdout.queue(terminal::Clear(how))?;
270 if how == terminal::ClearType::All {
271 stdout.queue(cursor::MoveTo(0, 0))?;
272 }
273 self.maybe_flush(stdout)
274 }
275
276 fn color(&self) -> (Option<u8>, Option<u8>) {
277 (self.fg_color, self.bg_color)
278 }
279
280 fn set_color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()> {
281 if fg == self.fg_color && bg == self.bg_color {
282 return Ok(());
283 }
284
285 let stdout = io::stdout();
286 let mut stdout = stdout.lock();
287 if fg != self.fg_color {
288 let ct_fg = match fg {
289 None => style::Color::Reset,
290 Some(color) => style::Color::AnsiValue(color),
291 };
292 stdout.queue(style::SetForegroundColor(ct_fg))?;
293 self.fg_color = fg;
294 }
295 if bg != self.bg_color {
296 let ct_bg = match bg {
297 None => style::Color::Reset,
298 Some(color) => style::Color::AnsiValue(color),
299 };
300 stdout.queue(style::SetBackgroundColor(ct_bg))?;
301 self.bg_color = bg;
302 }
303 self.maybe_flush(stdout)
304 }
305
306 fn enter_alt(&mut self) -> io::Result<()> {
307 if !self.alt_active {
308 let stdout = io::stdout();
309 let mut stdout = stdout.lock();
310 stdout.queue(terminal::EnterAlternateScreen)?;
311 self.alt_active = true;
312 self.maybe_flush(stdout)
313 } else {
314 Ok(())
315 }
316 }
317
318 fn hide_cursor(&mut self) -> io::Result<()> {
319 if self.cursor_visible {
320 let stdout = io::stdout();
321 let mut stdout = stdout.lock();
322 stdout.queue(cursor::Hide)?;
323 self.cursor_visible = false;
324 self.maybe_flush(stdout)
325 } else {
326 Ok(())
327 }
328 }
329
330 fn is_interactive(&self) -> bool {
331 self.is_tty
332 }
333
334 fn leave_alt(&mut self) -> io::Result<()> {
335 if self.alt_active {
336 let stdout = io::stdout();
337 let mut stdout = stdout.lock();
338 stdout.queue(terminal::LeaveAlternateScreen)?;
339 self.alt_active = false;
340 self.maybe_flush(stdout)
341 } else {
342 Ok(())
343 }
344 }
345
346 fn locate(&mut self, pos: CharsXY) -> io::Result<()> {
347 #[cfg(debug_assertions)]
348 {
349 let size = self.size_chars()?;
350 assert!(pos.x < size.x);
351 assert!(pos.y < size.y);
352 }
353
354 let stdout = io::stdout();
355 let mut stdout = stdout.lock();
356 stdout.queue(cursor::MoveTo(pos.x, pos.y))?;
357 self.maybe_flush(stdout)
358 }
359
360 fn move_within_line(&mut self, off: i16) -> io::Result<()> {
361 let stdout = io::stdout();
362 let mut stdout = stdout.lock();
363 match off.cmp(&0) {
364 Ordering::Less => stdout.queue(cursor::MoveLeft(-off as u16)),
365 Ordering::Equal => return Ok(()),
366 Ordering::Greater => stdout.queue(cursor::MoveRight(off as u16)),
367 }?;
368 self.maybe_flush(stdout)
369 }
370
371 fn print(&mut self, text: &str) -> io::Result<()> {
372 let text = remove_control_chars(text.to_owned());
373
374 let stdout = io::stdout();
375 let mut stdout = stdout.lock();
376 stdout.write_all(text.as_bytes())?;
377 if self.is_tty {
378 stdout.write_all(b"\r\n")?;
379 } else {
380 stdout.write_all(b"\n")?;
381 }
382 Ok(())
383 }
384
385 async fn poll_key(&mut self) -> io::Result<Option<Key>> {
386 (self as &mut dyn InputOps).poll_key().await
387 }
388
389 async fn read_key(&mut self) -> io::Result<Key> {
390 (self as &mut dyn InputOps).read_key().await
391 }
392
393 fn show_cursor(&mut self) -> io::Result<()> {
394 if !self.cursor_visible {
395 let stdout = io::stdout();
396 let mut stdout = stdout.lock();
397 stdout.queue(cursor::Show)?;
398 self.cursor_visible = true;
399 self.maybe_flush(stdout)
400 } else {
401 Ok(())
402 }
403 }
404
405 fn size_chars(&self) -> io::Result<CharsXY> {
406 let lines = get_env_var_as_u16("LINES");
410 let columns = get_env_var_as_u16("COLUMNS");
411 let size = match (lines, columns) {
412 (Some(l), Some(c)) => CharsXY::new(c, l),
413 (l, c) => {
414 let (actual_columns, actual_lines) = terminal::size()?;
415 CharsXY::new(c.unwrap_or(actual_columns), l.unwrap_or(actual_lines))
416 }
417 };
418 Ok(size)
419 }
420
421 fn write(&mut self, text: &str) -> io::Result<()> {
422 let text = remove_control_chars(text.to_owned());
423
424 let stdout = io::stdout();
425 let mut stdout = stdout.lock();
426 stdout.write_all(text.as_bytes())?;
427 self.maybe_flush(stdout)
428 }
429
430 fn sync_now(&mut self) -> io::Result<()> {
431 if self.sync_enabled { Ok(()) } else { io::stdout().flush() }
432 }
433
434 fn set_sync(&mut self, enabled: bool) -> io::Result<bool> {
435 if !self.sync_enabled {
436 io::stdout().flush()?;
437 }
438 let previous = self.sync_enabled;
439 self.sync_enabled = enabled;
440 Ok(previous)
441 }
442
443 async fn beep(&mut self) -> io::Result<()> {
444 let stdout = io::stdout();
445 let mut stdout = stdout.lock();
446 stdout.write_all(b"\x07")?;
447 stdout.flush()?;
448 thread::sleep(BEEP_TONE.duration);
449 Ok(())
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use futures_util::stream;
457 use std::thread;
458
459 #[test]
460 fn test_parse_key_event_delete() {
461 assert_eq!(
462 Some(Key::Delete),
463 parse_key_event(KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE))
464 );
465 }
466
467 #[test]
468 fn test_drive_raw_keys_eof_or_delete_does_not_terminate_worker() {
469 let (on_key_tx, on_key_rx) = async_channel::unbounded();
470 let (signals_tx, _signals_rx) = async_channel::unbounded();
471 let events = stream::iter([Ok(Event::Key(KeyEvent::new(
472 KeyCode::Char('d'),
473 KeyModifiers::CONTROL,
474 )))]);
475
476 let handle = thread::spawn(move || {
477 let runtime =
478 tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
479 runtime.block_on(TerminalConsole::raw_key_handler(events, on_key_tx, signals_tx));
480 });
481
482 assert_eq!(Key::EofOrDelete, on_key_rx.recv_blocking().unwrap());
483
484 on_key_rx.close();
485 handle.join().unwrap();
486 }
487
488 #[test]
489 fn test_drive_raw_keys_shutdown_without_extra_key() {
490 let (on_key_tx, on_key_rx) = async_channel::unbounded();
491 let (signals_tx, _signals_rx) = async_channel::unbounded();
492 let events = stream::pending();
493
494 let handle = thread::spawn(move || {
495 let runtime =
496 tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
497 runtime.block_on(TerminalConsole::raw_key_handler(events, on_key_tx, signals_tx));
498 });
499
500 on_key_rx.close();
501 handle.join().unwrap();
502
503 assert_eq!(Err(TryRecvError::Closed), on_key_rx.try_recv());
504 }
505}