diff --git a/README.md b/README.md
index f587b5193..988ad7268 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
-![Demo of Ratatui](https://repository-images.githubusercontent.com/600886023/96016d23-3bdd-4611-8c03-2e8b5836f900)
+![Demo of
+Ratatui](https://github.com/ratatui-org/ratatui/blob/ed851fb5bb3673b3213d1511e77be82fa6719ea0/examples/demo2-noborders.gif?raw=true)
diff --git a/examples/README.md b/examples/README.md
index 71bf26137..2f95cf0fe 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -8,7 +8,7 @@ occur in a terminal.
## Demo
-This is the demo example from the main README. It is available for each of the backends. Source:
+This is the previous demo example from the main README. It is available for each of the backends. Source:
[demo.rs](./demo/).
```shell
diff --git a/examples/demo2.tape b/examples/demo2.tape
index 8535d5b87..b7ef094d3 100644
--- a/examples/demo2.tape
+++ b/examples/demo2.tape
@@ -2,27 +2,71 @@
# 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)
+# Github social preview size (1280x640 with 80px padding) and must be < 1MB
+# This puts some constraints on the amount of interactivity we can do.
Set Width 1280
Set Height 640
Set Padding 80
+# Without the padding for README.md, etc.
+# Set Width 1120
+# Set Height 480
+# Set Padding 0
Hide
Type "cargo run --example demo2"
Enter
Sleep 2s
Show
# About screen
-Sleep 5s
+Sleep 3.5s
+Down # Red eye
+Sleep 0.5s
+Down # black eye
+Sleep 1s
Tab
# Email
-Sleep 5s
+Down
+Sleep 1s
+Down
+Sleep 1s
+Down
+Sleep 1s
+Down
+Sleep 2s
Tab
# Trace route
-Sleep 5s
-Tab
-# Misc
-Sleep 5s
+Sleep 1s
+Down
+Sleep 0.5s
+Down
+Sleep 0.5s
+Down
+Sleep 0.5s
+Down
+Sleep 0.5s
+Down
+Sleep 0.5s
+Down
+Sleep 0.5s
+Down
+Sleep 1s
Tab
# Recipe
+Sleep 1s
+Down
+Sleep 0.5s
+Down
+Sleep 0.5s
+Down
+Sleep 0.5s
+Down
+Sleep 0.5s
+Down
+Sleep 0.5s
+Down
+Sleep 0.5s
+Down
+Sleep 1s
+Tab
+# Misc
Sleep 5s
Tab
\ No newline at end of file
diff --git a/examples/demo2/app.rs b/examples/demo2/app.rs
index 0a26f332a..8da855264 100644
--- a/examples/demo2/app.rs
+++ b/examples/demo2/app.rs
@@ -1,79 +1,52 @@
-use std::{io::Stdout, time::Duration};
+use std::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,
-};
+use crate::{Root, Term};
+#[derive(Debug)]
pub struct App {
- terminal: Terminal
>,
+ term: Term,
should_quit: bool,
- tab_index: usize,
- selected_index: usize,
- tabs: Vec>,
+ context: AppContext,
+}
+
+#[derive(Debug, Default, Clone, Copy)]
+pub struct AppContext {
+ pub tab_index: usize,
+ pub row_index: usize,
}
impl App {
- pub fn new() -> Result {
- let terminal = tui::create_terminal()?;
+ fn new() -> Result {
Ok(Self {
- terminal,
+ term: Term::start()?,
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)),
- ],
+ context: AppContext::default(),
})
}
- pub fn run(&mut self) -> Result<()> {
- tui::setup()?;
- while !self.should_quit {
- self.draw()?;
- self.handle_events()?;
+ pub fn run() -> Result<()> {
+ install_panic_hook();
+ let mut app = Self::new()?;
+ while !app.should_quit {
+ app.draw()?;
+ app.handle_events()?;
}
- tui::restore()?;
+ Term::stop()?;
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)
- })
+ self.term
+ .draw(|frame| frame.render_widget(Root::new(&self.context), frame.size()))
.context("terminal.draw")?;
Ok(())
}
fn handle_events(&mut self) -> Result<()> {
- match tui::next_event(Duration::from_millis(16))? {
+ match Term::next_event(Duration::from_millis(16))? {
Some(Event::Key(key)) => self.handle_key_event(key),
_ => Ok(()),
}
@@ -83,26 +56,29 @@ impl App {
if key.kind != KeyEventKind::Press {
return Ok(());
}
+
+ let context = &mut self.context;
+ const TAB_COUNT: usize = 5;
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;
+ let tab_index = context.tab_index + TAB_COUNT; // to wrap around properly
+ context.tab_index = tab_index.saturating_sub(1) % TAB_COUNT;
+ context.row_index = 0;
}
KeyCode::Tab | KeyCode::BackTab => {
- self.tab_index = self.tab_index.saturating_add(1) % self.tabs.len();
- self.selected_index = 0;
+ context.tab_index = context.tab_index.saturating_add(1) % TAB_COUNT;
+ context.row_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);
+ context.row_index = context.row_index.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') => {
- self.selected_index = self.selected_index.saturating_add(1);
+ context.row_index = context.row_index.saturating_add(1);
}
_ => {}
};
@@ -110,17 +86,11 @@ impl App {
}
}
-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();
+ let _ = Term::stop();
hook(info);
std::process::exit(1);
}));
diff --git a/examples/demo2/app_widget.rs b/examples/demo2/app_widget.rs
deleted file mode 100644
index 5c5a142c1..000000000
--- a/examples/demo2/app_widget.rs
+++ /dev/null
@@ -1,69 +0,0 @@
-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
index 67489a97c..d82caeedb 100644
--- a/examples/demo2/colors.rs
+++ b/examples/demo2/colors.rs
@@ -1,11 +1,11 @@
-#![allow(dead_code)]
use palette::{
convert::{FromColorUnclamped, IntoColorUnclamped},
Okhsv, Srgb,
};
use ratatui::{prelude::*, widgets::*};
-fn render_16_colors(area: Rect, buf: &mut Buffer) {
+#[allow(dead_code)]
+fn render_16_color_swatch(area: Rect, buf: &mut Buffer) {
let sym = "██";
Paragraph::new(vec![
Line::from(vec![sym.black(), sym.red(), sym.green(), sym.yellow()]),
@@ -26,7 +26,8 @@ fn render_16_colors(area: Rect, buf: &mut Buffer) {
.render(area, buf);
}
-fn render_256_colors(area: Rect, buf: &mut Buffer) {
+#[allow(dead_code)]
+fn render_256_color_swatch(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);
@@ -38,7 +39,7 @@ fn render_256_colors(area: Rect, buf: &mut Buffer) {
}
}
-pub fn render_rgb_colors(area: Rect, buf: &mut Buffer) {
+pub fn render_rgb_swatch(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;
diff --git a/examples/demo2/main.rs b/examples/demo2/main.rs
index 8a2597901..bafa8e901 100644
--- a/examples/demo2/main.rs
+++ b/examples/demo2/main.rs
@@ -1,13 +1,16 @@
use anyhow::Result;
+pub use app::*;
+pub use root::*;
+pub use term::*;
+pub use theme::*;
mod app;
-mod app_widget;
mod colors;
-mod styles;
+mod root;
mod tabs;
-mod tui;
+mod term;
+mod theme;
fn main() -> Result<()> {
- app::install_panic_hook();
- app::App::new()?.run()
+ App::run()
}
diff --git a/examples/demo2/root.rs b/examples/demo2/root.rs
new file mode 100644
index 000000000..37890af32
--- /dev/null
+++ b/examples/demo2/root.rs
@@ -0,0 +1,74 @@
+use itertools::Itertools;
+use ratatui::{prelude::*, widgets::*};
+
+use crate::{layout, tabs::*, AppContext, THEME};
+
+pub struct Root<'a> {
+ context: &'a AppContext,
+}
+
+impl<'a> Root<'a> {
+ pub fn new(context: &'a AppContext) -> Self {
+ Root { context }
+ }
+}
+
+impl Widget for Root<'_> {
+ fn render(self, area: Rect, buf: &mut Buffer) {
+ Block::new().style(THEME.root).render(area, buf);
+ let area = 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 Root<'_> {
+ fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
+ let area = layout(area, Direction::Horizontal, vec![0, 53]);
+
+ Paragraph::new(Span::styled("Ratatui Demos ", THEME.app_title)).render(area[0], buf);
+ let titles = vec![" About ", " Email ", " Traceroute ", " Recipe ", " Misc "];
+ Tabs::new(titles)
+ .style(THEME.tabs)
+ .highlight_style(THEME.tabs_selected)
+ .select(self.context.tab_index)
+ .render(area[1], buf);
+ }
+
+ fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
+ let row_index = self.context.row_index;
+ match self.context.tab_index {
+ 0 => AboutTab::new(row_index).render(area, buf),
+ 1 => EmailTab::new(row_index).render(area, buf),
+ 2 => TracerouteTab::new(row_index).render(area, buf),
+ 3 => RecipeTab::new(row_index).render(area, buf),
+ 4 => MiscWidgetsTab::new().render(area, buf),
+ _ => unreachable!(),
+ };
+ }
+
+ fn render_bottom_bar(&self, area: Rect, buf: &mut Buffer) {
+ let keys = [
+ ("Q/Esc", "Quit"),
+ ("Tab", "Next Tab"),
+ ("←/h", "Left"),
+ ("→/l", "Right"),
+ ("↑/k", "Up"),
+ ("↓/j", "Down"),
+ ];
+ let spans = keys
+ .iter()
+ .flat_map(|(key, desc)| {
+ let key = Span::styled(format!(" {} ", key), THEME.key_binding.key);
+ let desc = Span::styled(format!(" {} ", desc), THEME.key_binding.description);
+ [key, desc]
+ })
+ .collect_vec();
+ Paragraph::new(Line::from(spans))
+ .alignment(Alignment::Center)
+ .fg(Color::Indexed(236))
+ .bg(Color::Indexed(232))
+ .render(area, buf);
+ }
+}
diff --git a/examples/demo2/styles.rs b/examples/demo2/styles.rs
deleted file mode 100644
index ea853a386..000000000
--- a/examples/demo2/styles.rs
+++ /dev/null
@@ -1,16 +0,0 @@
-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
index f5f91fd34..47a77e023 100644
--- a/examples/demo2/tabs.rs
+++ b/examples/demo2/tabs.rs
@@ -1,5 +1,3 @@
-use ratatui::prelude::*;
-
mod about;
mod email;
mod misc;
@@ -11,9 +9,3 @@ 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
index 33b4695d3..e070bac90 100644
--- a/examples/demo2/tabs/about.rs
+++ b/examples/demo2/tabs/about.rs
@@ -1,8 +1,7 @@
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
-use super::Tab;
-use crate::{colors, styles, tui::layout};
+use crate::{colors, layout, THEME};
const RATATUI_LOGO: [&str; 32] = [
" ███ ",
@@ -16,8 +15,8 @@ const RATATUI_LOGO: [&str; 32] = [
" █████████████ ██████",
" ███████████ ████████",
" █████ ███████████ ",
- " ███ ██xx████████ ",
- " █ ███xx████████ ",
+ " ███ ██ee████████ ",
+ " █ ███ee████████ ",
" ████ █████████████ ",
" █████████████████ ",
" ████████████████ ",
@@ -39,28 +38,65 @@ const RATATUI_LOGO: [&str; 32] = [
" █xxxxxxxxxxxxxxxxxxxx█ █ ",
];
-pub struct AboutTab;
+pub struct AboutTab {
+ selected_row: usize,
+}
impl AboutTab {
- pub fn new() -> Self {
- Self {}
+ pub fn new(selected_row: usize) -> Self {
+ Self { selected_row }
}
}
-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);
+impl Widget for AboutTab {
+ fn render(self, area: Rect, buf: &mut Buffer) {
+ colors::render_rgb_swatch(area, buf);
let area = layout(area, Direction::Horizontal, vec![34, 0]);
render_crate_description(area[1], buf);
- render_logo(area[0], buf);
+ render_logo(self.selected_row, area[0], buf);
}
}
-pub fn render_logo(area: Rect, buf: &mut Buffer) {
+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(THEME.content).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. Paragraph, Table) and draws them to the \
+ screen efficiently every frame.";
+ Paragraph::new(text)
+ .style(THEME.description)
+ .block(
+ Block::new()
+ .title(" Ratatui ")
+ .title_alignment(Alignment::Center)
+ .borders(Borders::TOP)
+ .border_style(THEME.description_title)
+ .padding(Padding::new(0, 0, 0, 0)),
+ )
+ .wrap(Wrap { trim: true })
+ .scroll((0, 0))
+ .render(area, buf);
+}
+
+pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
+ let eye_color = if selected_row % 2 == 0 {
+ THEME.logo.rat_eye
+ } else {
+ THEME.logo.rat_eye_alt
+ };
let area = area.inner(&Margin {
vertical: 0,
horizontal: 2,
@@ -70,70 +106,48 @@ pub fn render_logo(area: Rect, buf: &mut Buffer) {
let x = area.left() + x as u16;
let y = area.top() + y as u16;
let cell = buf.get_mut(x, y);
+ let rat_color = THEME.logo.rat;
+ let term_color = THEME.logo.term;
match (ch1, ch2) {
('█', '█') => {
cell.set_char('█');
- cell.fg = Color::Indexed(255);
+ cell.fg = rat_color;
}
('█', ' ') => {
cell.set_char('▀');
- cell.fg = Color::Indexed(255);
+ cell.fg = rat_color;
}
(' ', '█') => {
cell.set_char('▄');
- cell.fg = Color::Indexed(255);
+ cell.fg = rat_color;
}
('█', 'x') => {
cell.set_char('▀');
- cell.fg = Color::Indexed(255);
- cell.bg = Color::Black;
+ cell.fg = rat_color;
+ cell.bg = term_color;
}
('x', '█') => {
cell.set_char('▄');
- cell.fg = Color::Indexed(255);
- cell.bg = Color::Black;
+ cell.fg = rat_color;
+ cell.bg = term_color;
}
('x', 'x') => {
cell.set_char(' ');
- cell.fg = Color::Indexed(255);
- cell.bg = Color::Black;
+ cell.fg = term_color;
+ cell.bg = term_color;
+ }
+ ('█', 'e') => {
+ cell.set_char('▀');
+ cell.fg = rat_color;
+ cell.bg = eye_color;
+ }
+ ('e', '█') => {
+ cell.set_char('▄');
+ cell.fg = rat_color;
+ cell.bg = eye_color;
}
(_, _) => {}
};
}
}
}
-
-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
index 47f6596cc..d0841fa5d 100644
--- a/examples/demo2/tabs/email.rs
+++ b/examples/demo2/tabs/email.rs
@@ -2,8 +2,7 @@ use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use unicode_width::UnicodeWidthStr;
-use super::Tab;
-use crate::{colors, styles, tui::layout};
+use crate::{colors, layout, THEME};
#[derive(Debug, Default)]
pub struct Email {
@@ -12,25 +11,6 @@ pub struct Email {
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 ",
@@ -59,81 +39,95 @@ const EMAILS: &[Email] = &[
},
];
+#[derive(Debug, Default)]
+pub struct EmailTab {
+ selected_index: usize,
+}
+
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);
+impl Widget for EmailTab {
+ fn render(self, area: Rect, buf: &mut Buffer) {
+ colors::render_rgb_swatch(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);
+ render_inbox(self.selected_index, area[0], buf);
+ render_email(self.selected_index, area[1], buf);
}
+}
+fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
+ let area = layout(area, Direction::Vertical, vec![1, 0]);
+ let theme = THEME.email;
+ Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
+ .style(theme.tabs)
+ .highlight_style(theme.tabs_selected)
+ .select(0)
+ .divider("")
+ .render(area[0], 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,
- );
- }
+ 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(selected_index));
+ StatefulWidget::render(
+ List::new(items)
+ .style(theme.inbox)
+ .highlight_style(theme.selected_item)
+ .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);
- }
+fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
+ let theme = THEME.email;
+ let email = EMAILS.get(selected_index);
+ let block = Block::new().style(theme.body);
+ let inner = block.inner(area);
+ block.render(area, buf);
+ if let Some(email) = email {
+ let area = layout(inner, Direction::Vertical, vec![3, 0]);
+ let headers = vec![
+ Line::from(vec![
+ "From: ".set_style(theme.header),
+ email.from.set_style(theme.header_value),
+ ]),
+ Line::from(vec![
+ "Subject: ".set_style(theme.header),
+ email.subject.set_style(theme.header_value),
+ ]),
+ "-".repeat(inner.width as usize).dim().into(),
+ ];
+ Paragraph::new(headers)
+ .style(theme.body)
+ .render(area[0], buf);
+ let body = email.body.lines().map(Line::from).collect_vec();
+ Paragraph::new(body).style(theme.body).render(area[1], buf);
+ } else {
+ Paragraph::new("No email selected").render(inner, buf);
}
}
diff --git a/examples/demo2/tabs/misc.rs b/examples/demo2/tabs/misc.rs
index 5ac2e2142..7c960ce86 100644
--- a/examples/demo2/tabs/misc.rs
+++ b/examples/demo2/tabs/misc.rs
@@ -1,9 +1,6 @@
-#![allow(dead_code)]
-
use ratatui::{prelude::*, widgets::*};
-use super::Tab;
-use crate::{colors, styles, tui::layout};
+use crate::{colors, layout, THEME};
pub struct MiscWidgetsTab {
pub selected_row: usize,
@@ -15,19 +12,15 @@ impl MiscWidgetsTab {
}
}
-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);
+impl Widget for MiscWidgetsTab {
+ fn render(self, area: Rect, buf: &mut Buffer) {
+ colors::render_rgb_swatch(area, buf);
let area = area.inner(&Margin {
vertical: 1,
horizontal: 2,
});
Clear.render(area, buf);
- Block::new().style(styles::APP).render(area, buf);
+ Block::new().style(THEME.content).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);
diff --git a/examples/demo2/tabs/recipe.rs b/examples/demo2/tabs/recipe.rs
index acdd0869c..425015551 100644
--- a/examples/demo2/tabs/recipe.rs
+++ b/examples/demo2/tabs/recipe.rs
@@ -1,7 +1,88 @@
+use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
-use super::Tab;
-use crate::{colors, styles, tui};
+use crate::{colors, layout, THEME};
+
+#[derive(Debug, Default, Clone, Copy)]
+struct Ingredient {
+ quantity: &'static str,
+ name: &'static str,
+}
+
+impl Ingredient {
+ fn height(&self) -> u16 {
+ self.name.lines().count() as u16
+ }
+}
+
+impl<'a> From for Row<'a> {
+ fn from(i: Ingredient) -> Self {
+ Row::new(vec![i.quantity, i.name]).height(i.height())
+ }
+}
+
+// https://www.realsimple.com/food-recipes/browse-all-recipes/ratatouille
+const RECIPE: &[(&str, &str)] = &[
+ (
+ "Step 1: ",
+ "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.",
+ ),
+ (
+ "Step 2: ",
+ "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.",
+ ),
+];
+
+const INGREDIENTS: &[Ingredient] = &[
+ Ingredient {
+ quantity: "4 tbsp",
+ name: "olive oil",
+ },
+ Ingredient {
+ quantity: "1",
+ name: "onion thinly sliced",
+ },
+ Ingredient {
+ quantity: "4",
+ name: "cloves garlic\npeeled and sliced",
+ },
+ Ingredient {
+ quantity: "1",
+ name: "small bay leaf",
+ },
+ Ingredient {
+ quantity: "1",
+ name: "small eggplant cut\ninto 1/2 inch cubes",
+ },
+ Ingredient {
+ quantity: "1",
+ name: "small zucchini halved\nlengthwise and cut\ninto thin slices",
+ },
+ Ingredient {
+ quantity: "1",
+ name: "red bell pepper cut\ninto slivers",
+ },
+ Ingredient {
+ quantity: "4",
+ name: "plum tomatoes\ncoarsely chopped",
+ },
+ Ingredient {
+ quantity: "1 tsp",
+ name: "kosher salt",
+ },
+ Ingredient {
+ quantity: "1/4 cup",
+ name: "shredded fresh basil\nleaves",
+ },
+ Ingredient {
+ quantity: "",
+ name: "freshly ground black\npepper",
+ },
+];
#[derive(Debug)]
pub struct RecipeTab {
@@ -10,88 +91,82 @@ pub struct RecipeTab {
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,
+ selected_row: selected_row % INGREDIENTS.len(),
}
}
}
-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);
+impl Widget for RecipeTab {
+ fn render(self, area: Rect, buf: &mut Buffer) {
+ colors::render_rgb_swatch(area, buf);
let area = area.inner(&Margin {
vertical: 1,
horizontal: 2,
});
Clear.render(area, buf);
- Block::new().style(styles::APP).render(area, buf);
+ Block::new()
+ .title("Ratatouille Recipe")
+ .style(THEME.content)
+ .borders(Borders::ALL)
+ .border_type(BorderType::Rounded)
+ .border_style(THEME.borders)
+ .padding(Padding::new(2, 2, 1, 1))
+ .render(area, buf);
- let area = tui::layout(area, Direction::Vertical, vec![8, 0]);
+ let scrollbar_area = Rect {
+ y: area.y + 2,
+ height: area.height - 3,
+ ..area
+ };
+ render_scrollbar(self.selected_row, scrollbar_area, buf);
- 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 area = area.inner(&Margin {
+ horizontal: 2,
+ vertical: 1,
+ });
+ let area = layout(area, Direction::Horizontal, vec![44, 0]);
- 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,
- );
+ render_recipe(area[0], buf);
+ render_ingredients(self.selected_row, area[1], buf);
}
}
+
+fn render_recipe(area: Rect, buf: &mut Buffer) {
+ let lines = RECIPE
+ .iter()
+ .map(|(step, text)| Line::from(vec![step.white().bold(), text.gray()]))
+ .collect_vec();
+ Paragraph::new(lines)
+ .wrap(Wrap { trim: true })
+ .block(Block::new().padding(Padding::new(0, 1, 0, 0)))
+ .render(area, buf);
+}
+
+fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
+ let mut state = TableState::default().with_selected(Some(selected_row));
+ let rows = INGREDIENTS.iter().map(|&i| i.into()).collect_vec();
+ let theme = THEME.recipe;
+ StatefulWidget::render(
+ Table::new(rows)
+ .block(Block::new().style(theme.ingredients))
+ .header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
+ .widths(&[Constraint::Length(7), Constraint::Length(30)])
+ .highlight_style(Style::new().light_yellow()),
+ area,
+ buf,
+ &mut state,
+ );
+}
+
+fn render_scrollbar(position: usize, area: Rect, buf: &mut Buffer) {
+ let mut state = ScrollbarState::default()
+ .content_length(INGREDIENTS.len())
+ .viewport_content_length(6)
+ .position(position);
+ Scrollbar::new(ScrollbarOrientation::VerticalRight)
+ .begin_symbol(None)
+ .end_symbol(None)
+ .track_symbol(None)
+ .render(area, buf, &mut state)
+}
diff --git a/examples/demo2/tabs/traceroute.rs b/examples/demo2/tabs/traceroute.rs
index f7da25a46..db222f43a 100644
--- a/examples/demo2/tabs/traceroute.rs
+++ b/examples/demo2/tabs/traceroute.rs
@@ -1,31 +1,10 @@
use itertools::Itertools;
use ratatui::{
prelude::*,
- widgets::{
- canvas::{Canvas, Map, Points},
- *,
- },
+ widgets::{canvas::*, *},
};
-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,
- }
- }
-}
+use crate::{colors, layout, THEME};
#[derive(Debug)]
pub struct TracerouteTab {
@@ -40,94 +19,136 @@ impl TracerouteTab {
}
}
-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);
+impl Widget for TracerouteTab {
+ fn render(self, area: Rect, buf: &mut Buffer) {
+ colors::render_rgb_swatch(area, buf);
let area = area.inner(&Margin {
vertical: 1,
horizontal: 2,
});
Clear.render(area, buf);
- Block::new().style(styles::APP).render(area, buf);
+ Block::new().style(THEME.content).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_hops(self.selected_row, left_area[0], buf);
render_ping(self.selected_row, left_area[1], buf);
- self.render_map(area[1], buf);
+ render_map(self.selected_row, 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_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
+ let mut state = TableState::default().with_selected(Some(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"]).set_style(THEME.traceroute.header))
+ .widths(&[Constraint::Max(100), Constraint::Length(15)])
+ .highlight_style(THEME.traceroute.selected)
+ .block(block),
+ area,
+ buf,
+ &mut state,
+ );
+ let mut scrollbar_state = ScrollbarState::default()
+ .content_length(HOPS.len())
+ .position(selected_row);
+ let area = Rect {
+ y: area.y + 2,
+ height: area.height - 3,
+ ..area
+ };
+ Scrollbar::default()
+ .begin_symbol(None)
+ .end_symbol(None)
+ .track_symbol(Some(symbols::line::VERTICAL))
+ .render(area, buf, &mut scrollbar_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);
+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(THEME.traceroute.ping)
+ .render(area, buf);
+}
+
+fn render_map(selected_row: usize, area: Rect, buf: &mut Buffer) {
+ let theme = THEME.traceroute.map;
+ let path: Option<(&Hop, &Hop)> = HOPS.iter().tuple_windows().nth(selected_row);
+ let map = Map {
+ resolution: canvas::MapResolution::High,
+ color: theme.color,
+ };
+ Canvas::default()
+ .background_color(theme.background_color)
+ .block(
+ Block::new()
+ .title("Map")
+ .borders(Borders::ALL)
+ .style(theme.style),
+ )
+ .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,
+ theme.path,
+ ));
+ context.draw(&Points {
+ color: theme.source,
+ coords: &[path.0.location], // sydney
+ });
+ context.draw(&Points {
+ color: theme.destination,
+ coords: &[path.1.location], // perth
+ });
+ }
+ })
+ .render(area, buf);
+}
+
+#[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,
+ }
}
}
@@ -172,23 +193,3 @@ const HOPS: &[Hop] = &[
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/term.rs b/examples/demo2/term.rs
new file mode 100644
index 000000000..7d12b868d
--- /dev/null
+++ b/examples/demo2/term.rs
@@ -0,0 +1,91 @@
+use std::{
+ io::{self, stdout, Stdout},
+ ops::{Deref, DerefMut},
+ 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::*;
+
+/// A wrapper around the terminal that handles setting up and tearing down the terminal
+/// and provides a helper method to read events from the terminal.
+#[derive(Debug)]
+pub struct Term {
+ terminal: Terminal>,
+}
+
+impl Term {
+ pub fn start() -> 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)),
+ };
+ let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?;
+ enable_raw_mode().context("enable raw mode")?;
+ stdout()
+ .execute(EnterAlternateScreen)
+ .context("enter alternate screen")?;
+ Ok(Self { terminal })
+ }
+
+ pub fn stop() -> Result<()> {
+ disable_raw_mode().context("disable raw mode")?;
+ stdout()
+ .execute(LeaveAlternateScreen)
+ .context("leave alternate screen")?;
+ Ok(())
+ }
+
+ pub fn next_event(timeout: Duration) -> io::Result