use std::{
fmt,
fmt::{Display, Formatter},
fs::File,
io,
io::Write,
result,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread,
};
use crossbeam_channel::{select, unbounded, Receiver};
use termion::{
clear, color, cursor, get_tty,
input::TermRead,
raw::{IntoRawMode, RawTerminal},
screen, style, terminal_size,
};
use crate::{
backend::{resize, termion::cursor::position, Backend},
error,
error::ErrorKind,
Action, Attribute, Clear, Color, Event, Retrieved, Value,
};
const ENABLE_MOUSE_CAPTURE: &str = "\x1B[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h";
const DISABLE_MOUSE_CAPTURE: &str = "\x1B[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l";
struct ColorCodeWriter<T: color::Color> {
color: T,
is_fg: bool,
}
impl<T: color::Color> ColorCodeWriter<T> {
pub fn new(color: T, is_fg: bool) -> ColorCodeWriter<T> {
ColorCodeWriter { color, is_fg }
}
}
impl<T: color::Color> Display for ColorCodeWriter<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.is_fg {
self.color.write_fg(f)
} else {
self.color.write_bg(f)
}
}
}
pub struct BackendImpl<W: Write> {
raw_buffer: Option<Box<RawTerminal<File>>>,
buffer: W,
input_receiver: Option<Receiver<Event>>,
resize_receiver: Option<Receiver<()>>,
is_raw_mode_enabled: bool,
}
impl<W: Write> BackendImpl<W> {
pub fn w_color<T: color::Color>(&mut self, color: T, is_fg: bool) -> io::Result<()> {
if let Some(ref mut terminal) = self.raw_buffer {
write!(terminal, "{}", ColorCodeWriter::new(color, is_fg))
} else {
write!(self.buffer, "{}", ColorCodeWriter::new(color, is_fg))
}
}
pub fn f_color(&mut self, color: Color, is_fg: bool) -> io::Result<()> {
match color {
Color::Reset => self.w_color(color::Reset, is_fg),
Color::Black => self.w_color(color::Black, is_fg),
Color::DarkGrey => self.w_color(color::Black, is_fg),
Color::Red => self.w_color(color::LightRed, is_fg),
Color::DarkRed => self.w_color(color::Red, is_fg),
Color::Green => self.w_color(color::LightGreen, is_fg),
Color::DarkGreen => self.w_color(color::Green, is_fg),
Color::Yellow => self.w_color(color::LightYellow, is_fg),
Color::DarkYellow => self.w_color(color::Yellow, is_fg),
Color::Blue => self.w_color(color::LightBlue, is_fg),
Color::DarkBlue => self.w_color(color::Blue, is_fg),
Color::Magenta => self.w_color(color::LightMagenta, is_fg),
Color::DarkMagenta => self.w_color(color::Magenta, is_fg),
Color::Cyan => self.w_color(color::LightCyan, is_fg),
Color::DarkCyan => self.w_color(color::Cyan, is_fg),
Color::White => self.w_color(color::White, is_fg),
Color::Grey => self.w_color(color::LightWhite, is_fg),
Color::Rgb(r, g, b) => self.w_color(color::Rgb(r, g, b), is_fg),
Color::AnsiValue(val) => self.w_color(color::AnsiValue(val), is_fg),
}
}
pub fn w_display(&mut self, displayable: &dyn Display) -> io::Result<()> {
if let Some(ref mut terminal) = self.raw_buffer {
write!(terminal, "{}", displayable)
} else {
write!(self.buffer, "{}", displayable)
}
}
pub fn f_attribute(&mut self, attribute: Attribute) -> error::Result<()> {
match attribute {
Attribute::SlowBlink => self.w_display(&style::Blink)?,
Attribute::RapidBlink => self.w_display(&style::Blink)?,
Attribute::BlinkOff => self.w_display(&style::NoBlink)?,
Attribute::Bold => self.w_display(&style::Bold)?,
Attribute::BoldOff => self.w_display(&style::NoBold)?,
Attribute::Crossed => self.w_display(&style::CrossedOut)?,
Attribute::CrossedOff => self.w_display(&style::NoCrossedOut)?,
Attribute::BoldItalicOff => self.w_display(&style::Faint)?,
Attribute::Framed => self.w_display(&style::Framed)?,
Attribute::Reversed => self.w_display(&style::Invert)?,
Attribute::ReversedOff => self.w_display(&style::NoInvert)?,
Attribute::Italic => self.w_display(&style::Italic)?,
Attribute::ItalicOff => self.w_display(&style::NoItalic)?,
Attribute::Underlined => self.w_display(&style::Underline)?,
Attribute::UnderlinedOff => self.w_display(&style::NoUnderline)?,
Attribute::Reset => self.w_display(&style::Reset)?,
_ => {
return Err(error::ErrorKind::AttributeNotSupported(String::from(
attribute,
)));
}
};
Ok(())
}
}
impl<W: Write> Backend<W> for BackendImpl<W> {
fn create(buffer: W) -> Self {
let (input_sender, input_receiver) = unbounded::<Event>();
let (resize_sender, resize_receiver) = unbounded();
let running = Arc::new(AtomicBool::new(true));
#[cfg(unix)]
resize::start_resize_thread(resize_sender, Arc::clone(&running));
thread::spawn(move || {
let input = termion::get_tty().unwrap();
let mut events = input.events();
while let Some(Ok(event)) = events.next() {
if input_sender.send(Event::from(event)).is_err() {
break;
}
}
running.store(false, Ordering::Relaxed);
});
BackendImpl {
raw_buffer: None,
buffer,
resize_receiver: Some(resize_receiver),
input_receiver: Some(input_receiver),
is_raw_mode_enabled: false,
}
}
fn act(&mut self, action: Action) -> error::Result<()> {
self.batch(action)?;
self.flush_batch()
}
#[allow(clippy::cognitive_complexity)]
fn batch(&mut self, action: Action) -> error::Result<()> {
match action {
Action::MoveCursorTo(column, row) => {
self.w_display(&cursor::Goto(column + 1, row + 1))?
}
Action::HideCursor => self.w_display(&cursor::Hide)?,
Action::ShowCursor => self.w_display(&cursor::Show)?,
Action::ClearTerminal(clear_type) => match clear_type {
Clear::All => {
self.w_display(&clear::All)?;
}
Clear::FromCursorDown => self.w_display(&clear::AfterCursor)?,
Clear::FromCursorUp => self.w_display(&clear::BeforeCursor)?,
Clear::CurrentLine => self.w_display(&clear::CurrentLine)?,
Clear::UntilNewLine => self.w_display(&clear::UntilNewline)?,
},
Action::EnterAlternateScreen => self.w_display(&screen::ToAlternateScreen)?,
Action::LeaveAlternateScreen => self.w_display(&screen::ToMainScreen)?,
Action::SetForegroundColor(color) => self.f_color(color, true)?,
Action::SetBackgroundColor(color) => self.f_color(color, false)?,
Action::SetAttribute(attr) => self.f_attribute(attr)?,
Action::ResetColor => self.w_display(&format!(
"{}{}",
color::Reset.fg_str(),
color::Reset.bg_str()
))?,
Action::EnableRawMode => {
self.raw_buffer = Some(Box::new(termion::get_tty()?.into_raw_mode().unwrap()));
self.is_raw_mode_enabled = true;
}
Action::DisableRawMode => {
if self.raw_buffer.is_some() {
self.raw_buffer = None;
self.is_raw_mode_enabled = false;
}
}
Action::EnableMouseCapture => {
self.buffer.write_all(ENABLE_MOUSE_CAPTURE.as_bytes())?;
}
Action::DisableMouseCapture => {
self.buffer.write_all(DISABLE_MOUSE_CAPTURE.as_bytes())?;
}
Action::SetTerminalSize(..)
| Action::EnableBlinking
| Action::DisableBlinking
| Action::ScrollUp(_)
| Action::ScrollDown(_) => {
return Err(error::ErrorKind::ActionNotSupported(String::from(action)))
}
};
self.flush_batch()
}
fn flush_batch(&mut self) -> error::Result<()> {
self.buffer
.flush()
.map_err(|_| ErrorKind::FlushingBatchFailed)
}
fn get(&self, retrieve_operation: Value) -> error::Result<Retrieved> {
Ok(match retrieve_operation {
Value::TerminalSize => {
let size = terminal_size()?;
Retrieved::TerminalSize(size.0, size.1)
}
Value::CursorPosition => {
let (x, y) = if self.is_raw_mode_enabled {
position()?
} else {
get_tty()?.into_raw_mode()?;
position()?
};
Retrieved::CursorPosition(x, y)
}
Value::Event(duration) => {
if let Some(ref input_receiver) = self.input_receiver {
if let Some(ref resize_receiver) = self.resize_receiver {
let event = if let Some(duration) = duration {
select! {
recv(input_receiver) -> event => event.ok(),
recv(resize_receiver) -> _ => Some(Event::Resize),
default(duration) => None,
}
} else {
select! {
recv(input_receiver) -> event => event.ok(),
recv(resize_receiver) -> _ => Some(Event::Resize),
}
};
return Ok(event.map_or(Retrieved::Event(None), |event| {
Retrieved::Event(Some(event))
}));
};
};
Retrieved::Event(None)
}
})
}
}
impl<W: Write> Write for BackendImpl<W> {
fn write(&mut self, buf: &[u8]) -> result::Result<usize, io::Error> {
self.buffer.write(buf)
}
fn flush(&mut self) -> result::Result<(), io::Error> {
self.buffer.flush()
}
}