From 9bc61551e2ea328f4e9ceb9ce58847da3a1aa424 Mon Sep 17 00:00:00 2001 From: Florian Dehau Date: Sat, 5 Nov 2016 19:18:48 +0100 Subject: [PATCH] Refactor Terminal to be able to support multiple backends * Add Rustbox as an other possible backend --- Cargo.toml | 1 + src/layout.rs | 7 +- src/lib.rs | 3 +- src/style.rs | 28 ++++- src/terminal.rs | 251 ++++++++++++++++++++++++++++++++------------- src/widgets/mod.rs | 7 +- 6 files changed, 218 insertions(+), 79 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9dab6d9..81c1b71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Florian Dehau "] [dependencies] termion = "1.1.1" +rustbox = "0.9.0" bitflags = "0.7" cassowary = "0.2.0" log = "0.3" diff --git a/src/layout.rs b/src/layout.rs index c5590b8..4540a19 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -5,7 +5,7 @@ use cassowary::{Solver, Variable, Expression, Constraint}; use cassowary::WeightedRelation::*; use cassowary::strength::{REQUIRED, WEAK}; -use terminal::Terminal; +use terminal::{Terminal, Backend}; #[derive(Debug, Hash, PartialEq)] pub enum Direction { @@ -306,8 +306,9 @@ impl Group { self.sizes = Vec::from(sizes); self } - pub fn render(&self, t: &mut Terminal, area: &Rect, mut f: F) - where F: FnMut(&mut Terminal, &[Rect]) + pub fn render(&self, t: &mut Terminal, area: &Rect, mut f: F) + where B: Backend, + F: FnMut(&mut Terminal, &[Rect]) { let chunks = t.compute_layout(self, area); f(t, &chunks); diff --git a/src/lib.rs b/src/lib.rs index 013b120..17e0291 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ extern crate termion; +extern crate rustbox; #[macro_use] extern crate bitflags; #[macro_use] @@ -15,4 +16,4 @@ pub mod widgets; pub mod style; pub mod layout; -pub use self::terminal::Terminal; +pub use self::terminal::{Terminal, Backend, TermionBackend, RustboxBackend}; diff --git a/src/style.rs b/src/style.rs index 3efe778..b4e0b8d 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,4 +1,5 @@ use termion; +use rustbox; #[derive(Debug, Clone, Copy, PartialEq)] pub enum Color { @@ -37,7 +38,7 @@ macro_rules! termion_bg_rgb { } impl Color { - pub fn fg(&self) -> String { + pub fn termion_fg(&self) -> String { match *self { Color::Reset => termion_fg!(Reset), Color::Black => termion_fg!(Black), @@ -57,7 +58,7 @@ impl Color { Color::Rgb(r, g, b) => termion_fg_rgb!(r, g, b), } } - pub fn bg(&self) -> String { + pub fn termion_bg(&self) -> String { match *self { Color::Reset => termion_bg!(Reset), Color::Black => termion_bg!(Black), @@ -78,3 +79,26 @@ impl Color { } } } + +impl Into for Color { + fn into(self) -> rustbox::Color { + match self { + Color::Reset => rustbox::Color::Default, + Color::Black => rustbox::Color::Black, + Color::Red => rustbox::Color::Red, + Color::Green => rustbox::Color::Green, + Color::Yellow => rustbox::Color::Yellow, + Color::Magenta => rustbox::Color::Magenta, + Color::Cyan => rustbox::Color::Cyan, + Color::Gray => rustbox::Color::Black, + Color::DarkGray => rustbox::Color::Black, + Color::LightRed => rustbox::Color::Red, + Color::LightGreen => rustbox::Color::Green, + Color::LightYellow => rustbox::Color::Yellow, + Color::LightMagenta => rustbox::Color::Magenta, + Color::LightCyan => rustbox::Color::Cyan, + Color::White => rustbox::Color::White, + Color::Rgb(r, g, b) => rustbox::Color::Default, + } + } +} diff --git a/src/terminal.rs b/src/terminal.rs index defa737..8be6edd 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -5,12 +5,165 @@ use std::collections::HashMap; use termion; use termion::raw::{IntoRawMode, RawTerminal}; -use buffer::Buffer; +use rustbox; + +use buffer::{Buffer, Cell}; use layout::{Rect, Group, split}; use widgets::Widget; use style::Color; use util::hash; +pub trait Backend { + fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error> + where I: Iterator; + fn hide_cursor(&mut self) -> Result<(), io::Error>; + fn show_cursor(&mut self) -> Result<(), io::Error>; + fn clear(&mut self) -> Result<(), io::Error>; + fn size(&self) -> Result; + fn flush(&mut self) -> Result<(), io::Error>; +} + +pub struct TermionBackend { + stdout: RawTerminal, +} + +impl TermionBackend { + pub fn new() -> Result { + let stdout = try!(io::stdout().into_raw_mode()); + Ok(TermionBackend { stdout: stdout }) + } +} + +impl Backend for TermionBackend { + /// Clears the entire screen and move the cursor to the top left of the screen + fn clear(&mut self) -> Result<(), io::Error> { + try!(write!(self.stdout, "{}", termion::clear::All)); + try!(write!(self.stdout, "{}", termion::cursor::Goto(1, 1))); + try!(self.stdout.flush()); + Ok(()) + } + + /// Hides cursor + fn hide_cursor(&mut self) -> Result<(), io::Error> { + try!(write!(self.stdout, "{}", termion::cursor::Hide)); + try!(self.stdout.flush()); + Ok(()) + } + + /// Shows cursor + fn show_cursor(&mut self) -> Result<(), io::Error> { + try!(write!(self.stdout, "{}", termion::cursor::Show)); + try!(self.stdout.flush()); + Ok(()) + } + + fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error> + where I: Iterator + { + let mut string = String::with_capacity(content.size_hint().0 * 3); + let mut fg = Color::Reset; + let mut bg = Color::Reset; + let mut last_y = 0; + let mut last_x = 0; + let mut inst = 0; + for (x, y, cell) in content { + if y != last_y || x != last_x + 1 { + string.push_str(&format!("{}", termion::cursor::Goto(x + 1, y + 1))); + inst += 1; + } + last_x = x; + last_y = y; + if cell.fg != fg { + string.push_str(&cell.fg.termion_fg()); + fg = cell.fg; + inst += 1; + } + if cell.bg != bg { + string.push_str(&cell.bg.termion_bg()); + bg = cell.bg; + inst += 1; + } + string.push_str(&cell.symbol); + inst += 1; + } + debug!("{} instructions outputed.", inst); + try!(write!(self.stdout, + "{}{}{}", + string, + Color::Reset.termion_fg(), + Color::Reset.termion_bg())); + Ok(()) + } + + /// Return the size of the terminal + fn size(&self) -> Result { + let terminal = try!(termion::terminal_size()); + Ok(Rect { + x: 0, + y: 0, + width: terminal.0, + height: terminal.1, + }) + } + + fn flush(&mut self) -> Result<(), io::Error> { + try!(self.stdout.flush()); + Ok(()) + } +} + +pub struct RustboxBackend { + rustbox: rustbox::RustBox, +} + +impl RustboxBackend { + pub fn new() -> Result { + let rustbox = try!(rustbox::RustBox::init(Default::default())); + Ok(RustboxBackend { rustbox: rustbox }) + } +} + +impl Backend for RustboxBackend { + fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error> + where I: Iterator + { + let mut inst = 0; + for (x, y, cell) in content { + inst += 1; + self.rustbox.print(x as usize, + y as usize, + rustbox::RB_NORMAL, + cell.fg.into(), + cell.bg.into(), + &cell.symbol); + } + debug!("{} instructions outputed", inst); + Ok(()) + } + fn hide_cursor(&mut self) -> Result<(), io::Error> { + Ok(()) + } + fn show_cursor(&mut self) -> Result<(), io::Error> { + Ok(()) + } + fn clear(&mut self) -> Result<(), io::Error> { + self.rustbox.clear(); + Ok(()) + } + fn size(&self) -> Result { + Ok((Rect { + x: 0, + y: 0, + width: self.rustbox.width() as u16, + height: self.rustbox.height() as u16, + })) + } + fn flush(&mut self) -> Result<(), io::Error> { + self.rustbox.present(); + Ok(()) + } +} + /// Holds a computed layout and keeps track of its use between successive draw calls #[derive(Debug)] pub struct LayoutEntry { @@ -19,9 +172,10 @@ pub struct LayoutEntry { } /// Interface to the terminal backed by Termion -pub struct Terminal { - /// Raw mode termion terminal - stdout: RawTerminal, +pub struct Terminal + where B: Backend +{ + backend: B, /// Cache to prevent the layout to be computed at each draw call layout_cache: HashMap, /// Holds the results of the current and previous draw calls. The two are compared at the end @@ -31,29 +185,27 @@ pub struct Terminal { current: usize, } -impl Terminal { +impl Terminal + where B: Backend +{ /// Wrapper around Termion initialization. Each buffer is initialized with a blank string and /// default colors for the foreground and the background - pub fn new() -> Result { - let stdout = try!(io::stdout().into_raw_mode()); - let size = try!(Terminal::size()); + pub fn new(backend: B) -> Result, io::Error> { + let size = try!(backend.size()); Ok(Terminal { - stdout: stdout, + backend: backend, layout_cache: HashMap::new(), buffers: [Buffer::empty(size), Buffer::empty(size)], current: 0, }) } - /// Return the size of the terminal - pub fn size() -> Result { - let terminal = try!(termion::terminal_size()); - Ok(Rect { - x: 0, - y: 0, - width: terminal.0, - height: terminal.1, - }) + pub fn backend(&self) -> &B { + &self.backend + } + + pub fn backend_mut(&mut self) -> &mut B { + &mut self.backend } /// Check if we have already computed a layout for a given group, otherwise it creates one and @@ -82,11 +234,6 @@ impl Terminal { /// update the UI and writes it to stdout. pub fn flush(&mut self) -> Result<(), io::Error> { let width = self.buffers[self.current].area.width; - let mut string = String::with_capacity(self.buffers[self.current].content.len() * 3); - let mut fg = Color::Reset; - let mut bg = Color::Reset; - let mut last_y = 0; - let mut last_x = 0; let content = self.buffers[self.current] .content .iter() @@ -100,34 +247,7 @@ impl Terminal { } else { None }); - let mut inst = 0; - for (x, y, cell) in content { - if y != last_y || x != last_x + 1 { - string.push_str(&format!("{}", termion::cursor::Goto(x + 1, y + 1))); - inst += 1; - } - last_x = x; - last_y = y; - if cell.fg != fg { - string.push_str(&cell.fg.fg()); - fg = cell.fg; - inst += 1; - } - if cell.bg != bg { - string.push_str(&cell.bg.bg()); - bg = cell.bg; - inst += 1; - } - string.push_str(&cell.symbol); - inst += 1; - } - debug!("{} instructions outputed.", inst); - try!(write!(self.stdout, - "{}{}{}", - string, - Color::Reset.fg(), - Color::Reset.bg())); - Ok(()) + self.backend.draw(content) } /// Calls the draw method of a given widget on the current buffer @@ -144,7 +264,7 @@ impl Terminal { self.buffers[1 - self.current].resize(area); self.buffers[1 - self.current].reset(); self.layout_cache.clear(); - try!(self.clear()); + try!(self.backend.clear()); Ok(()) } @@ -173,29 +293,20 @@ impl Terminal { self.current = 1 - self.current; // Flush - try!(self.stdout.flush()); + try!(self.backend.flush()); Ok(()) } - /// Clears the entire screen and move the cursor to the top left of the screen - pub fn clear(&mut self) -> Result<(), io::Error> { - try!(write!(self.stdout, "{}", termion::clear::All)); - try!(write!(self.stdout, "{}", termion::cursor::Goto(1, 1))); - try!(self.stdout.flush()); - Ok(()) - } - - /// Hides cursor pub fn hide_cursor(&mut self) -> Result<(), io::Error> { - try!(write!(self.stdout, "{}", termion::cursor::Hide)); - try!(self.stdout.flush()); - Ok(()) + self.backend.hide_cursor() } - - /// Shows cursor pub fn show_cursor(&mut self) -> Result<(), io::Error> { - try!(write!(self.stdout, "{}", termion::cursor::Show)); - try!(self.stdout.flush()); - Ok(()) + self.backend.show_cursor() + } + pub fn clear(&mut self) -> Result<(), io::Error> { + self.backend.clear() + } + pub fn size(&self) -> Result { + self.backend.size() } } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index b29bb77..49e22a1 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -21,7 +21,7 @@ pub use self::table::Table; use buffer::Buffer; use layout::Rect; -use terminal::Terminal; +use terminal::{Backend, Terminal}; use style::Color; /// Bitflags that can be composed to set the visible borders essentially on the block widget. @@ -58,8 +58,9 @@ pub trait Widget { } } /// Helper method that can be chained with a widget's builder methods to render it. - fn render(&self, area: &Rect, t: &mut Terminal) - where Self: Sized + fn render(&self, area: &Rect, t: &mut Terminal) + where Self: Sized, + B: Backend { t.render(self, area); }