use async_channel::{Receiver, Sender, TryRecvError};
use async_trait::async_trait;
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use crossterm::tty::IsTty;
use crossterm::{QueueableCommand, cursor, style, terminal};
use endbasic_std::Signal;
use endbasic_std::console::graphics::InputOps;
use endbasic_std::console::{
CharsXY, ClearType, Console, Key, get_env_var_as_u16, read_key_from_stdin, remove_control_chars,
};
use endbasic_std::sound::BEEP_TONE;
use futures_util::StreamExt;
use std::cmp::Ordering;
use std::collections::VecDeque;
use std::io::{self, StdoutLock, Write};
use std::thread;
fn parse_key_event(ev: KeyEvent) -> Option<Key> {
if ev.kind != KeyEventKind::Press {
return None;
}
let key = match ev.code {
KeyCode::Backspace => Key::Backspace,
KeyCode::Delete => Key::Delete,
KeyCode::End => Key::End,
KeyCode::Esc => Key::Escape,
KeyCode::Home => Key::Home,
KeyCode::Tab => Key::Tab,
KeyCode::Up => Key::ArrowUp,
KeyCode::Down => Key::ArrowDown,
KeyCode::Left => Key::ArrowLeft,
KeyCode::Right => Key::ArrowRight,
KeyCode::PageDown => Key::PageDown,
KeyCode::PageUp => Key::PageUp,
KeyCode::Char('a') if ev.modifiers == KeyModifiers::CONTROL => Key::Home,
KeyCode::Char('b') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowLeft,
KeyCode::Char('c') if ev.modifiers == KeyModifiers::CONTROL => Key::Interrupt,
KeyCode::Char('d') if ev.modifiers == KeyModifiers::CONTROL => Key::EofOrDelete,
KeyCode::Char('e') if ev.modifiers == KeyModifiers::CONTROL => Key::End,
KeyCode::Char('f') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowRight,
KeyCode::Char('j') if ev.modifiers == KeyModifiers::CONTROL => Key::NewLine,
KeyCode::Char('m') if ev.modifiers == KeyModifiers::CONTROL => Key::NewLine,
KeyCode::Char('n') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowDown,
KeyCode::Char('p') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowUp,
KeyCode::Char(ch) => Key::Char(ch),
KeyCode::Enter => Key::NewLine,
_ => Key::Unknown,
};
Some(key)
}
pub struct TerminalConsole {
is_tty: bool,
fg_color: Option<u8>,
bg_color: Option<u8>,
cursor_visible: bool,
alt_active: bool,
sync_enabled: bool,
on_key_rx: Receiver<Key>,
}
impl Drop for TerminalConsole {
fn drop(&mut self) {
self.on_key_rx.close();
if self.is_tty {
terminal::disable_raw_mode().unwrap();
}
}
}
impl TerminalConsole {
pub fn from_stdio(signals_tx: Sender<Signal>) -> io::Result<Self> {
let (terminal, _on_key_tx) = Self::from_stdio_with_injector(signals_tx)?;
Ok(terminal)
}
pub fn from_stdio_with_injector(signals_tx: Sender<Signal>) -> io::Result<(Self, Sender<Key>)> {
let (on_key_tx, on_key_rx) = async_channel::unbounded();
let is_tty = io::stdin().is_tty() && io::stdout().is_tty();
if is_tty {
terminal::enable_raw_mode()?;
tokio::task::spawn(TerminalConsole::raw_key_handler(
EventStream::new(),
on_key_tx.clone(),
signals_tx,
));
} else {
tokio::task::spawn(TerminalConsole::stdio_key_handler(on_key_tx.clone()));
}
Ok((
Self {
is_tty,
fg_color: None,
bg_color: None,
cursor_visible: true,
alt_active: false,
sync_enabled: true,
on_key_rx,
},
on_key_tx,
))
}
async fn raw_key_handler<S>(mut events: S, on_key_tx: Sender<Key>, signals_tx: Sender<Signal>)
where
S: futures_util::stream::Stream<Item = io::Result<Event>> + Unpin,
{
loop {
let key = tokio::select! {
_ = on_key_tx.closed() => break,
maybe_event = events.next() => match maybe_event {
Some(Ok(Event::Key(ev))) => match parse_key_event(ev) {
Some(key) => key,
None => continue,
},
Some(Ok(_)) => continue,
Some(Err(_)) => Key::Unknown,
None => break,
}
};
if key == Key::Interrupt {
if signals_tx.send(Signal::Break).await.is_err() {
break;
}
}
if on_key_tx.send(key).await.is_err() {
break;
}
}
}
async fn stdio_key_handler(on_key_tx: Sender<Key>) {
let mut buffer = VecDeque::default();
let mut done = false;
while !done {
if on_key_tx.is_closed() {
break;
}
let key = match read_key_from_stdin(&mut buffer) {
Ok(key) => key,
Err(_) => {
Key::Unknown
}
};
done = key == Key::EofOrDelete;
if on_key_tx.send(key).await.is_err() {
break;
}
}
on_key_tx.close();
}
fn maybe_flush(&self, mut lock: StdoutLock<'_>) -> io::Result<()> {
if self.sync_enabled { lock.flush() } else { Ok(()) }
}
}
#[async_trait(?Send)]
impl InputOps for TerminalConsole {
async fn poll_key(&mut self) -> io::Result<Option<Key>> {
match self.on_key_rx.try_recv() {
Ok(k) => Ok(Some(k)),
Err(TryRecvError::Empty) => Ok(None),
Err(TryRecvError::Closed) => Ok(Some(Key::EofOrDelete)),
}
}
async fn read_key(&mut self) -> io::Result<Key> {
match self.on_key_rx.recv().await {
Ok(k) => Ok(k),
Err(_) => Ok(Key::EofOrDelete),
}
}
}
#[async_trait(?Send)]
impl Console for TerminalConsole {
fn clear(&mut self, how: ClearType) -> io::Result<()> {
let how = match how {
ClearType::All => terminal::ClearType::All,
ClearType::CurrentLine => terminal::ClearType::CurrentLine,
ClearType::PreviousChar => {
let stdout = io::stdout();
let mut stdout = stdout.lock();
stdout.write_all(b"\x08 \x08")?;
return self.maybe_flush(stdout);
}
ClearType::UntilNewLine => terminal::ClearType::UntilNewLine,
};
let stdout = io::stdout();
let mut stdout = stdout.lock();
stdout.queue(terminal::Clear(how))?;
if how == terminal::ClearType::All {
stdout.queue(cursor::MoveTo(0, 0))?;
}
self.maybe_flush(stdout)
}
fn color(&self) -> (Option<u8>, Option<u8>) {
(self.fg_color, self.bg_color)
}
fn set_color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()> {
if fg == self.fg_color && bg == self.bg_color {
return Ok(());
}
let stdout = io::stdout();
let mut stdout = stdout.lock();
if fg != self.fg_color {
let ct_fg = match fg {
None => style::Color::Reset,
Some(color) => style::Color::AnsiValue(color),
};
stdout.queue(style::SetForegroundColor(ct_fg))?;
self.fg_color = fg;
}
if bg != self.bg_color {
let ct_bg = match bg {
None => style::Color::Reset,
Some(color) => style::Color::AnsiValue(color),
};
stdout.queue(style::SetBackgroundColor(ct_bg))?;
self.bg_color = bg;
}
self.maybe_flush(stdout)
}
fn enter_alt(&mut self) -> io::Result<()> {
if !self.alt_active {
let stdout = io::stdout();
let mut stdout = stdout.lock();
stdout.queue(terminal::EnterAlternateScreen)?;
self.alt_active = true;
self.maybe_flush(stdout)
} else {
Ok(())
}
}
fn hide_cursor(&mut self) -> io::Result<()> {
if self.cursor_visible {
let stdout = io::stdout();
let mut stdout = stdout.lock();
stdout.queue(cursor::Hide)?;
self.cursor_visible = false;
self.maybe_flush(stdout)
} else {
Ok(())
}
}
fn is_interactive(&self) -> bool {
self.is_tty
}
fn leave_alt(&mut self) -> io::Result<()> {
if self.alt_active {
let stdout = io::stdout();
let mut stdout = stdout.lock();
stdout.queue(terminal::LeaveAlternateScreen)?;
self.alt_active = false;
self.maybe_flush(stdout)
} else {
Ok(())
}
}
fn locate(&mut self, pos: CharsXY) -> io::Result<()> {
#[cfg(debug_assertions)]
{
let size = self.size_chars()?;
assert!(pos.x < size.x);
assert!(pos.y < size.y);
}
let stdout = io::stdout();
let mut stdout = stdout.lock();
stdout.queue(cursor::MoveTo(pos.x, pos.y))?;
self.maybe_flush(stdout)
}
fn move_within_line(&mut self, off: i16) -> io::Result<()> {
let stdout = io::stdout();
let mut stdout = stdout.lock();
match off.cmp(&0) {
Ordering::Less => stdout.queue(cursor::MoveLeft(-off as u16)),
Ordering::Equal => return Ok(()),
Ordering::Greater => stdout.queue(cursor::MoveRight(off as u16)),
}?;
self.maybe_flush(stdout)
}
fn print(&mut self, text: &str) -> io::Result<()> {
let text = remove_control_chars(text.to_owned());
let stdout = io::stdout();
let mut stdout = stdout.lock();
stdout.write_all(text.as_bytes())?;
if self.is_tty {
stdout.write_all(b"\r\n")?;
} else {
stdout.write_all(b"\n")?;
}
Ok(())
}
async fn poll_key(&mut self) -> io::Result<Option<Key>> {
(self as &mut dyn InputOps).poll_key().await
}
async fn read_key(&mut self) -> io::Result<Key> {
(self as &mut dyn InputOps).read_key().await
}
fn show_cursor(&mut self) -> io::Result<()> {
if !self.cursor_visible {
let stdout = io::stdout();
let mut stdout = stdout.lock();
stdout.queue(cursor::Show)?;
self.cursor_visible = true;
self.maybe_flush(stdout)
} else {
Ok(())
}
}
fn size_chars(&self) -> io::Result<CharsXY> {
let lines = get_env_var_as_u16("LINES");
let columns = get_env_var_as_u16("COLUMNS");
let size = match (lines, columns) {
(Some(l), Some(c)) => CharsXY::new(c, l),
(l, c) => {
let (actual_columns, actual_lines) = terminal::size()?;
CharsXY::new(c.unwrap_or(actual_columns), l.unwrap_or(actual_lines))
}
};
Ok(size)
}
fn write(&mut self, text: &str) -> io::Result<()> {
let text = remove_control_chars(text.to_owned());
let stdout = io::stdout();
let mut stdout = stdout.lock();
stdout.write_all(text.as_bytes())?;
self.maybe_flush(stdout)
}
fn sync_now(&mut self) -> io::Result<()> {
if self.sync_enabled { Ok(()) } else { io::stdout().flush() }
}
fn set_sync(&mut self, enabled: bool) -> io::Result<bool> {
if !self.sync_enabled {
io::stdout().flush()?;
}
let previous = self.sync_enabled;
self.sync_enabled = enabled;
Ok(previous)
}
async fn beep(&mut self) -> io::Result<()> {
let stdout = io::stdout();
let mut stdout = stdout.lock();
stdout.write_all(b"\x07")?;
stdout.flush()?;
thread::sleep(BEEP_TONE.duration);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures_util::stream;
use std::thread;
#[test]
fn test_parse_key_event_delete() {
assert_eq!(
Some(Key::Delete),
parse_key_event(KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE))
);
}
#[test]
fn test_drive_raw_keys_eof_or_delete_does_not_terminate_worker() {
let (on_key_tx, on_key_rx) = async_channel::unbounded();
let (signals_tx, _signals_rx) = async_channel::unbounded();
let events = stream::iter([Ok(Event::Key(KeyEvent::new(
KeyCode::Char('d'),
KeyModifiers::CONTROL,
)))]);
let handle = thread::spawn(move || {
let runtime =
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
runtime.block_on(TerminalConsole::raw_key_handler(events, on_key_tx, signals_tx));
});
assert_eq!(Key::EofOrDelete, on_key_rx.recv_blocking().unwrap());
on_key_rx.close();
handle.join().unwrap();
}
#[test]
fn test_drive_raw_keys_shutdown_without_extra_key() {
let (on_key_tx, on_key_rx) = async_channel::unbounded();
let (signals_tx, _signals_rx) = async_channel::unbounded();
let events = stream::pending();
let handle = thread::spawn(move || {
let runtime =
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
runtime.block_on(TerminalConsole::raw_key_handler(events, on_key_tx, signals_tx));
});
on_key_rx.close();
handle.join().unwrap();
assert_eq!(Err(TryRecvError::Closed), on_key_rx.try_recv());
}
}