use crate::{ backend::{Backend, ClearType}, buffer::Cell, layout::Rect, style::{Color, Modifier}, }; use std::{ fmt, io::{self, Write}, }; pub struct TermionBackend where W: Write, { stdout: W, } impl TermionBackend where W: Write, { pub fn new(stdout: W) -> TermionBackend { TermionBackend { stdout } } } impl Write for TermionBackend where W: Write, { fn write(&mut self, buf: &[u8]) -> io::Result { self.stdout.write(buf) } fn flush(&mut self) -> io::Result<()> { self.stdout.flush() } } impl Backend for TermionBackend where W: Write, { fn clear(&mut self) -> io::Result<()> { self.clear_region(ClearType::All) } fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> { match clear_type { ClearType::All => write!(self.stdout, "{}", termion::clear::All)?, ClearType::AfterCursor => write!(self.stdout, "{}", termion::clear::AfterCursor)?, ClearType::BeforeCursor => write!(self.stdout, "{}", termion::clear::BeforeCursor)?, ClearType::CurrentLine => write!(self.stdout, "{}", termion::clear::CurrentLine)?, ClearType::UntilNewLine => write!(self.stdout, "{}", termion::clear::UntilNewline)?, }; self.stdout.flush() } fn append_lines(&mut self, n: u16) -> io::Result<()> { for _ in 0..n { writeln!(self.stdout)?; } self.stdout.flush() } /// Hides cursor fn hide_cursor(&mut self) -> io::Result<()> { write!(self.stdout, "{}", termion::cursor::Hide)?; self.stdout.flush() } /// Shows cursor fn show_cursor(&mut self) -> io::Result<()> { write!(self.stdout, "{}", termion::cursor::Show)?; self.stdout.flush() } /// Gets cursor position (0-based index) fn get_cursor(&mut self) -> io::Result<(u16, u16)> { termion::cursor::DetectCursorPos::cursor_pos(&mut self.stdout).map(|(x, y)| (x - 1, y - 1)) } /// Sets cursor position (0-based index) fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { write!(self.stdout, "{}", termion::cursor::Goto(x + 1, y + 1))?; self.stdout.flush() } fn draw<'a, I>(&mut self, content: I) -> io::Result<()> where I: Iterator, { use std::fmt::Write; let mut string = String::with_capacity(content.size_hint().0 * 3); let mut fg = Color::Reset; let mut bg = Color::Reset; let mut modifier = Modifier::empty(); let mut last_pos: Option<(u16, u16)> = None; for (x, y, cell) in content { // Move the cursor if the previous location was not (x - 1, y) if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) { write!(string, "{}", termion::cursor::Goto(x + 1, y + 1)).unwrap(); } last_pos = Some((x, y)); if cell.modifier != modifier { write!( string, "{}", ModifierDiff { from: modifier, to: cell.modifier } ) .unwrap(); modifier = cell.modifier; } if cell.fg != fg { write!(string, "{}", Fg(cell.fg)).unwrap(); fg = cell.fg; } if cell.bg != bg { write!(string, "{}", Bg(cell.bg)).unwrap(); bg = cell.bg; } string.push_str(&cell.symbol); } write!( self.stdout, "{}{}{}{}", string, Fg(Color::Reset), Bg(Color::Reset), termion::style::Reset, ) } /// Return the size of the terminal fn size(&self) -> io::Result { let terminal = termion::terminal_size()?; Ok(Rect::new(0, 0, terminal.0, terminal.1)) } fn flush(&mut self) -> io::Result<()> { self.stdout.flush() } } struct Fg(Color); struct Bg(Color); struct ModifierDiff { from: Modifier, to: Modifier, } impl fmt::Display for Fg { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use termion::color::Color as TermionColor; match self.0 { Color::Reset => termion::color::Reset.write_fg(f), Color::Black => termion::color::Black.write_fg(f), Color::Red => termion::color::Red.write_fg(f), Color::Green => termion::color::Green.write_fg(f), Color::Yellow => termion::color::Yellow.write_fg(f), Color::Blue => termion::color::Blue.write_fg(f), Color::Magenta => termion::color::Magenta.write_fg(f), Color::Cyan => termion::color::Cyan.write_fg(f), Color::Gray => termion::color::White.write_fg(f), Color::DarkGray => termion::color::LightBlack.write_fg(f), Color::LightRed => termion::color::LightRed.write_fg(f), Color::LightGreen => termion::color::LightGreen.write_fg(f), Color::LightBlue => termion::color::LightBlue.write_fg(f), Color::LightYellow => termion::color::LightYellow.write_fg(f), Color::LightMagenta => termion::color::LightMagenta.write_fg(f), Color::LightCyan => termion::color::LightCyan.write_fg(f), Color::White => termion::color::LightWhite.write_fg(f), Color::Indexed(i) => termion::color::AnsiValue(i).write_fg(f), Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_fg(f), } } } impl fmt::Display for Bg { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use termion::color::Color as TermionColor; match self.0 { Color::Reset => termion::color::Reset.write_bg(f), Color::Black => termion::color::Black.write_bg(f), Color::Red => termion::color::Red.write_bg(f), Color::Green => termion::color::Green.write_bg(f), Color::Yellow => termion::color::Yellow.write_bg(f), Color::Blue => termion::color::Blue.write_bg(f), Color::Magenta => termion::color::Magenta.write_bg(f), Color::Cyan => termion::color::Cyan.write_bg(f), Color::Gray => termion::color::White.write_bg(f), Color::DarkGray => termion::color::LightBlack.write_bg(f), Color::LightRed => termion::color::LightRed.write_bg(f), Color::LightGreen => termion::color::LightGreen.write_bg(f), Color::LightBlue => termion::color::LightBlue.write_bg(f), Color::LightYellow => termion::color::LightYellow.write_bg(f), Color::LightMagenta => termion::color::LightMagenta.write_bg(f), Color::LightCyan => termion::color::LightCyan.write_bg(f), Color::White => termion::color::LightWhite.write_bg(f), Color::Indexed(i) => termion::color::AnsiValue(i).write_bg(f), Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_bg(f), } } } impl fmt::Display for ModifierDiff { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let remove = self.from - self.to; if remove.contains(Modifier::REVERSED) { write!(f, "{}", termion::style::NoInvert)?; } if remove.contains(Modifier::BOLD) { // XXX: the termion NoBold flag actually enables double-underline on ECMA-48 compliant // terminals, and NoFaint additionally disables bold... so we use this trick to get // the right semantics. write!(f, "{}", termion::style::NoFaint)?; if self.to.contains(Modifier::DIM) { write!(f, "{}", termion::style::Faint)?; } } if remove.contains(Modifier::ITALIC) { write!(f, "{}", termion::style::NoItalic)?; } if remove.contains(Modifier::UNDERLINED) { write!(f, "{}", termion::style::NoUnderline)?; } if remove.contains(Modifier::DIM) { write!(f, "{}", termion::style::NoFaint)?; // XXX: the NoFaint flag additionally disables bold as well, so we need to re-enable it // here if we want it. if self.to.contains(Modifier::BOLD) { write!(f, "{}", termion::style::Bold)?; } } if remove.contains(Modifier::CROSSED_OUT) { write!(f, "{}", termion::style::NoCrossedOut)?; } if remove.contains(Modifier::SLOW_BLINK) || remove.contains(Modifier::RAPID_BLINK) { write!(f, "{}", termion::style::NoBlink)?; } let add = self.to - self.from; if add.contains(Modifier::REVERSED) { write!(f, "{}", termion::style::Invert)?; } if add.contains(Modifier::BOLD) { write!(f, "{}", termion::style::Bold)?; } if add.contains(Modifier::ITALIC) { write!(f, "{}", termion::style::Italic)?; } if add.contains(Modifier::UNDERLINED) { write!(f, "{}", termion::style::Underline)?; } if add.contains(Modifier::DIM) { write!(f, "{}", termion::style::Faint)?; } if add.contains(Modifier::CROSSED_OUT) { write!(f, "{}", termion::style::CrossedOut)?; } if add.contains(Modifier::SLOW_BLINK) || add.contains(Modifier::RAPID_BLINK) { write!(f, "{}", termion::style::Blink)?; } Ok(()) } }