mirror of
https://github.com/ratatui/ratatui.git
synced 2025-09-28 13:31:14 +00:00
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:
parent
1cc405d2dc
commit
4437835057
@ -32,6 +32,7 @@ unicode-segmentation = "1.10"
|
|||||||
unicode-width = "0.1"
|
unicode-width = "0.1"
|
||||||
termion = { version = "2.0", optional = true }
|
termion = { version = "2.0", optional = true }
|
||||||
crossterm = { version = "0.26", optional = true }
|
crossterm = { version = "0.26", optional = true }
|
||||||
|
termwiz = { version = "0.20.0", optional = true }
|
||||||
serde = { version = "1", optional = true, features = ["derive"]}
|
serde = { version = "1", optional = true, features = ["derive"]}
|
||||||
time = { version = "0.3.11", optional = true, features = ["local-offset"]}
|
time = { version = "0.3.11", optional = true, features = ["local-offset"]}
|
||||||
|
|
||||||
|
@ -13,10 +13,13 @@ dependencies = [
|
|||||||
"fmt",
|
"fmt",
|
||||||
"check-crossterm",
|
"check-crossterm",
|
||||||
"check-termion",
|
"check-termion",
|
||||||
|
"check-termwiz",
|
||||||
"test-crossterm",
|
"test-crossterm",
|
||||||
"test-termion",
|
"test-termion",
|
||||||
|
"test-termwiz",
|
||||||
"clippy-crossterm",
|
"clippy-crossterm",
|
||||||
"clippy-termion",
|
"clippy-termion",
|
||||||
|
"clippy-termwiz",
|
||||||
"test-doc",
|
"test-doc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -25,8 +28,11 @@ private = true
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"fmt",
|
"fmt",
|
||||||
"check-crossterm",
|
"check-crossterm",
|
||||||
|
"check-termwiz",
|
||||||
"test-crossterm",
|
"test-crossterm",
|
||||||
|
"test-termwiz",
|
||||||
"clippy-crossterm",
|
"clippy-crossterm",
|
||||||
|
"clippy-termwiz",
|
||||||
"test-doc",
|
"test-doc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -47,6 +53,10 @@ run_task = "check"
|
|||||||
env = { TUI_FEATURES = "serde,termion" }
|
env = { TUI_FEATURES = "serde,termion" }
|
||||||
run_task = "check"
|
run_task = "check"
|
||||||
|
|
||||||
|
[tasks.check-termwiz]
|
||||||
|
env = { TUI_FEATURES = "serde,termwiz" }
|
||||||
|
run_task = "check"
|
||||||
|
|
||||||
[tasks.check]
|
[tasks.check]
|
||||||
command = "cargo"
|
command = "cargo"
|
||||||
condition = { env_set = ["TUI_FEATURES"] }
|
condition = { env_set = ["TUI_FEATURES"] }
|
||||||
@ -66,6 +76,10 @@ run_task = "build"
|
|||||||
env = { TUI_FEATURES = "serde,termion" }
|
env = { TUI_FEATURES = "serde,termion" }
|
||||||
run_task = "build"
|
run_task = "build"
|
||||||
|
|
||||||
|
[tasks.build-termwiz]
|
||||||
|
env = { TUI_FEATURES = "serde,termwiz" }
|
||||||
|
run_task = "build"
|
||||||
|
|
||||||
[tasks.build]
|
[tasks.build]
|
||||||
command = "cargo"
|
command = "cargo"
|
||||||
condition = { env_set = ["TUI_FEATURES"] }
|
condition = { env_set = ["TUI_FEATURES"] }
|
||||||
@ -85,6 +99,10 @@ run_task = "clippy"
|
|||||||
env = { TUI_FEATURES = "serde,termion" }
|
env = { TUI_FEATURES = "serde,termion" }
|
||||||
run_task = "clippy"
|
run_task = "clippy"
|
||||||
|
|
||||||
|
[tasks.clippy-termwiz]
|
||||||
|
env = { TUI_FEATURES = "serde,termwiz" }
|
||||||
|
run_task = "clippy"
|
||||||
|
|
||||||
[tasks.clippy]
|
[tasks.clippy]
|
||||||
command = "cargo"
|
command = "cargo"
|
||||||
condition = { env_set = ["TUI_FEATURES"] }
|
condition = { env_set = ["TUI_FEATURES"] }
|
||||||
@ -107,6 +125,10 @@ run_task = "test"
|
|||||||
env = { TUI_FEATURES = "serde,termion,all-widgets" }
|
env = { TUI_FEATURES = "serde,termion,all-widgets" }
|
||||||
run_task = "test"
|
run_task = "test"
|
||||||
|
|
||||||
|
[tasks.test-termwiz]
|
||||||
|
env = { TUI_FEATURES = "serde,termwiz,all-widgets" }
|
||||||
|
run_task = "test"
|
||||||
|
|
||||||
[tasks.test]
|
[tasks.test]
|
||||||
command = "cargo"
|
command = "cargo"
|
||||||
condition = { env_set = ["TUI_FEATURES"] }
|
condition = { env_set = ["TUI_FEATURES"] }
|
||||||
|
@ -37,6 +37,7 @@ The library supports multiple backends:
|
|||||||
|
|
||||||
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
|
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
|
||||||
- [termion](https://github.com/ticki/termion)
|
- [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
|
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
|
buffers. This means that at each new frame you should build all widgets that are
|
||||||
|
@ -3,12 +3,18 @@ mod app;
|
|||||||
mod crossterm;
|
mod crossterm;
|
||||||
#[cfg(feature = "termion")]
|
#[cfg(feature = "termion")]
|
||||||
mod termion;
|
mod termion;
|
||||||
|
#[cfg(feature = "termwiz")]
|
||||||
|
mod termwiz;
|
||||||
|
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
#[cfg(feature = "crossterm")]
|
#[cfg(feature = "crossterm")]
|
||||||
use crate::crossterm::run;
|
use crate::crossterm::run;
|
||||||
#[cfg(feature = "termion")]
|
#[cfg(feature = "termion")]
|
||||||
use crate::termion::run;
|
use crate::termion::run;
|
||||||
|
#[cfg(feature = "termwiz")]
|
||||||
|
use crate::termwiz::run;
|
||||||
|
|
||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
use std::{error::Error, time::Duration};
|
use std::{error::Error, time::Duration};
|
||||||
|
|
||||||
|
75
examples/demo/termwiz.rs
Normal file
75
examples/demo/termwiz.rs
Normal 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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,11 @@ mod crossterm;
|
|||||||
#[cfg(feature = "crossterm")]
|
#[cfg(feature = "crossterm")]
|
||||||
pub use self::crossterm::CrosstermBackend;
|
pub use self::crossterm::CrosstermBackend;
|
||||||
|
|
||||||
|
#[cfg(feature = "termwiz")]
|
||||||
|
mod termwiz;
|
||||||
|
#[cfg(feature = "termwiz")]
|
||||||
|
pub use self::termwiz::TermwizBackend;
|
||||||
|
|
||||||
mod test;
|
mod test;
|
||||||
pub use self::test::TestBackend;
|
pub use self::test::TestBackend;
|
||||||
|
|
||||||
|
199
src/backend/termwiz.rs
Normal file
199
src/backend/termwiz.rs
Normal 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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user