#![allow(clippy::await_holding_refcell_ref)]
#![allow(clippy::collapsible_else_if)]
#![warn(anonymous_parameters, bad_style, missing_docs)]
#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)]
#![warn(unsafe_code)]
use async_channel::{Receiver, Sender, TryRecvError};
use async_trait::async_trait;
use crossterm::{cursor, event, style, terminal, tty::IsTty, QueueableCommand};
use endbasic_core::exec::Signal;
use endbasic_std::console::{
get_env_var_as_u16, read_key_from_stdin, remove_control_chars, CharsXY, ClearType, Console, Key,
};
use std::cmp::Ordering;
use std::collections::VecDeque;
use std::io::{self, StdoutLock, Write};
fn crossterm_error_to_io_error(e: crossterm::ErrorKind) -> io::Error {
match e {
crossterm::ErrorKind::IoError(e) => e,
crossterm::ErrorKind::Utf8Error(e) => {
io::Error::new(io::ErrorKind::InvalidData, format!("{}", e))
}
_ => io::Error::new(io::ErrorKind::Other, format!("{}", e)),
}
}
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) {
if self.is_tty {
terminal::disable_raw_mode().unwrap();
}
}
}
impl TerminalConsole {
pub fn from_stdio(signals_tx: Sender<Signal>) -> io::Result<Self> {
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().map_err(crossterm_error_to_io_error)?;
tokio::task::spawn(TerminalConsole::raw_key_handler(on_key_tx, signals_tx));
} else {
tokio::task::spawn(TerminalConsole::stdio_key_handler(on_key_tx));
}
Ok(Self {
is_tty,
fg_color: None,
bg_color: None,
cursor_visible: true,
alt_active: false,
sync_enabled: true,
on_key_rx,
})
}
async fn raw_key_handler(on_key_tx: Sender<Key>, signals_tx: Sender<Signal>) {
use event::{KeyCode, KeyModifiers};
let mut done = false;
while !done {
let key = match event::read().map_err(crossterm_error_to_io_error) {
Ok(event::Event::Key(ev)) => match ev.code {
KeyCode::Backspace => Key::Backspace,
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::Eof,
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(format!("{:?}", ev)),
},
Ok(_) => {
continue;
}
Err(e) => {
Key::Unknown(format!("{:?}", e))
}
};
done = key == Key::Eof;
if key == Key::Interrupt {
signals_tx
.send(Signal::Break)
.await
.expect("Send to unbounded channel should not have failed")
}
let _ = on_key_tx.send(key).await;
}
signals_tx.close();
on_key_tx.close();
}
async fn stdio_key_handler(on_key_tx: Sender<Key>) {
let mut buffer = VecDeque::default();
let mut done = false;
while !done {
let key = match read_key_from_stdin(&mut buffer) {
Ok(key) => key,
Err(e) => {
Key::Unknown(format!("{:?}", e))
}
};
done = key == Key::Eof;
let _ = on_key_tx.send(key).await;
}
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 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)).map_err(crossterm_error_to_io_error)?;
if how == terminal::ClearType::All {
stdout.queue(cursor::MoveTo(0, 0)).map_err(crossterm_error_to_io_error)?;
}
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)).map_err(crossterm_error_to_io_error)?;
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)).map_err(crossterm_error_to_io_error)?;
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).map_err(crossterm_error_to_io_error)?;
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).map_err(crossterm_error_to_io_error)?;
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).map_err(crossterm_error_to_io_error)?;
self.alt_active = false;
self.maybe_flush(stdout)
} else {
Ok(())
}
}
fn locate(&mut self, pos: CharsXY) -> io::Result<()> {
#[cfg(not(release))]
{
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)).map_err(crossterm_error_to_io_error)?;
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)),
}
.map_err(crossterm_error_to_io_error)?;
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>> {
match self.on_key_rx.try_recv() {
Ok(k) => Ok(Some(k)),
Err(TryRecvError::Empty) => Ok(None),
Err(TryRecvError::Closed) => Ok(Some(Key::Eof)),
}
}
async fn read_key(&mut self) -> io::Result<Key> {
match self.on_key_rx.recv().await {
Ok(k) => Ok(k),
Err(_) => Ok(Key::Eof),
}
}
fn show_cursor(&mut self) -> io::Result<()> {
if !self.cursor_visible {
let stdout = io::stdout();
let mut stdout = stdout.lock();
stdout.queue(cursor::Show).map_err(crossterm_error_to_io_error)?;
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().map_err(crossterm_error_to_io_error)?;
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)
}
}