diff --git a/.markdownlint.yaml b/.markdownlint.yaml
index 68c7bfe77..5168908af 100644
--- a/.markdownlint.yaml
+++ b/.markdownlint.yaml
@@ -1,10 +1,13 @@
# configuration for https://github.com/DavidAnson/markdownlint
+first-line-heading: false
no-inline-html:
allowed_elements:
- img
- details
- summary
+ - div
+ - br
line-length:
line_length: 100
diff --git a/Cargo.toml b/Cargo.toml
index 9526ee0e8..1e15a1e00 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -48,14 +48,16 @@ lru = "0.11.1"
[dev-dependencies]
anyhow = "1.0.71"
-argh = "0.1"
+argh = "0.1.12"
better-panic = "0.3.0"
cargo-husky = { version = "1.5.0", default-features = false, features = [
"user-hooks",
] }
-criterion = { version = "0.5", features = ["html_reports"] }
+criterion = { version = "0.5.1", features = ["html_reports"] }
fakeit = "1.1"
-rand = "0.8"
+lipsum = "0.9.0"
+rand = "0.8.5"
+palette = "0.7.3"
pretty_assertions = "1.4.0"
[features]
@@ -149,6 +151,12 @@ name = "demo"
# this runs for all of the terminal backends, so it can't be built using --all-features or scraped
doc-scrape-examples = false
+[[example]]
+name = "demo2"
+required-features = ["crossterm"]
+# this runs for all of the terminal backends, so it can't be built using --all-features or scraped
+doc-scrape-examples = false
+
[[example]]
name = "gauge"
required-features = ["crossterm"]
diff --git a/README.md b/README.md
index 1dc42ee90..f587b5193 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,32 @@
-# Ratatui
-
-
+![Demo of Ratatui](https://repository-images.githubusercontent.com/600886023/96016d23-3bdd-4611-8c03-2e8b5836f900)
+
-`ratatui` is a [Rust](https://www.rust-lang.org) library that is all about cooking up terminal user interfaces.
-It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
-project.
+
[![Crates.io](https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square)](https://crates.io/crates/ratatui)
[![License](https://img.shields.io/crates/l/ratatui?style=flat-square)](./LICENSE) [![GitHub CI
Status](https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github)](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
-[![Docs.rs](https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square)](https://docs.rs/crate/ratatui/)
+[![Docs.rs](https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square)](https://docs.rs/crate/ratatui/)
[![Dependency
Status](https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square)](https://deps.rs/repo/github/ratatui-org/ratatui)
[![Codecov](https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST)](https://app.codecov.io/gh/ratatui-org/ratatui)
[![Discord](https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square)](https://discord.gg/pMCEU9hNEj)
-[![Matrix](https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix)](https://matrix.to/#/#ratatui:matrix.org)
+[![Matrix](https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix)](https://matrix.to/#/#ratatui:matrix.org)
+[Documentation](https://docs.rs/ratatui)
+· [Examples](https://github.com/ratatui-org/ratatui/tree/main/examples)
+· [Report a bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
+· [Request a Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
+· [Send a Pull Request](https://github.com/ratatui-org/ratatui/compare)
-
-![Demo of Ratatui](https://vhs.charm.sh/vhs-tF0QbuPbtHgUeG0sTVgFr.gif)
+
+
+
+
+# Ratatui
+
+`ratatui` is a [Rust](https://www.rust-lang.org) library that is all about cooking up terminal user
+interfaces. It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
+project.
Table of Contents
diff --git a/examples/colors_rgb.rs b/examples/colors_rgb.rs
index 6f6451f1b..d531fe1d6 100644
--- a/examples/colors_rgb.rs
+++ b/examples/colors_rgb.rs
@@ -13,7 +13,10 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
-use itertools::Itertools;
+use palette::{
+ convert::{FromColorUnclamped, IntoColorUnclamped},
+ Okhsv, Srgb,
+};
use ratatui::{prelude::*, widgets::*};
type Result = std::result::Result>;
@@ -77,9 +80,8 @@ struct RgbColors;
impl Widget for RgbColors {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = Self::layout(area);
- let rgb_colors = Self::create_rgb_color_grid(area.width, area.height * 2);
Self::render_title(layout[0], buf);
- Self::render_colors(layout[1], buf, rgb_colors);
+ Self::render_colors(layout[1], buf);
}
}
@@ -99,41 +101,26 @@ impl RgbColors {
}
/// Render a colored grid of half block characters (`"▀"`) each with a different RGB color.
- fn render_colors(area: Rect, buf: &mut Buffer, rgb_colors: Vec>) {
- for (x, column) in (area.left()..area.right()).zip(rgb_colors.iter()) {
- for (y, (fg, bg)) in (area.top()..area.bottom()).zip(column.iter().tuples()) {
- let cell = buf.get_mut(x, y);
- cell.fg = *fg;
- cell.bg = *bg;
- cell.symbol = "▀".into();
- }
- }
- }
-
- /// Generate a smooth grid of colors
- ///
- /// Red ranges from 0 to 255 across the x axis. Green ranges from 0 to 255 across the y axis.
- /// Blue repeats every 32 pixels in both directions, but flipped every 16 pixels so that it
- /// doesn't transition sharply from light to dark.
- ///
- /// The result stored in a 2d vector of colors with the x axis as the first dimension, and the
- /// y axis the second dimension.
- fn create_rgb_color_grid(width: u16, height: u16) -> Vec> {
- let mut result = vec![];
- for x in 0..width {
- let mut column = vec![];
- for y in 0..height {
- // flip both axes every 16 pixels. E.g. [0, 1, ... 15, 15, ... 1, 0]
- let yy = if (y % 32) < 16 { y % 32 } else { 31 - y % 32 };
- let xx = if (x % 32) < 16 { x % 32 } else { 31 - x % 32 };
- let r = (256 * x / width) as u8;
- let g = (256 * y / height) as u8;
- let b = (yy * 16 + xx) as u8;
- column.push(Color::Rgb(r, g, b))
+ fn render_colors(area: Rect, buf: &mut Buffer) {
+ for (xi, x) in (area.left()..area.right()).enumerate() {
+ for (yi, y) in (area.top()..area.bottom()).enumerate() {
+ let hue = xi as f32 * 360.0 / area.width as f32;
+
+ let value_fg = (yi as f32) / (area.height as f32 - 0.5);
+ let fg = Okhsv::::new(hue, Okhsv::max_saturation(), value_fg);
+ let fg: Srgb = fg.into_color_unclamped();
+ let fg: Srgb = fg.into_format();
+ let fg = Color::Rgb(fg.red, fg.green, fg.blue);
+
+ let value_bg = (yi as f32 + 0.5) / (area.height as f32 - 0.5);
+ let bg = Okhsv::new(hue, Okhsv::max_saturation(), value_bg);
+ let bg = Srgb::::from_color_unclamped(bg);
+ let bg: Srgb = bg.into_format();
+ let bg = Color::Rgb(bg.red, bg.green, bg.blue);
+
+ buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
}
- result.push(column);
}
- result
}
}
diff --git a/examples/demo2.tape b/examples/demo2.tape
new file mode 100644
index 000000000..8535d5b87
--- /dev/null
+++ b/examples/demo2.tape
@@ -0,0 +1,28 @@
+# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
+# To run this script, install vhs and run `vhs ./examples/demo.tape`
+Output "target/demo2.gif"
+Set Theme "OceanicMaterial"
+# Github social preview size (1280x640 with 80px padding)
+Set Width 1280
+Set Height 640
+Set Padding 80
+Hide
+Type "cargo run --example demo2"
+Enter
+Sleep 2s
+Show
+# About screen
+Sleep 5s
+Tab
+# Email
+Sleep 5s
+Tab
+# Trace route
+Sleep 5s
+Tab
+# Misc
+Sleep 5s
+Tab
+# Recipe
+Sleep 5s
+Tab
\ No newline at end of file
diff --git a/examples/demo2/app.rs b/examples/demo2/app.rs
new file mode 100644
index 000000000..0a26f332a
--- /dev/null
+++ b/examples/demo2/app.rs
@@ -0,0 +1,127 @@
+use std::{io::Stdout, time::Duration};
+
+use anyhow::{Context, Result};
+use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
+use itertools::Itertools;
+use ratatui::prelude::*;
+
+use crate::{
+ app_widget::AppWidget,
+ tabs,
+ tabs::{AboutTab, EmailTab, MiscWidgetsTab, RecipeTab, Tab, TracerouteTab},
+ tui,
+};
+
+pub struct App {
+ terminal: Terminal>,
+ should_quit: bool,
+ tab_index: usize,
+ selected_index: usize,
+ tabs: Vec>,
+}
+
+impl App {
+ pub fn new() -> Result {
+ let terminal = tui::create_terminal()?;
+ Ok(Self {
+ terminal,
+ should_quit: false,
+ tab_index: 0,
+ selected_index: 0,
+ tabs: vec![
+ Box::new(tabs::AboutTab::new()),
+ Box::new(tabs::EmailTab::new(0)),
+ Box::new(tabs::TracerouteTab::new(0)),
+ Box::new(tabs::MiscWidgetsTab::new()),
+ Box::new(tabs::RecipeTab::new(0)),
+ ],
+ })
+ }
+
+ pub fn run(&mut self) -> Result<()> {
+ tui::setup()?;
+ while !self.should_quit {
+ self.draw()?;
+ self.handle_events()?;
+ }
+ tui::restore()?;
+ Ok(())
+ }
+
+ fn draw(&mut self) -> Result<()> {
+ self.terminal
+ .draw(|frame| {
+ let titles = self.tabs.iter().map(|tab| tab.title()).collect_vec();
+ let tab: Box = {
+ // This is a bit of a hack to get around the borrow checker.
+ // which works because we know that the tabs are all static.
+ match self.tab_index {
+ 0 => Box::new(AboutTab::new()),
+ 1 => Box::new(EmailTab::new(self.selected_index)),
+ 2 => Box::new(TracerouteTab::new(self.selected_index)),
+ 3 => Box::new(MiscWidgetsTab::new()),
+ 4 => Box::new(RecipeTab::new(self.selected_index)),
+ _ => unreachable!(),
+ }
+ };
+ let view = AppWidget::new(tab, self.tab_index, titles);
+ let area = frame.size();
+ frame.render_widget(view, area)
+ })
+ .context("terminal.draw")?;
+ Ok(())
+ }
+
+ fn handle_events(&mut self) -> Result<()> {
+ match tui::next_event(Duration::from_millis(16))? {
+ Some(Event::Key(key)) => self.handle_key_event(key),
+ _ => Ok(()),
+ }
+ }
+
+ fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> {
+ if key.kind != KeyEventKind::Press {
+ return Ok(());
+ }
+ match key.code {
+ KeyCode::Char('q') => {
+ self.should_quit = true;
+ }
+ KeyCode::Tab | KeyCode::BackTab if key.modifiers.contains(KeyModifiers::SHIFT) => {
+ let tab_index = self.tab_index + self.tabs.len(); // to wrap around properly
+ self.tab_index = tab_index.saturating_sub(1) % self.tabs.len();
+ self.selected_index = 0;
+ }
+ KeyCode::Tab | KeyCode::BackTab => {
+ self.tab_index = self.tab_index.saturating_add(1) % self.tabs.len();
+ self.selected_index = 0;
+ }
+ KeyCode::Left | KeyCode::Char('h') => {}
+ KeyCode::Right | KeyCode::Char('l') => {}
+ KeyCode::Up | KeyCode::Char('k') => {
+ self.selected_index = self.selected_index.saturating_sub(1);
+ }
+ KeyCode::Down | KeyCode::Char('j') => {
+ self.selected_index = self.selected_index.saturating_add(1);
+ }
+ _ => {}
+ };
+ Ok(())
+ }
+}
+
+impl Drop for App {
+ fn drop(&mut self) {
+ let _ = tui::restore();
+ }
+}
+
+pub fn install_panic_hook() {
+ better_panic::install();
+ let hook = std::panic::take_hook();
+ std::panic::set_hook(Box::new(move |info| {
+ let _ = tui::restore();
+ hook(info);
+ std::process::exit(1);
+ }));
+}
diff --git a/examples/demo2/app_widget.rs b/examples/demo2/app_widget.rs
new file mode 100644
index 000000000..5c5a142c1
--- /dev/null
+++ b/examples/demo2/app_widget.rs
@@ -0,0 +1,69 @@
+use ratatui::{prelude::*, widgets::*};
+
+use crate::{styles, tabs::Tab, tui};
+
+pub struct AppWidget {
+ tab: Box,
+ tab_index: usize,
+ titles: Vec,
+}
+
+impl AppWidget {
+ pub fn new(tab: Box, tab_index: usize, titles: Vec) -> Self {
+ AppWidget {
+ tab,
+ tab_index,
+ titles,
+ }
+ }
+}
+
+impl Widget for AppWidget {
+ fn render(self, area: Rect, buf: &mut Buffer) {
+ Block::new().bg(styles::APP_BACKGROUND).render(area, buf);
+ let area = tui::layout(area, Direction::Vertical, vec![1, 0, 1]);
+ self.render_title_bar(area[0], buf);
+ self.render_selected_tab(area[1], buf);
+ self.render_bottom_bar(area[2], buf);
+ }
+}
+
+impl AppWidget {
+ fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
+ let area = tui::layout(area, Direction::Horizontal, vec![17, 0]);
+
+ Paragraph::new(Span::styled("Ratatui v0.23.0 ", styles::APP_TITLE)).render(area[0], buf);
+
+ Tabs::new(self.titles.clone())
+ .style(styles::TABS)
+ .highlight_style(styles::TABS_SELECTED)
+ .select(self.tab_index)
+ .render(area[1], buf);
+ }
+
+ fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
+ self.tab.render(area, buf);
+ }
+
+ fn render_bottom_bar(&self, area: Rect, buf: &mut Buffer) {
+ let key_style = Style::new().fg(Color::Indexed(232)).bg(Color::Indexed(236));
+ Paragraph::new(Line::from(vec![
+ " Q/Esc ".set_style(key_style),
+ " Quit ".into(),
+ " Tab ".set_style(key_style),
+ " Next Tab ".into(),
+ " ←/h ".set_style(key_style),
+ " Left ".into(),
+ " →/l ".set_style(key_style),
+ " Right ".into(),
+ " ↑/k ".set_style(key_style),
+ " Up ".into(),
+ " ↓/j ".set_style(key_style),
+ " Down".into(),
+ ]))
+ .alignment(Alignment::Center)
+ .fg(Color::Indexed(236))
+ .bg(Color::Indexed(232))
+ .render(area, buf);
+ }
+}
diff --git a/examples/demo2/colors.rs b/examples/demo2/colors.rs
new file mode 100644
index 000000000..67489a97c
--- /dev/null
+++ b/examples/demo2/colors.rs
@@ -0,0 +1,59 @@
+#![allow(dead_code)]
+use palette::{
+ convert::{FromColorUnclamped, IntoColorUnclamped},
+ Okhsv, Srgb,
+};
+use ratatui::{prelude::*, widgets::*};
+
+fn render_16_colors(area: Rect, buf: &mut Buffer) {
+ let sym = "██";
+ Paragraph::new(vec![
+ Line::from(vec![sym.black(), sym.red(), sym.green(), sym.yellow()]),
+ Line::from(vec![sym.blue(), sym.magenta(), sym.cyan(), sym.gray()]),
+ Line::from(vec![
+ sym.dark_gray(),
+ sym.light_red(),
+ sym.light_green(),
+ sym.light_yellow(),
+ ]),
+ Line::from(vec![
+ sym.light_blue(),
+ sym.light_magenta(),
+ sym.light_cyan(),
+ sym.white(),
+ ]),
+ ])
+ .render(area, buf);
+}
+
+fn render_256_colors(area: Rect, buf: &mut Buffer) {
+ for (xi, x) in (16..52).zip(area.left()..area.right()) {
+ for (yi, y) in (0..3).zip(area.top()..area.bottom()) {
+ let fg = Color::Indexed(yi * 72 + xi);
+ let bg = Color::Indexed(yi * 72 + xi + 36);
+ buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
+ }
+ let fg = Color::Indexed(xi.saturating_add(216));
+ buf.get_mut(x, area.bottom() - 1).set_char('█').set_fg(fg);
+ }
+}
+
+pub fn render_rgb_colors(area: Rect, buf: &mut Buffer) {
+ for (xi, x) in (area.left()..area.right()).enumerate() {
+ for (yi, y) in (area.top()..area.bottom()).enumerate() {
+ let yi = area.height as usize - yi - 1;
+ let hue = xi as f32 * 360.0 / area.width as f32;
+ let value_bg = (yi as f32 - 0.0) / (area.height as f32);
+ let value_fg = (yi as f32 + 0.5) / (area.height as f32);
+ let fg = Okhsv::::new(hue, Okhsv::max_saturation(), value_fg);
+ let fg: Srgb = fg.into_color_unclamped();
+ let fg: Srgb = fg.into_format();
+ let fg = Color::Rgb(fg.red, fg.green, fg.blue);
+ let bg = Okhsv::new(hue, Okhsv::max_saturation(), value_bg);
+ let bg = Srgb::::from_color_unclamped(bg);
+ let bg: Srgb = bg.into_format();
+ let bg = Color::Rgb(bg.red, bg.green, bg.blue);
+ buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
+ }
+ }
+}
diff --git a/examples/demo2/main.rs b/examples/demo2/main.rs
new file mode 100644
index 000000000..8a2597901
--- /dev/null
+++ b/examples/demo2/main.rs
@@ -0,0 +1,13 @@
+use anyhow::Result;
+
+mod app;
+mod app_widget;
+mod colors;
+mod styles;
+mod tabs;
+mod tui;
+
+fn main() -> Result<()> {
+ app::install_panic_hook();
+ app::App::new()?.run()
+}
diff --git a/examples/demo2/styles.rs b/examples/demo2/styles.rs
new file mode 100644
index 000000000..ea853a386
--- /dev/null
+++ b/examples/demo2/styles.rs
@@ -0,0 +1,16 @@
+use ratatui::prelude::*;
+
+pub const DARK_BLUE: Color = Color::Rgb(16, 24, 48);
+pub const LIGHT_BLUE: Color = Color::Rgb(64, 96, 192);
+pub const APP_BACKGROUND: Color = DARK_BLUE;
+pub const APP: Style = Style::new().bg(APP_BACKGROUND);
+pub const APP_TITLE: Style = Style::new()
+ .add_modifier(Modifier::BOLD)
+ .fg(Color::Indexed(252))
+ .bg(Color::Indexed(232));
+pub const TABS: Style = Style::new().fg(Color::Indexed(244)).bg(Color::Indexed(232));
+pub const TABS_SELECTED: Style = Style::new().add_modifier(Modifier::BOLD).fg(LIGHT_BLUE);
+pub const BORDERS: Style = Style::new()
+ .fg(Color::Indexed(252))
+ .add_modifier(Modifier::BOLD);
+pub const DESCRIPTION: Style = Style::new().fg(Color::Gray).bg(DARK_BLUE);
diff --git a/examples/demo2/tabs.rs b/examples/demo2/tabs.rs
new file mode 100644
index 000000000..f5f91fd34
--- /dev/null
+++ b/examples/demo2/tabs.rs
@@ -0,0 +1,19 @@
+use ratatui::prelude::*;
+
+mod about;
+mod email;
+mod misc;
+mod recipe;
+mod traceroute;
+
+pub use about::AboutTab;
+pub use email::EmailTab;
+pub use misc::MiscWidgetsTab;
+pub use recipe::RecipeTab;
+pub use traceroute::TracerouteTab;
+
+pub trait Tab {
+ fn title(&self) -> String;
+ fn render(&self, area: Rect, buf: &mut Buffer);
+ fn select(&mut self, _row: usize) {}
+}
diff --git a/examples/demo2/tabs/about.rs b/examples/demo2/tabs/about.rs
new file mode 100644
index 000000000..33b4695d3
--- /dev/null
+++ b/examples/demo2/tabs/about.rs
@@ -0,0 +1,139 @@
+use itertools::Itertools;
+use ratatui::{prelude::*, widgets::*};
+
+use super::Tab;
+use crate::{colors, styles, tui::layout};
+
+const RATATUI_LOGO: [&str; 32] = [
+ " ███ ",
+ " █████ ",
+ " ███████ ",
+ " ████████ ",
+ " █████████ ",
+ " ██████████ ",
+ " ████████████ ",
+ " █████████████ ",
+ " █████████████ ██████",
+ " ███████████ ████████",
+ " █████ ███████████ ",
+ " ███ ██xx████████ ",
+ " █ ███xx████████ ",
+ " ████ █████████████ ",
+ " █████████████████ ",
+ " ████████████████ ",
+ " ████████████████ ",
+ " ███ ██████████ ",
+ " ██ █████████ ",
+ " █xx█ █████████ ",
+ " █xxxx█ ██████████ ",
+ " █xx█xxx█ █████████ ",
+ " █xx██xxxx█ ████████ ",
+ " █xxxxxxxxxx█ ██████████ ",
+ " █xxxxxxxxxxxx█ ██████████ ",
+ " █xxxxxxx██xxxxx█ █████████ ",
+ " █xxxxxxxxx██xxxxx█ ████ ███ ",
+ " █xxxxxxxxxxxxxxxxxx█ ██ ███ ",
+ "█xxxxxxxxxxxxxxxxxxxx█ █ ███ ",
+ " █xxxxxxxxxxxxxxxxxxxx█ ███ ",
+ " █xxxxxxxxxxxxxxxxxxxx█ ██ ",
+ " █xxxxxxxxxxxxxxxxxxxx█ █ ",
+];
+
+pub struct AboutTab;
+
+impl AboutTab {
+ pub fn new() -> Self {
+ Self {}
+ }
+}
+
+impl Tab for AboutTab {
+ fn title(&self) -> String {
+ "About".to_string()
+ }
+
+ fn render(&self, area: Rect, buf: &mut Buffer) {
+ colors::render_rgb_colors(area, buf);
+ let area = layout(area, Direction::Horizontal, vec![34, 0]);
+ render_crate_description(area[1], buf);
+ render_logo(area[0], buf);
+ }
+}
+
+pub fn render_logo(area: Rect, buf: &mut Buffer) {
+ let area = area.inner(&Margin {
+ vertical: 0,
+ horizontal: 2,
+ });
+ for (y, (line1, line2)) in RATATUI_LOGO.iter().tuples().enumerate() {
+ for (x, (ch1, ch2)) in line1.chars().zip(line2.chars()).enumerate() {
+ let x = area.left() + x as u16;
+ let y = area.top() + y as u16;
+ let cell = buf.get_mut(x, y);
+ match (ch1, ch2) {
+ ('█', '█') => {
+ cell.set_char('█');
+ cell.fg = Color::Indexed(255);
+ }
+ ('█', ' ') => {
+ cell.set_char('▀');
+ cell.fg = Color::Indexed(255);
+ }
+ (' ', '█') => {
+ cell.set_char('▄');
+ cell.fg = Color::Indexed(255);
+ }
+ ('█', 'x') => {
+ cell.set_char('▀');
+ cell.fg = Color::Indexed(255);
+ cell.bg = Color::Black;
+ }
+ ('x', '█') => {
+ cell.set_char('▄');
+ cell.fg = Color::Indexed(255);
+ cell.bg = Color::Black;
+ }
+ ('x', 'x') => {
+ cell.set_char(' ');
+ cell.fg = Color::Indexed(255);
+ cell.bg = Color::Black;
+ }
+ (_, _) => {}
+ };
+ }
+ }
+}
+
+fn render_crate_description(area: Rect, buf: &mut Buffer) {
+ let area = area.inner(
+ &(Margin {
+ vertical: 4,
+ horizontal: 2,
+ }),
+ );
+ Clear.render(area, buf); // clear out the color swatches
+ Block::new().style(styles::APP).render(area, buf);
+ let area = area.inner(
+ &(Margin {
+ vertical: 1,
+ horizontal: 2,
+ }),
+ );
+ let text = "- cooking up terminal user interfaces -
+
+ Ratatui is a Rust crate that provides widgets (e.g. Pargraph, Table) and draws them to the \
+ screen efficiently every frame.";
+ Paragraph::new(text)
+ .style(styles::DESCRIPTION)
+ .block(
+ Block::new()
+ .title("Ratatui")
+ .title_alignment(Alignment::Center)
+ .borders(Borders::TOP)
+ .border_style(styles::BORDERS)
+ .padding(Padding::new(0, 0, 0, 0)),
+ )
+ .wrap(Wrap { trim: true })
+ .scroll((0, 0))
+ .render(area, buf);
+}
diff --git a/examples/demo2/tabs/email.rs b/examples/demo2/tabs/email.rs
new file mode 100644
index 000000000..47f6596cc
--- /dev/null
+++ b/examples/demo2/tabs/email.rs
@@ -0,0 +1,139 @@
+use itertools::Itertools;
+use ratatui::{prelude::*, widgets::*};
+use unicode_width::UnicodeWidthStr;
+
+use super::Tab;
+use crate::{colors, styles, tui::layout};
+
+#[derive(Debug, Default)]
+pub struct Email {
+ from: &'static str,
+ subject: &'static str,
+ body: &'static str,
+}
+
+#[derive(Debug, Default)]
+pub struct EmailTab {
+ selected_index: usize,
+}
+
+impl Tab for EmailTab {
+ fn title(&self) -> String {
+ "Email".to_string()
+ }
+
+ fn select(&mut self, row: usize) {
+ self.selected_index = row;
+ }
+
+ fn render(&self, area: Rect, buf: &mut Buffer) {
+ self.render(area, buf);
+ }
+}
+
+const EMAILS: &[Email] = &[
+ Email {
+ from: "Alice ",
+ subject: "Hello",
+ body: "Hi Bob,\n\nHow are you?\n\nAlice",
+ },
+ Email {
+ from: "Bob ",
+ subject: "Re: Hello",
+ body: "Hi Alice,\nI'm fine, thanks!\n\nBob",
+ },
+ Email {
+ from: "Charlie ",
+ subject: "Re: Hello",
+ body: "Hi Alice,\nI'm fine, thanks!\n\nCharlie",
+ },
+ Email {
+ from: "Dave ",
+ subject: "Re: Hello (STOP REPLYING TO ALL)",
+ body: "Hi Everyone,\nPlease stop replying to all.\n\nDave",
+ },
+ Email {
+ from: "Eve ",
+ subject: "Re: Hello (STOP REPLYING TO ALL)",
+ body: "Hi Everyone,\nI'm reading all your emails.\n\nEve",
+ },
+];
+
+impl EmailTab {
+ pub fn new(selected_index: usize) -> Self {
+ Self {
+ selected_index: selected_index % EMAILS.len(),
+ }
+ }
+
+ fn render(&self, area: Rect, buf: &mut Buffer) {
+ colors::render_rgb_colors(area, buf);
+ let area = area.inner(&Margin {
+ vertical: 1,
+ horizontal: 2,
+ });
+ Clear.render(area, buf);
+ let area = layout(area, Direction::Vertical, vec![6, 0]);
+ self.render_inbox(area[0], buf);
+ self.render_email(area[1], buf);
+ }
+
+ fn render_inbox(&self, area: Rect, buf: &mut Buffer) {
+ let area = layout(area, Direction::Vertical, vec![1, 0]);
+ Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
+ .style(Style::new().fg(Color::Indexed(244)).bg(Color::Indexed(232)))
+ .highlight_style(
+ Style::new()
+ .bold()
+ .fg(Color::Indexed(232))
+ .bg(Color::Rgb(64, 96, 192)),
+ )
+ .select(0)
+ .divider("")
+ .render(area[0], buf);
+
+ let highlight_symbol = ">>";
+ let from_width = EMAILS
+ .iter()
+ .map(|e| e.from.width())
+ .max()
+ .unwrap_or_default();
+ let subject_width = area[1].width as usize - from_width - highlight_symbol.width() - 1;
+ let items = EMAILS
+ .iter()
+ .map(|e| {
+ let from = format!("{:width$}", e.from, width = from_width);
+ let subject = format!("{:width$}", e.subject, width = subject_width);
+ let text = [from, subject].join(" ");
+ ListItem::new(text)
+ })
+ .collect_vec();
+ let mut state = ListState::default().with_selected(Some(self.selected_index));
+ StatefulWidget::render(
+ List::new(items)
+ .highlight_style(Style::new().bold().yellow())
+ .highlight_symbol(highlight_symbol),
+ area[1],
+ buf,
+ &mut state,
+ );
+ }
+
+ fn render_email(&self, area: Rect, buf: &mut Buffer) {
+ let email = EMAILS.get(self.selected_index);
+ let block = Block::new().borders(Borders::TOP).style(styles::APP);
+ let inner = block.inner(area);
+ block.render(area, buf);
+ if let Some(email) = email {
+ let mut text = vec![
+ Line::from(vec!["From: ".bold(), email.from.clone().into()]),
+ Line::from(vec!["Subject: ".bold(), email.subject.clone().into()]),
+ "-".repeat(inner.width as usize).dim().into(),
+ ];
+ text.extend(email.body.lines().map(Line::from));
+ Paragraph::new(text).render(inner, buf);
+ } else {
+ Paragraph::new("No email selected").render(inner, buf);
+ }
+ }
+}
diff --git a/examples/demo2/tabs/misc.rs b/examples/demo2/tabs/misc.rs
new file mode 100644
index 000000000..5ac2e2142
--- /dev/null
+++ b/examples/demo2/tabs/misc.rs
@@ -0,0 +1,182 @@
+#![allow(dead_code)]
+
+use ratatui::{prelude::*, widgets::*};
+
+use super::Tab;
+use crate::{colors, styles, tui::layout};
+
+pub struct MiscWidgetsTab {
+ pub selected_row: usize,
+}
+
+impl MiscWidgetsTab {
+ pub fn new() -> Self {
+ Self { selected_row: 0 }
+ }
+}
+
+impl Tab for MiscWidgetsTab {
+ fn title(&self) -> String {
+ "Misc Widgets".to_string()
+ }
+
+ fn render(&self, area: Rect, buf: &mut Buffer) {
+ colors::render_rgb_colors(area, buf);
+ let area = area.inner(&Margin {
+ vertical: 1,
+ horizontal: 2,
+ });
+ Clear.render(area, buf);
+ Block::new().style(styles::APP).render(area, buf);
+ let area = layout(area, Direction::Vertical, vec![0, 5]);
+ render_bars(area[0], buf);
+ render_gauges(self.selected_row, area[1], buf);
+ }
+}
+
+fn render_bars(area: Rect, buf: &mut Buffer) {
+ let area = Layout::default()
+ .direction(Direction::Horizontal)
+ .constraints(vec![Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
+ .split(area);
+
+ render_simple_barchart(area[0], buf);
+ render_horizontal_barchart(area[1], buf);
+}
+
+fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
+ let data = vec![
+ ("Jan", 10),
+ ("Feb", 20),
+ ("Mar", 30),
+ ("Apr", 40),
+ ("May", 50),
+ ("Jun", 60),
+ ("Jul", 70),
+ ];
+ let block = Block::default()
+ .title("BarChart")
+ .borders(Borders::ALL)
+ .border_type(BorderType::Rounded);
+ let dark_green = Color::Rgb(32, 96, 48);
+ let light_green = Color::Rgb(64, 192, 96);
+ BarChart::default()
+ .data(&data)
+ .block(block)
+ .bar_width(3)
+ .bar_gap(1)
+ .value_style(Style::default().fg(dark_green).bg(light_green))
+ .label_style(Style::default().fg(light_green))
+ .bar_style(Style::default().fg(light_green))
+ .render(area, buf);
+}
+
+fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
+ // https://www.videocardbenchmark.net/high_end_gpus.html
+ let bg = Color::Rgb(32, 48, 96);
+ let nvidia = Style::new().light_green().bg(bg);
+ let amd = Style::new().light_red().bg(bg);
+ let data = [
+ Bar::default()
+ .text_value("GeForce RTX 4090 (38,978)".into())
+ .value_style(nvidia)
+ .value(38978),
+ Bar::default()
+ .text_value("GeForce RTX 4080 (34,879)".into())
+ .value_style(nvidia)
+ .value(34879),
+ Bar::default()
+ .text_value("Radeon PRO W7800 (32,146)".into())
+ .value_style(amd)
+ .value(32146),
+ Bar::default()
+ .text_value("GeForce RTX 4070 Ti (31,659)".into())
+ .value_style(nvidia)
+ .value(31659),
+ Bar::default()
+ .text_value("Radeon RX 7900 XTX (31,180)".into())
+ .value_style(amd)
+ .value(31180),
+ ];
+ let group = BarGroup::default().label("GPU".into()).bars(&data);
+ let block = Block::default()
+ .title("Passmark")
+ .borders(Borders::ALL)
+ .border_type(BorderType::Rounded);
+ BarChart::default()
+ .direction(Direction::Horizontal)
+ .block(block)
+ .data(group)
+ .bar_gap(1)
+ .bar_style(Style::default().fg(bg))
+ .render(area, buf);
+}
+
+pub fn render_gauges(progress: usize, area: Rect, buf: &mut Buffer) {
+ let block = Block::new()
+ .title("Gauges")
+ .borders(Borders::ALL)
+ .border_type(BorderType::Rounded);
+ let inner = block.inner(area);
+ block.render(area, buf);
+ let area = layout(inner, Direction::Vertical, vec![1, 1, 1, 0]);
+
+ let percent = (progress * 2 + 20).min(100) as f64;
+ let progress_label = if percent < 100.0 {
+ format!("{}%", percent)
+ } else {
+ "Done!".into()
+ };
+
+ render_gauge(percent, &progress_label, area[0], buf);
+ render_line_gauge(percent, &progress_label, area[1], buf);
+ render_sparkline(progress, "Sparkline", area[2], buf);
+}
+
+fn render_gauge(percent: f64, label: &str, area: Rect, buf: &mut Buffer) {
+ let area = layout(area, Direction::Horizontal, vec![10, 0]);
+ Paragraph::new("Gauge")
+ .style(Style::new().light_green())
+ .render(area[0], buf);
+ let bg = Color::Rgb(32, 96, 48);
+ let fg = Color::Rgb(64, 192, 96);
+ Gauge::default()
+ .ratio(percent / 100.0)
+ .label(format!("Processing: {}", label))
+ .gauge_style(Style::new().fg(fg).bg(bg))
+ .use_unicode(false)
+ .render(area[1], buf);
+}
+
+fn render_line_gauge(percent: f64, label: &str, area: Rect, buf: &mut Buffer) {
+ let area = layout(area, Direction::Horizontal, vec![10, 0]);
+ Paragraph::new("LineGauge")
+ .style(Style::new().light_blue())
+ .render(area[0], buf);
+ LineGauge::default()
+ .ratio(percent / 100.0)
+ .label(format!("Download: {}", label))
+ .style(Style::new().light_blue())
+ .gauge_style(Style::new().blue().on_light_blue())
+ .line_set(symbols::line::THICK)
+ .render(area[1], buf);
+}
+
+pub fn render_sparkline(progress: usize, title: &str, area: Rect, buf: &mut Buffer) {
+ let area = layout(area, Direction::Horizontal, vec![10, 0]);
+ Paragraph::new(title)
+ .style(Style::new().white())
+ .render(area[0], buf);
+ let mut data = [
+ 8, 8, 8, 8, 7, 7, 7, 6, 6, 5, 4, 3, 3, 2, 2, 1, 1, 1, 2, 2, 3, 4, 5, 6, 7, 7, 8, 8, 8, 7,
+ 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 2, 4, 6, 7, 8, 8, 8, 8, 6, 4, 2, 1, 1, 1, 1, 2, 2, 2, 3,
+ 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7,
+ ];
+ let mid = progress % data.len();
+ data.rotate_left(mid);
+ let style = Style::new().fg(Color::Rgb(192, 192, 64));
+ Sparkline::default()
+ .data(&data)
+ .style(style)
+ .render(area[1], buf);
+}
diff --git a/examples/demo2/tabs/recipe.rs b/examples/demo2/tabs/recipe.rs
new file mode 100644
index 000000000..acdd0869c
--- /dev/null
+++ b/examples/demo2/tabs/recipe.rs
@@ -0,0 +1,97 @@
+use ratatui::{prelude::*, widgets::*};
+
+use super::Tab;
+use crate::{colors, styles, tui};
+
+#[derive(Debug)]
+pub struct RecipeTab {
+ selected_row: usize,
+}
+
+impl RecipeTab {
+ pub fn new(selected_row: usize) -> Self {
+ const INGREDIENT_COUNT: usize = 11; // TODO: derive this from the table
+ Self {
+ selected_row: selected_row % INGREDIENT_COUNT,
+ }
+ }
+}
+
+impl Tab for RecipeTab {
+ fn title(&self) -> String {
+ "Recipe".to_string()
+ }
+
+ fn select(&mut self, row: usize) {
+ self.selected_row = row;
+ }
+
+ fn render(&self, area: Rect, buf: &mut Buffer) {
+ colors::render_rgb_colors(area, buf);
+ let area = area.inner(&Margin {
+ vertical: 1,
+ horizontal: 2,
+ });
+ Clear.render(area, buf);
+ Block::new().style(styles::APP).render(area, buf);
+
+ let area = tui::layout(area, Direction::Vertical, vec![8, 0]);
+
+ let lines: Vec = vec![
+ Line::from(vec![
+ "Step 1: ".white().bold(),
+ "Over medium-low heat, add the oil to a large skillet with the onion, garlic, and bay \
+ leaf, stirring occasionally, until the onion has softened."
+ .into(),
+ ]),
+ Line::from(vec![
+ "Step 2: ".white().bold(),
+ "Add the eggplant and cook, stirring occasionally, for 8 minutes or until the \
+ eggplant has softened. Stir in the zucchini, red bell pepper, tomatoes, and salt, and \
+ cook over medium heat, stirring occasionally, for 5 to 7 minutes or until the \
+ vegetables are tender. Stir in the basil and few grinds of pepper to taste."
+ .into(),
+ ]),
+ Line::from(vec!["Ingredients:".white().bold()]),
+ ];
+ Paragraph::new(lines)
+ .wrap(Wrap { trim: true })
+ .render(area[0], buf);
+
+ let mut state = TableState::default().with_selected(Some(self.selected_row));
+ // https://www.realsimple.com/food-recipes/browse-all-recipes/ratatouille
+ StatefulWidget::render(
+ Table::new(vec![
+ Row::new(vec!["4 tbsp", "olive oil", ""]),
+ Row::new(vec!["1", "onion", "thinly sliced"]),
+ Row::new(vec!["4", "cloves garlic", "peeled and sliced"]),
+ Row::new(vec!["1", "small bay leaf", ""]),
+ Row::new(vec!["1", "small eggplant", "cut into 1/2 inch cubes"]),
+ Row::new(vec![
+ "1".into(),
+ "small zucchini".into(),
+ Text::raw("halved lengthwise and cut into\nthin slices"),
+ ])
+ .height(2),
+ Row::new(vec!["1", "red bell pepper", "cut into slivers"]),
+ Row::new(vec!["4", "plum tomatoes", "coarsely chopped"]),
+ Row::new(vec!["1 tsp", "kosher salt", ""]),
+ Row::new(vec!["1/4 cup", "shredded fresh basil leaves", ""]),
+ Row::new(vec!["", "freshly ground black pepper", ""]),
+ ])
+ .header(
+ Row::new(vec!["Qty", "Ingredient", "Notes"])
+ .style(Style::new().white().underlined()),
+ )
+ .widths(&[
+ Constraint::Length(7),
+ Constraint::Length(30),
+ Constraint::Length(450),
+ ])
+ .highlight_style(Style::new().light_yellow()),
+ area[1],
+ buf,
+ &mut state,
+ );
+ }
+}
diff --git a/examples/demo2/tabs/traceroute.rs b/examples/demo2/tabs/traceroute.rs
new file mode 100644
index 000000000..f7da25a46
--- /dev/null
+++ b/examples/demo2/tabs/traceroute.rs
@@ -0,0 +1,194 @@
+use itertools::Itertools;
+use ratatui::{
+ prelude::*,
+ widgets::{
+ canvas::{Canvas, Map, Points},
+ *,
+ },
+};
+
+use super::Tab;
+use crate::{colors, styles, tui::layout};
+
+#[derive(Debug)]
+struct Hop {
+ host: &'static str,
+ address: &'static str,
+ location: (f64, f64),
+}
+
+impl Hop {
+ const fn new(name: &'static str, address: &'static str, location: (f64, f64)) -> Self {
+ Self {
+ host: name,
+ address,
+ location,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct TracerouteTab {
+ selected_row: usize,
+}
+
+impl TracerouteTab {
+ pub fn new(selected_row: usize) -> Self {
+ Self {
+ selected_row: selected_row % HOPS.len(),
+ }
+ }
+}
+
+impl Tab for TracerouteTab {
+ fn title(&self) -> String {
+ "Traceroute".to_string()
+ }
+
+ fn render(&self, area: Rect, buf: &mut Buffer) {
+ self.render_traceroute_tab(area, buf);
+ }
+
+ fn select(&mut self, row: usize) {
+ self.selected_row = row;
+ }
+}
+
+impl TracerouteTab {
+ fn render_traceroute_tab(&self, area: Rect, buf: &mut Buffer) {
+ colors::render_rgb_colors(area, buf);
+ let area = area.inner(&Margin {
+ vertical: 1,
+ horizontal: 2,
+ });
+ Clear.render(area, buf);
+ Block::new().style(styles::APP).render(area, buf);
+ let area = Layout::default()
+ .direction(Direction::Horizontal)
+ .constraints(vec![Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
+ .split(area);
+ let left_area = layout(area[0], Direction::Vertical, vec![0, 4]);
+ self.render_hops(left_area[0], buf);
+ render_ping(self.selected_row, left_area[1], buf);
+ self.render_map(area[1], buf);
+ }
+
+ fn render_hops(&self, area: Rect, buf: &mut Buffer) {
+ let mut state = TableState::default().with_selected(Some(self.selected_row));
+ let rows = HOPS
+ .iter()
+ .map(|hop| Row::new(vec![hop.host, hop.address]))
+ .collect_vec();
+ let block = Block::default()
+ .title("Traceroute bad.horse")
+ .borders(Borders::ALL)
+ .border_type(BorderType::Rounded);
+ StatefulWidget::render(
+ Table::new(rows)
+ .header(Row::new(vec!["Host", "Address"]).bold().underlined())
+ .widths(&[Constraint::Max(100), Constraint::Length(15)])
+ .highlight_style(Style::new().dark_gray().on_white())
+ .block(block),
+ area,
+ buf,
+ &mut state,
+ );
+ }
+
+ fn render_map(&self, area: Rect, buf: &mut Buffer) {
+ let path: Option<(&Hop, &Hop)> = HOPS.iter().tuple_windows().nth(self.selected_row);
+ let block = Block::new().title("Map").borders(Borders::ALL);
+ let map = Map {
+ resolution: canvas::MapResolution::High,
+ color: Color::Gray,
+ };
+ Canvas::default()
+ .marker(Marker::Dot)
+ .x_bounds([113.0, 154.0]) // australia
+ .y_bounds([-42.0, -11.0]) // australia
+ .paint(|context| {
+ context.draw(&map);
+ if let Some(path) = path {
+ context.draw(&canvas::Line::new(
+ path.0.location.0,
+ path.0.location.1,
+ path.1.location.0,
+ path.1.location.1,
+ Color::Blue,
+ ));
+ context.draw(&Points {
+ color: Color::Green,
+ coords: &[path.0.location], // sydney
+ });
+ context.draw(&Points {
+ color: Color::Red,
+ coords: &[path.1.location], // perth
+ });
+ }
+ })
+ .block(block)
+ .render(area, buf);
+ }
+}
+
+const CANBERRA: (f64, f64) = (149.1, -35.3);
+const SYDNEY: (f64, f64) = (151.1, -33.9);
+const MELBOURNE: (f64, f64) = (144.9, -37.8);
+const PERTH: (f64, f64) = (115.9, -31.9);
+const DARWIN: (f64, f64) = (130.8, -12.4);
+const BRISBANE: (f64, f64) = (153.0, -27.5);
+const ADELAIDE: (f64, f64) = (138.6, -34.9);
+
+// Go traceroute bad.horse some time, it's fun. these locations are made up and don't correspond
+// to the actual IP addresses (which are in Toronto, Canada).
+const HOPS: &[Hop] = &[
+ Hop::new("home", "127.0.0.1", CANBERRA),
+ Hop::new("bad.horse", "162.252.205.130", SYDNEY),
+ Hop::new("bad.horse", "162.252.205.131", MELBOURNE),
+ Hop::new("bad.horse", "162.252.205.132", BRISBANE),
+ Hop::new("bad.horse", "162.252.205.133", SYDNEY),
+ Hop::new("he.rides.across.the.nation", "162.252.205.134", PERTH),
+ Hop::new("the.thoroughbred.of.sin", "162.252.205.135", DARWIN),
+ Hop::new("he.got.the.application", "162.252.205.136", BRISBANE),
+ Hop::new("that.you.just.sent.in", "162.252.205.137", ADELAIDE),
+ Hop::new("it.needs.evaluation", "162.252.205.138", DARWIN),
+ Hop::new("so.let.the.games.begin", "162.252.205.139", PERTH),
+ Hop::new("a.heinous.crime", "162.252.205.140", BRISBANE),
+ Hop::new("a.show.of.force", "162.252.205.141", CANBERRA),
+ Hop::new("a.murder.would.be.nice.of.course", "162.252.205.142", PERTH),
+ Hop::new("bad.horse", "162.252.205.143", MELBOURNE),
+ Hop::new("bad.horse", "162.252.205.144", DARWIN),
+ Hop::new("bad.horse", "162.252.205.145", MELBOURNE),
+ Hop::new("he-s.bad", "162.252.205.146", PERTH),
+ Hop::new("the.evil.league.of.evil", "162.252.205.147", BRISBANE),
+ Hop::new("is.watching.so.beware", "162.252.205.148", DARWIN),
+ Hop::new("the.grade.that.you.receive", "162.252.205.149", PERTH),
+ Hop::new("will.be.your.last.we.swear", "162.252.205.150", ADELAIDE),
+ Hop::new("so.make.the.bad.horse.gleeful", "162.252.205.151", SYDNEY),
+ Hop::new("or.he-ll.make.you.his.mare", "162.252.205.152", MELBOURNE),
+ Hop::new("o_o", "162.252.205.153", BRISBANE),
+ Hop::new("you-re.saddled.up", "162.252.205.154", DARWIN),
+ Hop::new("there-s.no.recourse", "162.252.205.155", PERTH),
+ Hop::new("it-s.hi-ho.silver", "162.252.205.156", SYDNEY),
+ Hop::new("signed.bad.horse", "162.252.205.157", CANBERRA),
+];
+
+pub fn render_ping(progress: usize, area: Rect, buf: &mut Buffer) {
+ let mut data = [
+ 8, 8, 8, 8, 7, 7, 7, 6, 6, 5, 4, 3, 3, 2, 2, 1, 1, 1, 2, 2, 3, 4, 5, 6, 7, 7, 8, 8, 8, 7,
+ 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 2, 4, 6, 7, 8, 8, 8, 8, 6, 4, 2, 1, 1, 1, 1, 2, 2, 2, 3,
+ 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7,
+ ];
+ let mid = progress % data.len();
+ data.rotate_left(mid);
+ Sparkline::default()
+ .block(
+ Block::new()
+ .title("Ping")
+ .borders(Borders::ALL)
+ .border_type(BorderType::Rounded),
+ )
+ .data(&data)
+ .style(Style::new().white())
+ .render(area, buf);
+}
diff --git a/examples/demo2/tui.rs b/examples/demo2/tui.rs
new file mode 100644
index 000000000..16aab0c34
--- /dev/null
+++ b/examples/demo2/tui.rs
@@ -0,0 +1,67 @@
+use std::{
+ io::{self, stdout, Stdout},
+ rc::Rc,
+ time::Duration,
+};
+
+use anyhow::{Context, Result};
+use crossterm::{
+ event::{self, Event},
+ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
+ ExecutableCommand,
+};
+use itertools::Itertools;
+use ratatui::prelude::*;
+
+pub fn create_terminal() -> Result>> {
+ // this size is to match the size of the terminal when running the demo
+ // using vhs in a 1280x640 sized window (github social preview size)
+ let options = TerminalOptions {
+ viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)),
+ // viewport: Viewport::Fullscreen,
+ };
+ let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?;
+ Ok(terminal)
+}
+
+pub fn setup() -> Result<()> {
+ enable_raw_mode().context("enable raw mode")?;
+ stdout()
+ .execute(EnterAlternateScreen)
+ .context("enter alternate screen")?;
+ Ok(())
+}
+
+pub fn restore() -> Result<()> {
+ disable_raw_mode().context("disable raw mode")?;
+ stdout()
+ .execute(LeaveAlternateScreen)
+ .context("leave alternate screen")?;
+ Ok(())
+}
+
+pub fn next_event(timeout: Duration) -> io::Result> {
+ if !event::poll(timeout)? {
+ return Ok(None);
+ }
+ let event = event::read()?;
+ Ok(Some(event))
+}
+
+/// helper method to split an area into multiple sub-areas
+pub fn layout(area: Rect, direction: Direction, heights: Vec) -> Rc<[Rect]> {
+ let constraints = heights
+ .iter()
+ .map(|&h| {
+ if h > 0 {
+ Constraint::Length(h)
+ } else {
+ Constraint::Min(0)
+ }
+ })
+ .collect_vec();
+ Layout::default()
+ .direction(direction)
+ .constraints(constraints)
+ .split(area)
+}