feat(backend): add termwiz backend and example (#5)

* build: bump MSRV to 1.65

The latest version of the time crate requires Rust 1.65.0

```
cargo +1.64.0-x86_64-apple-darwin test --no-default-features \
  --features serde,crossterm,all-widgets --lib --tests --examples
error: package `time v0.3.21` cannot be built because it requires rustc
1.65.0 or newer, while the currently active rustc version is 1.64.0
```

* feat(backend): add termwiz backend and demo

* ci(termwiz): add termwiz to makefile.toml

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
Co-authored-by: Prabir Shrestha <mail@prabir.me>
This commit is contained in:
Orhun Parmaksız 2023-05-12 17:58:01 +02:00 committed by GitHub
parent 1cc405d2dc
commit 4437835057
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 309 additions and 0 deletions

View File

@ -32,6 +32,7 @@ unicode-segmentation = "1.10"
unicode-width = "0.1"
termion = { version = "2.0", optional = true }
crossterm = { version = "0.26", optional = true }
termwiz = { version = "0.20.0", optional = true }
serde = { version = "1", optional = true, features = ["derive"]}
time = { version = "0.3.11", optional = true, features = ["local-offset"]}

View File

@ -13,10 +13,13 @@ dependencies = [
"fmt",
"check-crossterm",
"check-termion",
"check-termwiz",
"test-crossterm",
"test-termion",
"test-termwiz",
"clippy-crossterm",
"clippy-termion",
"clippy-termwiz",
"test-doc",
]
@ -25,8 +28,11 @@ private = true
dependencies = [
"fmt",
"check-crossterm",
"check-termwiz",
"test-crossterm",
"test-termwiz",
"clippy-crossterm",
"clippy-termwiz",
"test-doc",
]
@ -47,6 +53,10 @@ run_task = "check"
env = { TUI_FEATURES = "serde,termion" }
run_task = "check"
[tasks.check-termwiz]
env = { TUI_FEATURES = "serde,termwiz" }
run_task = "check"
[tasks.check]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
@ -66,6 +76,10 @@ run_task = "build"
env = { TUI_FEATURES = "serde,termion" }
run_task = "build"
[tasks.build-termwiz]
env = { TUI_FEATURES = "serde,termwiz" }
run_task = "build"
[tasks.build]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
@ -85,6 +99,10 @@ run_task = "clippy"
env = { TUI_FEATURES = "serde,termion" }
run_task = "clippy"
[tasks.clippy-termwiz]
env = { TUI_FEATURES = "serde,termwiz" }
run_task = "clippy"
[tasks.clippy]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
@ -107,6 +125,10 @@ run_task = "test"
env = { TUI_FEATURES = "serde,termion,all-widgets" }
run_task = "test"
[tasks.test-termwiz]
env = { TUI_FEATURES = "serde,termwiz,all-widgets" }
run_task = "test"
[tasks.test]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }

View File

@ -37,6 +37,7 @@ The library supports multiple backends:
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
- [termion](https://github.com/ticki/termion)
- [termwiz](https://github.com/wez/wezterm/tree/master/termwiz)
The library is based on the principle of immediate rendering with intermediate
buffers. This means that at each new frame you should build all widgets that are

View File

@ -3,12 +3,18 @@ mod app;
mod crossterm;
#[cfg(feature = "termion")]
mod termion;
#[cfg(feature = "termwiz")]
mod termwiz;
mod ui;
#[cfg(feature = "crossterm")]
use crate::crossterm::run;
#[cfg(feature = "termion")]
use crate::termion::run;
#[cfg(feature = "termwiz")]
use crate::termwiz::run;
use argh::FromArgs;
use std::{error::Error, time::Duration};

75
examples/demo/termwiz.rs Normal file
View File

@ -0,0 +1,75 @@
use ratatui::{backend::TermwizBackend, Terminal};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use termwiz::{input::*, terminal::Terminal as TermwizTerminal};
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
let backend = TermwizBackend::new()?;
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// create app and run it
let app = App::new("Termwiz Demo", enhanced_graphics);
let res = run_app(&mut terminal, app, tick_rate);
terminal.show_cursor()?;
terminal.flush()?;
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app(
terminal: &mut Terminal<TermwizBackend>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if let Ok(Some(input)) = terminal
.backend_mut()
.buffered_terminal_mut()
.terminal()
.poll_input(Some(timeout))
{
match input {
InputEvent::Key(key_code) => match key_code.key {
KeyCode::UpArrow => app.on_up(),
KeyCode::DownArrow => app.on_down(),
KeyCode::LeftArrow => app.on_left(),
KeyCode::RightArrow => app.on_right(),
KeyCode::Char(c) => app.on_key(c),
_ => {}
},
InputEvent::Resized { cols, rows } => {
terminal
.backend_mut()
.buffered_terminal_mut()
.resize(cols, rows);
}
_ => {}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
if app.should_quit {
return Ok(());
}
}
}

View File

@ -13,6 +13,11 @@ mod crossterm;
#[cfg(feature = "crossterm")]
pub use self::crossterm::CrosstermBackend;
#[cfg(feature = "termwiz")]
mod termwiz;
#[cfg(feature = "termwiz")]
pub use self::termwiz::TermwizBackend;
mod test;
pub use self::test::TestBackend;

199
src/backend/termwiz.rs Normal file
View File

@ -0,0 +1,199 @@
use crate::{
backend::Backend,
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
};
use std::{error::Error, io};
use termwiz::{
caps::Capabilities,
cell::*,
color::*,
surface::*,
terminal::{buffered::BufferedTerminal, SystemTerminal, Terminal},
};
pub struct TermwizBackend {
buffered_terminal: BufferedTerminal<SystemTerminal>,
}
impl TermwizBackend {
pub fn new() -> Result<TermwizBackend, Box<dyn Error>> {
let mut buffered_terminal =
BufferedTerminal::new(SystemTerminal::new(Capabilities::new_from_env()?)?)?;
buffered_terminal.terminal().set_raw_mode()?;
buffered_terminal.terminal().enter_alternate_screen()?;
Ok(TermwizBackend { buffered_terminal })
}
pub fn with_buffered_terminal(instance: BufferedTerminal<SystemTerminal>) -> TermwizBackend {
TermwizBackend {
buffered_terminal: instance,
}
}
pub fn buffered_terminal(&self) -> &BufferedTerminal<SystemTerminal> {
&self.buffered_terminal
}
pub fn buffered_terminal_mut(&mut self) -> &mut BufferedTerminal<SystemTerminal> {
&mut self.buffered_terminal
}
}
impl Backend for TermwizBackend {
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
for (x, y, cell) in content {
self.buffered_terminal.add_changes(vec![
Change::CursorPosition {
x: Position::Absolute(x as usize),
y: Position::Absolute(y as usize),
},
Change::Attribute(AttributeChange::Foreground(cell.fg.into())),
Change::Attribute(AttributeChange::Background(cell.bg.into())),
]);
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Intensity(
if cell.modifier.contains(Modifier::BOLD) {
Intensity::Bold
} else if cell.modifier.contains(Modifier::DIM) {
Intensity::Half
} else {
Intensity::Normal
},
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Italic(
cell.modifier.contains(Modifier::ITALIC),
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Underline(
if cell.modifier.contains(Modifier::UNDERLINED) {
Underline::Single
} else {
Underline::None
},
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Reverse(
cell.modifier.contains(Modifier::REVERSED),
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Invisible(
cell.modifier.contains(Modifier::HIDDEN),
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::StrikeThrough(
cell.modifier.contains(Modifier::CROSSED_OUT),
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Blink(
if cell.modifier.contains(Modifier::SLOW_BLINK) {
Blink::Slow
} else if cell.modifier.contains(Modifier::RAPID_BLINK) {
Blink::Rapid
} else {
Blink::None
},
)));
self.buffered_terminal.add_change(&cell.symbol);
}
Ok(())
}
fn hide_cursor(&mut self) -> Result<(), io::Error> {
self.buffered_terminal
.add_change(Change::CursorVisibility(CursorVisibility::Hidden));
Ok(())
}
fn show_cursor(&mut self) -> Result<(), io::Error> {
self.buffered_terminal
.add_change(Change::CursorVisibility(CursorVisibility::Visible));
Ok(())
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
let (x, y) = self.buffered_terminal.cursor_position();
Ok((x as u16, y as u16))
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.buffered_terminal.add_change(Change::CursorPosition {
x: Position::Absolute(x as usize),
y: Position::Absolute(y as usize),
});
Ok(())
}
fn clear(&mut self) -> Result<(), io::Error> {
self.buffered_terminal
.add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
Ok(())
}
fn size(&self) -> Result<Rect, io::Error> {
let (term_width, term_height) = self.buffered_terminal.dimensions();
let max = u16::max_value();
Ok(Rect::new(
0,
0,
if term_width > usize::from(max) {
max
} else {
term_width as u16
},
if term_height > usize::from(max) {
max
} else {
term_height as u16
},
))
}
fn flush(&mut self) -> Result<(), io::Error> {
self.buffered_terminal
.flush()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok(())
}
}
impl From<Color> for ColorAttribute {
fn from(color: Color) -> ColorAttribute {
match color {
Color::Reset => ColorAttribute::Default,
Color::Black => AnsiColor::Black.into(),
Color::Gray | Color::DarkGray => AnsiColor::Grey.into(),
Color::Red => AnsiColor::Maroon.into(),
Color::LightRed => AnsiColor::Red.into(),
Color::Green => AnsiColor::Green.into(),
Color::LightGreen => AnsiColor::Lime.into(),
Color::Yellow => AnsiColor::Olive.into(),
Color::LightYellow => AnsiColor::Yellow.into(),
Color::Magenta => AnsiColor::Purple.into(),
Color::LightMagenta => AnsiColor::Fuchsia.into(),
Color::Cyan => AnsiColor::Teal.into(),
Color::LightCyan => AnsiColor::Aqua.into(),
Color::White => AnsiColor::White.into(),
Color::Blue => AnsiColor::Navy.into(),
Color::LightBlue => AnsiColor::Blue.into(),
Color::Indexed(i) => ColorAttribute::PaletteIndex(i),
Color::Rgb(r, g, b) => {
ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple::from((r, g, b)))
}
}
}
}