Skip to content

Demo

This is the original demo example from the main README. It is available for each of the backends.

run
git clone https://github.com/ratatui/ratatui.git --branch latest
cd ratatui
cargo run --example=demo --features=crossterm
cargo run --example=demo --no-default-features --features=termion
cargo run --example=demo --no-default-features --features=termwiz

demo

demo/main.rs
//! # [Ratatui] Original Demo example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::{error::Error, time::Duration};
use argh::FromArgs;
mod app;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(all(not(windows), feature = "termion"))]
mod termion;
#[cfg(feature = "termwiz")]
mod termwiz;
mod ui;
/// Demo
#[derive(Debug, FromArgs)]
struct Cli {
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
let tick_rate = Duration::from_millis(cli.tick_rate);
#[cfg(feature = "crossterm")]
crate::crossterm::run(tick_rate, cli.enhanced_graphics)?;
#[cfg(all(not(windows), feature = "termion", not(feature = "crossterm")))]
crate::termion::run(tick_rate, cli.enhanced_graphics)?;
#[cfg(all(
feature = "termwiz",
not(feature = "crossterm"),
not(feature = "termion")
))]
crate::termwiz::run(tick_rate, cli.enhanced_graphics)?;
Ok(())
}
demo/app.rs
use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::widgets::ListState;
const TASKS: [&str; 24] = [
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
"Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17", "Item18", "Item19",
"Item20", "Item21", "Item22", "Item23", "Item24",
];
const LOGS: [(&str, &str); 26] = [
("Event1", "INFO"),
("Event2", "INFO"),
("Event3", "CRITICAL"),
("Event4", "ERROR"),
("Event5", "INFO"),
("Event6", "INFO"),
("Event7", "WARNING"),
("Event8", "INFO"),
("Event9", "INFO"),
("Event10", "INFO"),
("Event11", "CRITICAL"),
("Event12", "INFO"),
("Event13", "INFO"),
("Event14", "INFO"),
("Event15", "INFO"),
("Event16", "INFO"),
("Event17", "ERROR"),
("Event18", "ERROR"),
("Event19", "INFO"),
("Event20", "INFO"),
("Event21", "WARNING"),
("Event22", "INFO"),
("Event23", "INFO"),
("Event24", "WARNING"),
("Event25", "INFO"),
("Event26", "INFO"),
];
const EVENTS: [(&str, u64); 24] = [
("B1", 9),
("B2", 12),
("B3", 5),
("B4", 8),
("B5", 2),
("B6", 4),
("B7", 5),
("B8", 9),
("B9", 14),
("B10", 15),
("B11", 1),
("B12", 0),
("B13", 4),
("B14", 6),
("B15", 4),
("B16", 6),
("B17", 4),
("B18", 7),
("B19", 13),
("B20", 8),
("B21", 11),
("B22", 9),
("B23", 3),
("B24", 5),
];
#[derive(Clone)]
pub struct RandomSignal {
distribution: Uniform<u64>,
rng: ThreadRng,
}
impl RandomSignal {
pub fn new(lower: u64, upper: u64) -> Self {
Self {
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
}
}
impl Iterator for RandomSignal {
type Item = u64;
fn next(&mut self) -> Option<u64> {
Some(self.distribution.sample(&mut self.rng))
}
}
#[derive(Clone)]
pub struct SinSignal {
x: f64,
interval: f64,
period: f64,
scale: f64,
}
impl SinSignal {
pub const fn new(interval: f64, period: f64, scale: f64) -> Self {
Self {
x: 0.0,
interval,
period,
scale,
}
}
}
impl Iterator for SinSignal {
type Item = (f64, f64);
fn next(&mut self) -> Option<Self::Item> {
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
self.x += self.interval;
Some(point)
}
}
pub struct TabsState<'a> {
pub titles: Vec<&'a str>,
pub index: usize,
}
impl<'a> TabsState<'a> {
pub const fn new(titles: Vec<&'a str>) -> Self {
Self { titles, index: 0 }
}
pub fn next(&mut self) {
self.index = (self.index + 1) % self.titles.len();
}
pub fn previous(&mut self) {
if self.index > 0 {
self.index -= 1;
} else {
self.index = self.titles.len() - 1;
}
}
}
pub struct StatefulList<T> {
pub state: ListState,
pub items: Vec<T>,
}
impl<T> StatefulList<T> {
pub fn with_items(items: Vec<T>) -> Self {
Self {
state: ListState::default(),
items,
}
}
pub fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
}
pub struct Signal<S: Iterator> {
source: S,
pub points: Vec<S::Item>,
tick_rate: usize,
}
impl<S> Signal<S>
where
S: Iterator,
{
fn on_tick(&mut self) {
self.points.drain(0..self.tick_rate);
self.points
.extend(self.source.by_ref().take(self.tick_rate));
}
}
pub struct Signals {
pub sin1: Signal<SinSignal>,
pub sin2: Signal<SinSignal>,
pub window: [f64; 2],
}
impl Signals {
fn on_tick(&mut self) {
self.sin1.on_tick();
self.sin2.on_tick();
self.window[0] += 1.0;
self.window[1] += 1.0;
}
}
pub struct Server<'a> {
pub name: &'a str,
pub location: &'a str,
pub coords: (f64, f64),
pub status: &'a str,
}
pub struct App<'a> {
pub title: &'a str,
pub should_quit: bool,
pub tabs: TabsState<'a>,
pub show_chart: bool,
pub progress: f64,
pub sparkline: Signal<RandomSignal>,
pub tasks: StatefulList<&'a str>,
pub logs: StatefulList<(&'a str, &'a str)>,
pub signals: Signals,
pub barchart: Vec<(&'a str, u64)>,
pub servers: Vec<Server<'a>>,
pub enhanced_graphics: bool,
}
impl<'a> App<'a> {
pub fn new(title: &'a str, enhanced_graphics: bool) -> Self {
let mut rand_signal = RandomSignal::new(0, 100);
let sparkline_points = rand_signal.by_ref().take(300).collect();
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);
let sin1_points = sin_signal.by_ref().take(100).collect();
let mut sin_signal2 = SinSignal::new(0.1, 2.0, 10.0);
let sin2_points = sin_signal2.by_ref().take(200).collect();
App {
title,
should_quit: false,
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2"]),
show_chart: true,
progress: 0.0,
sparkline: Signal {
source: rand_signal,
points: sparkline_points,
tick_rate: 1,
},
tasks: StatefulList::with_items(TASKS.to_vec()),
logs: StatefulList::with_items(LOGS.to_vec()),
signals: Signals {
sin1: Signal {
source: sin_signal,
points: sin1_points,
tick_rate: 5,
},
sin2: Signal {
source: sin_signal2,
points: sin2_points,
tick_rate: 10,
},
window: [0.0, 20.0],
},
barchart: EVENTS.to_vec(),
servers: vec![
Server {
name: "NorthAmerica-1",
location: "New York City",
coords: (40.71, -74.00),
status: "Up",
},
Server {
name: "Europe-1",
location: "Paris",
coords: (48.85, 2.35),
status: "Failure",
},
Server {
name: "SouthAmerica-1",
location: "São Paulo",
coords: (-23.54, -46.62),
status: "Up",
},
Server {
name: "Asia-1",
location: "Singapore",
coords: (1.35, 103.86),
status: "Up",
},
],
enhanced_graphics,
}
}
pub fn on_up(&mut self) {
self.tasks.previous();
}
pub fn on_down(&mut self) {
self.tasks.next();
}
pub fn on_right(&mut self) {
self.tabs.next();
}
pub fn on_left(&mut self) {
self.tabs.previous();
}
pub fn on_key(&mut self, c: char) {
match c {
'q' => {
self.should_quit = true;
}
't' => {
self.show_chart = !self.show_chart;
}
_ => {}
}
}
pub fn on_tick(&mut self) {
// Update progress
self.progress += 0.001;
if self.progress > 1.0 {
self.progress = 0.0;
}
self.sparkline.on_tick();
self.signals.on_tick();
let log = self.logs.items.pop().unwrap();
self.logs.items.insert(0, log);
let event = self.barchart.pop().unwrap();
self.barchart.insert(0, event);
}
}
demo/ui.rs
use ratatui::{
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{self, Span},
widgets::{
canvas::{self, Canvas, Circle, Map, MapResolution, Rectangle},
Axis, BarChart, Block, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem, Paragraph,
Row, Sparkline, Table, Tabs, Wrap,
},
Frame,
};
use crate::app::App;
pub fn draw(frame: &mut Frame, app: &mut App) {
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(frame.area());
let tabs = app
.tabs
.titles
.iter()
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect::<Tabs>()
.block(Block::bordered().title(app.title))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index);
frame.render_widget(tabs, chunks[0]);
match app.tabs.index {
0 => draw_first_tab(frame, app, chunks[1]),
1 => draw_second_tab(frame, app, chunks[1]),
2 => draw_third_tab(frame, app, chunks[1]),
_ => {}
};
}
fn draw_first_tab(frame: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::vertical([
Constraint::Length(9),
Constraint::Min(8),
Constraint::Length(7),
])
.split(area);
draw_gauges(frame, app, chunks[0]);
draw_charts(frame, app, chunks[1]);
draw_text(frame, chunks[2]);
}
fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::vertical([
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(2),
])
.margin(1)
.split(area);
let block = Block::bordered().title("Graphs");
frame.render_widget(block, area);
let label = format!("{:.2}%", app.progress * 100.0);
let gauge = Gauge::default()
.block(Block::new().title("Gauge:"))
.gauge_style(
Style::default()
.fg(Color::Magenta)
.bg(Color::Black)
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
)
.use_unicode(app.enhanced_graphics)
.label(label)
.ratio(app.progress);
frame.render_widget(gauge, chunks[0]);
let sparkline = Sparkline::default()
.block(Block::new().title("Sparkline:"))
.style(Style::default().fg(Color::Green))
.data(&app.sparkline.points)
.bar_set(if app.enhanced_graphics {
symbols::bar::NINE_LEVELS
} else {
symbols::bar::THREE_LEVELS
});
frame.render_widget(sparkline, chunks[1]);
let line_gauge = LineGauge::default()
.block(Block::new().title("LineGauge:"))
.filled_style(Style::default().fg(Color::Magenta))
.line_set(if app.enhanced_graphics {
symbols::line::THICK
} else {
symbols::line::NORMAL
})
.ratio(app.progress);
frame.render_widget(line_gauge, chunks[2]);
}
#[allow(clippy::too_many_lines)]
fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
let constraints = if app.show_chart {
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
} else {
vec![Constraint::Percentage(100)]
};
let chunks = Layout::horizontal(constraints).split(area);
{
let chunks = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[0]);
{
let chunks =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[0]);
// Draw tasks
let tasks: Vec<ListItem> = app
.tasks
.items
.iter()
.map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))]))
.collect();
let tasks = List::new(tasks)
.block(Block::bordered().title("List"))
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("> ");
frame.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
// Draw logs
let info_style = Style::default().fg(Color::Blue);
let warning_style = Style::default().fg(Color::Yellow);
let error_style = Style::default().fg(Color::Magenta);
let critical_style = Style::default().fg(Color::Red);
let logs: Vec<ListItem> = app
.logs
.items
.iter()
.map(|&(evt, level)| {
let s = match level {
"ERROR" => error_style,
"CRITICAL" => critical_style,
"WARNING" => warning_style,
_ => info_style,
};
let content = vec![text::Line::from(vec![
Span::styled(format!("{level:<9}"), s),
Span::raw(evt),
])];
ListItem::new(content)
})
.collect();
let logs = List::new(logs).block(Block::bordered().title("List"));
frame.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
}
let barchart = BarChart::default()
.block(Block::bordered().title("Bar chart"))
.data(&app.barchart)
.bar_width(3)
.bar_gap(2)
.bar_set(if app.enhanced_graphics {
symbols::bar::NINE_LEVELS
} else {
symbols::bar::THREE_LEVELS
})
.value_style(
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::ITALIC),
)
.label_style(Style::default().fg(Color::Yellow))
.bar_style(Style::default().fg(Color::Green));
frame.render_widget(barchart, chunks[1]);
}
if app.show_chart {
let x_labels = vec![
Span::styled(
format!("{}", app.signals.window[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!(
"{}",
(app.signals.window[0] + app.signals.window[1]) / 2.0
)),
Span::styled(
format!("{}", app.signals.window[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let datasets = vec![
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.signals.sin1.points),
Dataset::default()
.name("data3")
.marker(if app.enhanced_graphics {
symbols::Marker::Braille
} else {
symbols::Marker::Dot
})
.style(Style::default().fg(Color::Yellow))
.data(&app.signals.sin2.points),
];
let chart = Chart::new(datasets)
.block(
Block::bordered().title(Span::styled(
"Chart",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.bounds(app.signals.window)
.labels(x_labels),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.bounds([-20.0, 20.0])
.labels([
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("0"),
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
]),
);
frame.render_widget(chart, chunks[1]);
}
}
fn draw_text(frame: &mut Frame, area: Rect) {
let text = vec![
text::Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
text::Line::from(""),
text::Line::from(vec![
Span::from("For example: "),
Span::styled("under", Style::default().fg(Color::Red)),
Span::raw(" "),
Span::styled("the", Style::default().fg(Color::Green)),
Span::raw(" "),
Span::styled("rainbow", Style::default().fg(Color::Blue)),
Span::raw("."),
]),
text::Line::from(vec![
Span::raw("Oh and if you didn't "),
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
Span::raw(" you can "),
Span::styled("automatically", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" "),
Span::styled("wrap", Style::default().add_modifier(Modifier::REVERSED)),
Span::raw(" your "),
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
Span::raw(".")
]),
text::Line::from(
"One more thing is that it should display unicode characters: 10€"
),
];
let block = Block::bordered().title(Span::styled(
"Footer",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
frame.render_widget(paragraph, area);
}
fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
let chunks =
Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]).split(area);
let up_style = Style::default().fg(Color::Green);
let failure_style = Style::default()
.fg(Color::Red)
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
let rows = app.servers.iter().map(|s| {
let style = if s.status == "Up" {
up_style
} else {
failure_style
};
Row::new(vec![s.name, s.location, s.status]).style(style)
});
let table = Table::new(
rows,
[
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(10),
],
)
.header(
Row::new(vec!["Server", "Location", "Status"])
.style(Style::default().fg(Color::Yellow))
.bottom_margin(1),
)
.block(Block::bordered().title("Servers"));
frame.render_widget(table, chunks[0]);
let map = Canvas::default()
.block(Block::bordered().title("World"))
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
resolution: MapResolution::High,
});
ctx.layer();
ctx.draw(&Rectangle {
x: 0.0,
y: 30.0,
width: 10.0,
height: 10.0,
color: Color::Yellow,
});
ctx.draw(&Circle {
x: app.servers[2].coords.1,
y: app.servers[2].coords.0,
radius: 10.0,
color: Color::Green,
});
for (i, s1) in app.servers.iter().enumerate() {
for s2 in &app.servers[i + 1..] {
ctx.draw(&canvas::Line {
x1: s1.coords.1,
y1: s1.coords.0,
y2: s2.coords.0,
x2: s2.coords.1,
color: Color::Yellow,
});
}
}
for server in &app.servers {
let color = if server.status == "Up" {
Color::Green
} else {
Color::Red
};
ctx.print(
server.coords.1,
server.coords.0,
Span::styled("X", Style::default().fg(color)),
);
}
})
.marker(if app.enhanced_graphics {
symbols::Marker::Braille
} else {
symbols::Marker::Dot
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0]);
frame.render_widget(map, chunks[1]);
}
fn draw_third_tab(frame: &mut Frame, _app: &mut App, area: Rect) {
let chunks = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area);
let colors = [
Color::Reset,
Color::Black,
Color::Red,
Color::Green,
Color::Yellow,
Color::Blue,
Color::Magenta,
Color::Cyan,
Color::Gray,
Color::DarkGray,
Color::LightRed,
Color::LightGreen,
Color::LightYellow,
Color::LightBlue,
Color::LightMagenta,
Color::LightCyan,
Color::White,
];
let items: Vec<Row> = colors
.iter()
.map(|c| {
let cells = vec![
Cell::from(Span::raw(format!("{c:?}: "))),
Cell::from(Span::styled("Foreground", Style::default().fg(*c))),
Cell::from(Span::styled("Background", Style::default().bg(*c))),
];
Row::new(cells)
})
.collect();
let table = Table::new(
items,
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
],
)
.block(Block::bordered().title("Colors"));
frame.render_widget(table, chunks[0]);
}
demo/crossterm.rs
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
Terminal,
};
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new("Crossterm Demo", enhanced_graphics);
let app_result = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = app_result {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Left | KeyCode::Char('h') => app.on_left(),
KeyCode::Up | KeyCode::Char('k') => app.on_up(),
KeyCode::Right | KeyCode::Char('l') => app.on_right(),
KeyCode::Down | KeyCode::Char('j') => app.on_down(),
KeyCode::Char(c) => app.on_key(c),
_ => {}
}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
if app.should_quit {
return Ok(());
}
}
}
demo/termion.rs
#![allow(dead_code)]
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
use ratatui::{
backend::{Backend, TermionBackend},
termion::{
event::Key,
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
screen::IntoAlternateScreen,
},
Terminal,
};
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
// setup terminal
let stdout = io::stdout()
.into_raw_mode()
.unwrap()
.into_alternate_screen()
.unwrap();
let stdout = MouseTerminal::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new("Termion demo", enhanced_graphics);
run_app(&mut terminal, app, tick_rate)?;
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> Result<(), Box<dyn Error>> {
let events = events(tick_rate);
loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?;
match events.recv()? {
Event::Input(key) => match key {
Key::Up | Key::Char('k') => app.on_up(),
Key::Down | Key::Char('j') => app.on_down(),
Key::Left | Key::Char('h') => app.on_left(),
Key::Right | Key::Char('l') => app.on_right(),
Key::Char(c) => app.on_key(c),
_ => {}
},
Event::Tick => app.on_tick(),
}
if app.should_quit {
return Ok(());
}
}
}
enum Event {
Input(Key),
Tick,
}
fn events(tick_rate: Duration) -> mpsc::Receiver<Event> {
let (tx, rx) = mpsc::channel();
let keys_tx = tx.clone();
thread::spawn(move || {
let stdin = io::stdin();
for key in stdin.keys().flatten() {
if let Err(err) = keys_tx.send(Event::Input(key)) {
eprintln!("{err}");
return;
}
}
});
thread::spawn(move || loop {
if let Err(err) = tx.send(Event::Tick) {
eprintln!("{err}");
break;
}
thread::sleep(tick_rate);
});
rx
}
demo/termwiz.rs
#![allow(dead_code)]
use std::{
error::Error,
time::{Duration, Instant},
};
use ratatui::{
backend::TermwizBackend,
termwiz::{
input::{InputEvent, KeyCode},
terminal::Terminal as TermwizTerminal,
},
Terminal,
};
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 app_result = run_app(&mut terminal, app, tick_rate);
terminal.show_cursor()?;
terminal.flush()?;
if let Err(err) = app_result {
println!("{err:?}");
}
Ok(())
}
fn run_app(
terminal: &mut Terminal<TermwizBackend>,
mut app: App,
tick_rate: Duration,
) -> Result<(), Box<dyn Error>> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if let 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 | KeyCode::Char('k') => app.on_up(),
KeyCode::DownArrow | KeyCode::Char('j') => app.on_down(),
KeyCode::LeftArrow | KeyCode::Char('h') => app.on_left(),
KeyCode::RightArrow | KeyCode::Char('l') => 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(());
}
}
}