aboutsummaryrefslogtreecommitdiffstats
path: root/src/ratatui/backend
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@elliehuxtable.com>2023-03-31 22:57:37 +0100
committerGitHub <noreply@github.com>2023-03-31 22:57:37 +0100
commita515b06bcb556c1be2d0fc3095cd778d413fe40d (patch)
tree4b544de9aa53d6976177c08b91aa3943ef4d9e92 /src/ratatui/backend
parentfeat: add github action to test the nix builds (#833) (diff)
downloadatuin-a515b06bcb556c1be2d0fc3095cd778d413fe40d.zip
Vendor ratatui temporarily (#835)
* Vendor ratatui temporarily Once https://github.com/tui-rs-revival/ratatui/pull/114 has been merged, we can undo this! But otherwise we can't publish to crates.io with a git dependency. * make tests pass * Shush. * these literally just fail in nix, nowhere else idk how to work with nix properly, and they're also not our tests
Diffstat (limited to 'src/ratatui/backend')
-rw-r--r--src/ratatui/backend/crossterm.rs241
-rw-r--r--src/ratatui/backend/mod.rs58
-rw-r--r--src/ratatui/backend/termion.rs275
3 files changed, 574 insertions, 0 deletions
diff --git a/src/ratatui/backend/crossterm.rs b/src/ratatui/backend/crossterm.rs
new file mode 100644
index 00000000..3dceb6ad
--- /dev/null
+++ b/src/ratatui/backend/crossterm.rs
@@ -0,0 +1,241 @@
+use crate::ratatui::{
+ backend::{Backend, ClearType},
+ buffer::Cell,
+ layout::Rect,
+ style::{Color, Modifier},
+};
+use crossterm::{
+ cursor::{Hide, MoveTo, Show},
+ execute, queue,
+ style::{
+ Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
+ SetForegroundColor,
+ },
+ terminal::{self, Clear},
+};
+use std::io::{self, Write};
+
+pub struct CrosstermBackend<W: Write> {
+ buffer: W,
+}
+
+impl<W> CrosstermBackend<W>
+where
+ W: Write,
+{
+ pub fn new(buffer: W) -> CrosstermBackend<W> {
+ CrosstermBackend { buffer }
+ }
+}
+
+impl<W> Write for CrosstermBackend<W>
+where
+ W: Write,
+{
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ self.buffer.write(buf)
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ self.buffer.flush()
+ }
+}
+
+impl<W> Backend for CrosstermBackend<W>
+where
+ W: Write,
+{
+ fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
+ where
+ I: Iterator<Item = (u16, u16, &'a Cell)>,
+ {
+ 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) {
+ map_error(queue!(self.buffer, MoveTo(x, y)))?;
+ }
+ last_pos = Some((x, y));
+ if cell.modifier != modifier {
+ let diff = ModifierDiff {
+ from: modifier,
+ to: cell.modifier,
+ };
+ diff.queue(&mut self.buffer)?;
+ modifier = cell.modifier;
+ }
+ if cell.fg != fg {
+ let color = CColor::from(cell.fg);
+ map_error(queue!(self.buffer, SetForegroundColor(color)))?;
+ fg = cell.fg;
+ }
+ if cell.bg != bg {
+ let color = CColor::from(cell.bg);
+ map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
+ bg = cell.bg;
+ }
+
+ map_error(queue!(self.buffer, Print(&cell.symbol)))?;
+ }
+
+ map_error(queue!(
+ self.buffer,
+ SetForegroundColor(CColor::Reset),
+ SetBackgroundColor(CColor::Reset),
+ SetAttribute(CAttribute::Reset)
+ ))
+ }
+
+ fn hide_cursor(&mut self) -> io::Result<()> {
+ map_error(execute!(self.buffer, Hide))
+ }
+
+ fn show_cursor(&mut self) -> io::Result<()> {
+ map_error(execute!(self.buffer, Show))
+ }
+
+ fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
+ crossterm::cursor::position()
+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
+ }
+
+ fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
+ map_error(execute!(self.buffer, MoveTo(x, y)))
+ }
+
+ fn clear(&mut self) -> io::Result<()> {
+ self.clear_region(ClearType::All)
+ }
+
+ fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
+ map_error(execute!(
+ self.buffer,
+ Clear(match clear_type {
+ ClearType::All => crossterm::terminal::ClearType::All,
+ ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
+ ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
+ ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
+ ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
+ })
+ ))
+ }
+
+ fn append_lines(&mut self, n: u16) -> io::Result<()> {
+ for _ in 0..n {
+ map_error(queue!(self.buffer, Print("\n")))?;
+ }
+ self.buffer.flush()
+ }
+
+ fn size(&self) -> io::Result<Rect> {
+ let (width, height) =
+ terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
+
+ Ok(Rect::new(0, 0, width, height))
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ self.buffer.flush()
+ }
+}
+
+fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
+ error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
+}
+
+impl From<Color> for CColor {
+ fn from(color: Color) -> Self {
+ match color {
+ Color::Reset => CColor::Reset,
+ Color::Black => CColor::Black,
+ Color::Red => CColor::DarkRed,
+ Color::Green => CColor::DarkGreen,
+ Color::Yellow => CColor::DarkYellow,
+ Color::Blue => CColor::DarkBlue,
+ Color::Magenta => CColor::DarkMagenta,
+ Color::Cyan => CColor::DarkCyan,
+ Color::Gray => CColor::Grey,
+ Color::DarkGray => CColor::DarkGrey,
+ Color::LightRed => CColor::Red,
+ Color::LightGreen => CColor::Green,
+ Color::LightBlue => CColor::Blue,
+ Color::LightYellow => CColor::Yellow,
+ Color::LightMagenta => CColor::Magenta,
+ Color::LightCyan => CColor::Cyan,
+ Color::White => CColor::White,
+ Color::Indexed(i) => CColor::AnsiValue(i),
+ Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
+ }
+ }
+}
+
+#[derive(Debug)]
+struct ModifierDiff {
+ pub from: Modifier,
+ pub to: Modifier,
+}
+
+impl ModifierDiff {
+ fn queue<W>(&self, mut w: W) -> io::Result<()>
+ where
+ W: io::Write,
+ {
+ //use crossterm::Attribute;
+ let removed = self.from - self.to;
+ if removed.contains(Modifier::REVERSED) {
+ map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
+ }
+ if removed.contains(Modifier::BOLD) {
+ map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
+ if self.to.contains(Modifier::DIM) {
+ map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
+ }
+ }
+ if removed.contains(Modifier::ITALIC) {
+ map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
+ }
+ if removed.contains(Modifier::UNDERLINED) {
+ map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
+ }
+ if removed.contains(Modifier::DIM) {
+ map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
+ }
+ if removed.contains(Modifier::CROSSED_OUT) {
+ map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
+ }
+ if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
+ map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
+ }
+
+ let added = self.to - self.from;
+ if added.contains(Modifier::REVERSED) {
+ map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
+ }
+ if added.contains(Modifier::BOLD) {
+ map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
+ }
+ if added.contains(Modifier::ITALIC) {
+ map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
+ }
+ if added.contains(Modifier::UNDERLINED) {
+ map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
+ }
+ if added.contains(Modifier::DIM) {
+ map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
+ }
+ if added.contains(Modifier::CROSSED_OUT) {
+ map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
+ }
+ if added.contains(Modifier::SLOW_BLINK) {
+ map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
+ }
+ if added.contains(Modifier::RAPID_BLINK) {
+ map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
+ }
+
+ Ok(())
+ }
+}
diff --git a/src/ratatui/backend/mod.rs b/src/ratatui/backend/mod.rs
new file mode 100644
index 00000000..a360db18
--- /dev/null
+++ b/src/ratatui/backend/mod.rs
@@ -0,0 +1,58 @@
+use std::io;
+
+use crate::ratatui::buffer::Cell;
+use crate::ratatui::layout::Rect;
+
+#[cfg(feature = "termion")]
+mod termion;
+#[cfg(feature = "termion")]
+pub use self::termion::TermionBackend;
+
+mod crossterm;
+pub use self::crossterm::CrosstermBackend;
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum ClearType {
+ All,
+ AfterCursor,
+ BeforeCursor,
+ CurrentLine,
+ UntilNewLine,
+}
+
+pub trait Backend {
+ fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
+ where
+ I: Iterator<Item = (u16, u16, &'a Cell)>;
+
+ /// Insert `n` line breaks to the terminal screen
+ fn append_lines(&mut self, n: u16) -> io::Result<()> {
+ // to get around the unused warning
+ let _n = n;
+ Ok(())
+ }
+
+ fn hide_cursor(&mut self) -> Result<(), io::Error>;
+ fn show_cursor(&mut self) -> Result<(), io::Error>;
+ fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
+ fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
+
+ /// Clears the whole terminal screen
+ fn clear(&mut self) -> Result<(), io::Error>;
+
+ /// Clears a specific region of the terminal specified by the [`ClearType`] parameter
+ fn clear_region(&mut self, clear_type: ClearType) -> Result<(), io::Error> {
+ match clear_type {
+ ClearType::All => self.clear(),
+ ClearType::AfterCursor
+ | ClearType::BeforeCursor
+ | ClearType::CurrentLine
+ | ClearType::UntilNewLine => Err(io::Error::new(
+ io::ErrorKind::Other,
+ format!("clear_type [{clear_type:?}] not supported with this backend"),
+ )),
+ }
+ }
+ fn size(&self) -> Result<Rect, io::Error>;
+ fn flush(&mut self) -> Result<(), io::Error>;
+}
diff --git a/src/ratatui/backend/termion.rs b/src/ratatui/backend/termion.rs
new file mode 100644
index 00000000..76def792
--- /dev/null
+++ b/src/ratatui/backend/termion.rs
@@ -0,0 +1,275 @@
+use crate::{
+ backend::{Backend, ClearType},
+ buffer::Cell,
+ layout::Rect,
+ style::{Color, Modifier},
+};
+use std::{
+ fmt,
+ io::{self, Write},
+};
+
+pub struct TermionBackend<W>
+where
+ W: Write,
+{
+ stdout: W,
+}
+
+impl<W> TermionBackend<W>
+where
+ W: Write,
+{
+ pub fn new(stdout: W) -> TermionBackend<W> {
+ TermionBackend { stdout }
+ }
+}
+
+impl<W> Write for TermionBackend<W>
+where
+ W: Write,
+{
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ self.stdout.write(buf)
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ self.stdout.flush()
+ }
+}
+
+impl<W> Backend for TermionBackend<W>
+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<Item = (u16, u16, &'a Cell)>,
+ {
+ 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<Rect> {
+ 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(())
+ }
+}