Of all places, I found Ratatui from a post on Threads. It’s basically a really light fast Rust-based Terminal User Interface (TUI).
It has some VHS examples as well. I haven’t used VHS before so let’s see what that is about as we build out our own TUI using the framework with Google Antigravity.
Ratatui
Let’s start by cloning the GIT repo
I realized my local rust environment was way out of date
$ cargo version
cargo 1.74.0 (ecb9851af 2023-10-18)
so i did a brew install rust to update it to current.
Then set the current with
$ rustup update stable
$ rustup default stable
Let’s try an example
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/rattui/ratatui$ cargo run -p color-explorer
Compiling object v0.36.7
Compiling ratatui-core v0.1.2 (/home/builder/Workspaces/rattui/ratatui/ratatui-core)
Compiling ratatui-widgets v0.3.2 (/home/builder/Workspaces/rattui/ratatui/ratatui-widgets)
Compiling ratatui-crossterm v0.1.2 (/home/builder/Workspaces/rattui/ratatui/ratatui-crossterm)
Compiling ratatui-macros v0.7.2 (/home/builder/Workspaces/rattui/ratatui/ratatui-macros)
Compiling ratatui v0.30.2 (/home/builder/Workspaces/rattui/ratatui/ratatui)
Compiling backtrace v0.3.75
Compiling color-eyre v0.6.5
Compiling color-explorer v0.0.0 (/home/builder/Workspaces/rattui/ratatui/examples/apps/color-explorer)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.97s
Running `target/debug/color-explorer`
Note: If you see an error like
$ cargo run -p color-explorer
error: failed to load manifest for workspace member `/home/builder/Workspaces/rattui/ratatui/examples/apps/advanced-widget-impl`
Caused by:
failed to parse manifest at `/home/builder/Workspaces/rattui/ratatui/examples/apps/advanced-widget-impl/Cargo.toml`
Caused by:
TOML parse error at line 1, column 1
|
1 | [package]
| ^^^^^^^^^
missing field `version`
That is due to an old version of rust, go back and follow the steps to update your Rust toolchain to something newer than 0.75.
here is the ‘calendar-explorer’ example:
And here is cargo run -p demo
I found a lot of these examples did not respect ctrl-x to kill so I would need to use kill to terminate the example
builder@DESKTOP-QADGF36:~$ ps -ef | grep demo
builder 3337147 3336773 2 08:24 pts/4 00:00:01 target/debug/demo
builder 3338391 3338139 0 08:25 pts/11 00:00:00 grep --color=auto demo
builder@DESKTOP-QADGF36:~$ kill 3337147
VHS
I wanted to run some VHS Tapes. I never heard of vhs (other than literal cassettes I have because I’m old)
To do that we need to ensure we have ttyd installed (while ttyd is in the apt repos, its too old for this example, so we will use homebrew)
$ brew install ttyd
Then the VHS charm. For this, I needed to sort out my charm apt repo first
$ sudo mkdir -p /etc/apt/keyrings
$ curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
$ echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
$ sudo apt update
$ sudo apt install vhs
I can now run the VHS examples.
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/rattui/ratatui$ vhs ./examples/vhs/hello-world.tape
File: ./examples/vhs/hello-world.tape
[launcher.Browser]2026/07/07 08:02:39 Download: https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/1321438/chrome-linux.zip
[launcher.Browser]2026/07/07 08:02:39 Progress: 00%
[launcher.Browser]2026/07/07 08:02:40 Progress: 37%
[launcher.Browser]2026/07/07 08:02:41 Progress: 72%
[launcher.Browser]2026/07/07 08:02:41 Unzip: /home/builder/.cache/rod/browser/chromium-1321438
[launcher.Browser]2026/07/07 08:02:41 Progress: 00%
[launcher.Browser]2026/07/07 08:02:42 Progress: 23%
[launcher.Browser]2026/07/07 08:02:43 Progress: 43%
[launcher.Browser]2026/07/07 08:02:44 Progress: 78%
[launcher.Browser]2026/07/07 08:02:45 Downloaded: /home/builder/.cache/rod/browser/chromium-1321438
Output .gif target/hello-world.gif
Set Theme Aardvark Blue
Set Width 1200
Set Height 200
Hide
Type cargo run -p hello-world
Enter 1
Sleep 1s
Show
Sleep 5s
Creating target/hello-world.gif...
Host your GIF on vhs.charm.sh: vhs publish <file>.gif
This basically makes a recording of a terminal (hence “VHS”)
So going back to that demo app, which I recorded with ShareX then had to force kill the process, let’s do it as a tape instead
builder@DESKTOP-QADGF36:~/Workspaces/rattui/ratatui$ vhs ./examples/vhs/demo.tape
File: ./examples/vhs/demo.tape
Output .gif target/demo.gif
Set Theme Aardvark Blue
Set Width 1200
Set Height 1200
Set PlaybackSpeed 0.5
Hide
Type cargo run -p demo
Enter 1
Sleep 2s
Show
Sleep 1s
Down 1s 12
Right 1
Sleep 4s
Right 1
Sleep 4s
Creating target/demo.gif...
Host your GIF on vhs.charm.sh: vhs publish <file>.gif
Antigravity help
I’m a bit out of my element in the Rust programming language. I can read it, but cannot write it offhand.
So I’ll use Google Antigravity to get this going. I would like an anagram TUI.
Agy made me a simple app as I requested
Here you can see it run, then I do an update with agy and run again
Let’s look at the actual files created
builder@DESKTOP-QADGF36:~/Workspaces/rattui/ratatui$ tree examples/apps/anagram-tui/
examples/apps/anagram-tui/
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
We have a basic cargo.toml
[package]
name = "anagram-tui"
version = "0.1.0"
publish = false
license.workspace = true
edition.workspace = true
rust-version.workspace = true
[dependencies]
color-eyre.workspace = true
crossterm.workspace = true
ratatui.workspace = true
[lints]
workspace = true
And the Rust app
/// An interactive TUI application built with Ratatui to find anagrams of a entered name/word.
///
/// It loads words from `/usr/share/dict/words` on startup, falling back to a built-in
/// dictionary of classic anagram pairs if the file is unavailable.
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::time::{Duration, Instant};
use color_eyre::Result;
use crossterm::event::{self, KeyCode, KeyEventKind};
use ratatui::layout::{Constraint, Layout, Position};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Block, BorderType, List, ListItem, ListState, Paragraph};
use ratatui::{DefaultTerminal, Frame};
const FALLBACK_WORDS: &[&str] = &[
"silent", "listen", "enlist", "tinsel", "inlets", "stone", "tones", "notes", "steno", "onset",
"post", "stop", "tops", "pots", "spot", "opts", "meat", "team", "tame", "mate", "pears",
"spear", "parse", "reaps", "spare", "heart", "earth", "hater", "rat", "art", "tar", "cat",
"act", "dog", "god", "anagram", "nag a ram", "hello", "world", "rust", "tui", "ratatui",
"baker", "brake", "break", "care", "race", "acre", "live", "evil", "veil", "vile", "lemon",
"melon", "dormitory", "dirty room", "astronomer", "moon starer", "schoolmaster", "the classroom",
"conversation", "voices rant on", "the eyes", "they see", "eleven plus two", "twelve plus one",
];
struct Dictionary {
map: HashMap<String, Vec<String>>,
total_words: usize,
is_fallback: bool,
}
impl Dictionary {
fn load() -> Self {
let mut map = HashMap::new();
let mut total_words = 0;
let mut is_fallback = false;
if let Ok(file) = File::open("/usr/share/dict/words") {
let reader = BufReader::new(file);
for line in reader.lines() {
if let Ok(word) = line {
let trimmed = word.trim();
if trimmed.is_empty() {
continue;
}
// Filter out single character words except 'a' and 'i'
let char_count = trimmed.chars().filter(|c| c.is_alphabetic()).count();
if char_count == 1 {
let lower = trimmed.to_lowercase();
if lower != "a" && lower != "i" {
continue;
}
}
let key = Self::signature(trimmed);
if !key.is_empty() {
let entry = map.entry(key).or_insert_with(Vec::new);
let word_str = trimmed.to_string();
if !entry.contains(&word_str) {
entry.push(word_str);
total_words += 1;
}
}
}
}
}
// If dictionary is empty or failed to load, load fallback words
if map.is_empty() {
is_fallback = true;
for &word in FALLBACK_WORDS {
let key = Self::signature(word);
if !key.is_empty() {
let entry = map.entry(key).or_insert_with(Vec::new);
let word_str = word.to_string();
if !entry.contains(&word_str) {
entry.push(word_str);
total_words += 1;
}
}
}
}
Self {
map,
total_words,
is_fallback,
}
}
fn signature(word: &str) -> String {
let mut chars: Vec<char> = word
.chars()
.filter(|c| c.is_alphabetic())
.map(|c| c.to_ascii_lowercase())
.collect();
chars.sort_unstable();
chars.into_iter().collect()
}
fn is_sub_multiset(b: &str, a: &str) -> bool {
if b.len() < a.len() {
return false;
}
let mut a_chars = a.chars().peekable();
for b_char in b.chars() {
if let Some(&a_char) = a_chars.peek() {
if b_char == a_char {
a_chars.next();
} else if b_char > a_char {
return false;
}
}
}
a_chars.peek().is_none()
}
fn subtract_signature(b: &str, a: &str) -> Option<String> {
if b.len() < a.len() {
return None;
}
let mut result = String::with_capacity(b.len() - a.len());
let mut a_chars = a.chars().peekable();
for b_char in b.chars() {
if let Some(&a_char) = a_chars.peek() {
if b_char == a_char {
a_chars.next();
continue;
} else if b_char > a_char {
return None;
}
}
result.push(b_char);
}
if a_chars.peek().is_none() {
Some(result)
} else {
None
}
}
fn search(
remaining_sig: &str,
current_words: &mut Vec<String>,
results: &mut Vec<String>,
candidates: &[(&String, &Vec<String>)],
clean_input: &str,
start_time: Instant,
max_results: usize,
max_words: usize,
) {
if remaining_sig.is_empty() {
let phrase = current_words.join(" ");
let clean_phrase = phrase
.chars()
.filter(|c| c.is_alphabetic())
.collect::<String>()
.to_lowercase();
if clean_phrase != clean_input {
results.push(phrase);
}
return;
}
if current_words.len() >= max_words {
return;
}
if results.len() >= max_results {
return;
}
if start_time.elapsed() > Duration::from_millis(50) {
return;
}
for (key, words) in candidates {
if let Some(next_sig) = Self::subtract_signature(remaining_sig, key) {
for word in *words {
current_words.push(word.clone());
Self::search(
&next_sig,
current_words,
results,
candidates,
clean_input,
start_time,
max_results,
max_words,
);
current_words.pop();
if results.len() >= max_results || start_time.elapsed() > Duration::from_millis(50) {
return;
}
}
}
}
}
fn find_anagrams(&self, name: &str) -> Vec<String> {
let input_sig = Self::signature(name);
if input_sig.is_empty() {
return Vec::new();
}
let clean_input = name
.chars()
.filter(|c| c.is_alphabetic())
.collect::<String>()
.to_lowercase();
let mut candidates: Vec<(&String, &Vec<String>)> = self
.map
.iter()
.filter(|(key, _)| Self::is_sub_multiset(&input_sig, key))
.collect();
candidates.sort_by_key(|(key, _)| std::cmp::Reverse(key.len()));
let mut results = Vec::new();
let mut current_words = Vec::new();
let start_time = Instant::now();
Self::search(
&input_sig,
&mut current_words,
&mut results,
&candidates,
&clean_input,
start_time,
1000,
4,
);
results.sort_by(|a, b| {
let a_words = a.split_whitespace().count();
let b_words = b.split_whitespace().count();
a_words.cmp(&b_words).then_with(|| a.cmp(b))
});
results.dedup();
results
}
}
struct App {
dictionary: Dictionary,
input: String,
character_index: usize,
anagrams: Vec<String>,
search_duration: Duration,
list_state: ListState,
}
impl App {
fn new(dictionary: Dictionary) -> Self {
Self {
dictionary,
input: String::new(),
character_index: 0,
anagrams: Vec::new(),
search_duration: Duration::ZERO,
list_state: ListState::default(),
}
}
fn move_cursor_left(&mut self) {
if self.character_index > 0 {
self.character_index -= 1;
}
}
fn move_cursor_right(&mut self) {
let char_count = self.input.chars().count();
if self.character_index < char_count {
self.character_index += 1;
}
}
fn enter_char(&mut self, new_char: char) {
let mut chars: Vec<char> = self.input.chars().collect();
chars.insert(self.character_index, new_char);
self.input = chars.into_iter().collect();
self.character_index += 1;
self.update_anagrams();
}
fn delete_char(&mut self) {
if self.character_index > 0 {
let mut chars: Vec<char> = self.input.chars().collect();
chars.remove(self.character_index - 1);
self.input = chars.into_iter().collect();
self.character_index -= 1;
self.update_anagrams();
}
}
fn update_anagrams(&mut self) {
let start = Instant::now();
self.anagrams = self.dictionary.find_anagrams(&self.input);
self.search_duration = start.elapsed();
self.list_state.select(None);
if !self.anagrams.is_empty() {
self.list_state.select(Some(0));
}
}
fn select_next(&mut self) {
if self.anagrams.is_empty() {
self.list_state.select(None);
return;
}
let i = match self.list_state.selected() {
Some(i) => {
if i >= self.anagrams.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.list_state.select(Some(i));
}
fn select_previous(&mut self) {
if self.anagrams.is_empty() {
self.list_state.select(None);
return;
}
let i = match self.list_state.selected() {
Some(i) => {
if i == 0 {
self.anagrams.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.list_state.select(Some(i));
}
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
loop {
terminal.draw(|frame| self.render(frame))?;
if let Some(key) = event::read()?.as_key_press_event() {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Esc => {
return Ok(());
}
KeyCode::Char(to_insert) => {
self.enter_char(to_insert);
}
KeyCode::Backspace => {
self.delete_char();
}
KeyCode::Left => {
self.move_cursor_left();
}
KeyCode::Right => {
self.move_cursor_right();
}
KeyCode::Down => {
self.select_next();
}
KeyCode::Up => {
self.select_previous();
}
_ => {}
}
}
}
}
}
fn render(&mut self, frame: &mut Frame) {
let chunks = Layout::vertical([
Constraint::Length(3), // Header
Constraint::Length(3), // Input box
Constraint::Min(1), // Main results area
Constraint::Length(1), // Footer / Status bar
])
.split(frame.area());
// 1. Header
let title_content = vec![
Span::styled(" RATATUI ANAGRAM FINDER ", Style::default().fg(Color::Yellow).bold()),
Span::styled(" - Hello World TUI Edition", Style::default().fg(Color::Cyan)),
];
let header_paragraph = Paragraph::new(Line::from(title_content))
.block(
Block::bordered()
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Magenta)),
);
frame.render_widget(header_paragraph, chunks[0]);
// 2. Input Box
let input_text = Paragraph::new(self.input.as_str())
.style(Style::default().fg(Color::Yellow))
.block(
Block::bordered()
.title(" Type Your Name / Word ")
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Cyan)),
);
frame.render_widget(input_text, chunks[1]);
// Set cursor position in the input field
frame.set_cursor_position(Position::new(
chunks[1].x + self.character_index as u16 + 1,
chunks[1].y + 1,
));
// 3. Main results area (splits into list on left, stats/tips on right)
let main_chunks = Layout::horizontal([
Constraint::Percentage(60),
Constraint::Percentage(40),
])
.split(chunks[2]);
// Left side: Anagrams List
let list_items: Vec<ListItem> = self
.anagrams
.iter()
.map(|word| {
ListItem::new(Line::from(vec![
Span::styled("• ", Style::default().fg(Color::Cyan)),
Span::raw(word),
]))
})
.collect();
let list_title = format!(" Anagrams found for '{}' ", self.input);
let list_widget = List::new(list_items)
.block(
Block::bordered()
.title(if self.input.is_empty() {
" Enter a word to see anagrams ".to_string()
} else {
list_title
})
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Green)),
)
.highlight_style(
Style::default()
.bg(Color::Rgb(40, 40, 40))
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
frame.render_stateful_widget(list_widget, main_chunks[0], &mut self.list_state);
// Right side: Info / Stats
let mut info_text = Vec::new();
info_text.push(Line::from(""));
info_text.push(Line::from(vec![
Span::styled("Dictionary Mode: ", Style::default().fg(Color::DarkGray)),
if self.dictionary.is_fallback {
Span::styled("Fallback (Built-in)", Style::default().fg(Color::Red))
} else {
Span::styled("System Dict (/usr/share/dict/words)", Style::default().fg(Color::Green))
},
]));
info_text.push(Line::from(vec![
Span::styled("Dictionary Size: ", Style::default().fg(Color::DarkGray)),
Span::styled(
format!("{} words", self.dictionary.total_words),
Style::default().fg(Color::White),
),
]));
if !self.input.is_empty() {
info_text.push(Line::from(""));
info_text.push(Line::from(vec![
Span::styled("Results count: ", Style::default().fg(Color::DarkGray)),
Span::styled(
format!("{} anagrams", self.anagrams.len()),
Style::default().fg(Color::White).bold(),
),
]));
info_text.push(Line::from(vec![
Span::styled("Search time: ", Style::default().fg(Color::DarkGray)),
Span::styled(
format!("{:.2?}", self.search_duration),
Style::default().fg(Color::LightGreen),
),
]));
}
info_text.push(Line::from(""));
info_text.push(Line::from(Span::styled("Try typing these names/words/phrases:", Style::default().fg(Color::Cyan).underlined())));
info_text.push(Line::from(" • Listen (gives 'silent', 'enlist', ...)"));
info_text.push(Line::from(" • Stop (gives 'post', 'pots', ...)"));
info_text.push(Line::from(" • dirty room (gives 'dormitory')"));
info_text.push(Line::from(" • moon starer (gives 'astronomer')"));
let info_paragraph = Paragraph::new(info_text).block(
Block::bordered()
.title(" Statistics & Tips ")
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Blue)),
);
frame.render_widget(info_paragraph, main_chunks[1]);
// 4. Footer
let footer_text = Text::from(Line::from(vec![
Span::styled(" Esc ", Style::default().bg(Color::Rgb(50, 50, 50)).fg(Color::White).bold()),
Span::styled(" Exit | ", Style::default().fg(Color::DarkGray)),
Span::styled(" Up/Down ", Style::default().bg(Color::Rgb(50, 50, 50)).fg(Color::White).bold()),
Span::styled(" Scroll Anagrams | ", Style::default().fg(Color::DarkGray)),
Span::styled(" Backspace ", Style::default().bg(Color::Rgb(50, 50, 50)).fg(Color::White).bold()),
Span::styled(" Delete | Type to search instantly", Style::default().fg(Color::DarkGray)),
]));
let footer_paragraph = Paragraph::new(footer_text);
frame.render_widget(footer_paragraph, chunks[3]);
}
}
fn main() -> Result<()> {
color_eyre::install()?;
let dictionary = Dictionary::load();
let app = App::new(dictionary);
ratatui::run(|terminal| app.run(terminal))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_anagrams() {
let dictionary = Dictionary::load();
// Single word anagram test
let silent_anagrams = dictionary.find_anagrams("silent");
assert!(silent_anagrams.contains(&"listen".to_string()));
assert!(silent_anagrams.contains(&"enlist".to_string()));
assert!(!silent_anagrams.contains(&"silent".to_string()));
// Multi word anagram test depending on dictionary type
if dictionary.is_fallback {
let multi_anagrams = dictionary.find_anagrams("rust tui");
assert!(multi_anagrams.contains(&"tui rust".to_string()));
} else {
let rat_cat_anagrams = dictionary.find_anagrams("rat cat");
assert!(rat_cat_anagrams.contains(&"cat rat".to_string()) || rat_cat_anagrams.contains(&"act tar".to_string()));
}
}
}
the parts that Ratatui bring are really there in the main:
fn main() -> Result<()> {
color_eyre::install()?;
let dictionary = Dictionary::load();
let app = App::new(dictionary);
ratatui::run(|terminal| app.run(terminal))
}
Essentially we fire up main, the “color_eyer” is just about colour coding error output in the terminal. The dictionary pulls in the standard Linux words list (from /usr/share/dict/words for the most part) and then the we fire up the app with App::new in Ratatui’s framework on the last line.
Let’s use VHS to make a tape of this with my name:
builder@DESKTOP-QADGF36:~/Workspaces/rattui/ratatui$ cat ./examples/vhs/anagram.tape
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/vhs/calendar-explorer.tape`
Output "target/anagram-tui.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 800
Hide
Type "cargo run -p anagram-tui"
Enter
Sleep 3s
Show
Set TypingSpeed 1s
Type "I"
Type "s"
Type "a"
Type 'a'
Type 'c'
Enter
Sleep 2s
Show
Now we can run it to build the GIF
builder@DESKTOP-QADGF36:~/Workspaces/rattui/ratatui$ vhs ./examples/vhs/anagram.tape
File: ./examples/vhs/anagram.tape
Output .gif target/anagram-tui.gif
Set Theme Aardvark Blue
Set Width 1200
Set Height 800
Hide
Type cargo run -p anagram-tui
Enter 1
Sleep 3s
Show
Set TypingSpeed 1s
Type I
Type s
Type a
Type a
Type c
Enter 1
Sleep 2s
Show
Creating target/anagram-tui.gif...
Host your GIF on vhs.charm.sh: vhs publish <file>.gif
Release mode
We can use cargo run -p anagram-tui --release to run this in an optimized release mode. It takes a bit longer to compile.
builder@DESKTOP-QADGF36:~/Workspaces/rattui/ratatui$ cargo run -p anagram-tui --release
Compiling proc-macro2 v1.0.106
Compiling unicode-ident v1.0.18
Compiling quote v1.0.45
Compiling libc v0.2.175
Compiling cfg-if v1.0.1
Compiling equivalent v1.0.2
Compiling rustversion v1.0.22
Compiling foldhash v0.2.0
Compiling allocator-api2 v0.2.21
Compiling ident_case v1.0.1
Compiling either v1.15.0
Compiling bitflags v2.13.0
Compiling strsim v0.11.1
Compiling thiserror v2.0.18
Compiling autocfg v1.5.0
Compiling heck v0.5.0
Compiling once_cell v1.21.3
Compiling unicode-segmentation v1.13.3
Compiling instability v0.3.12
Compiling itoa v1.0.15
Compiling ryu v1.0.20
Compiling itertools v0.14.0
Compiling signal-hook v0.3.18
Compiling static_assertions v1.1.0
Compiling unicode-width v0.2.2
Compiling parking_lot_core v0.9.11
Compiling tracing-core v0.1.36
Compiling itertools v0.15.0
Compiling scopeguard v1.2.0
Compiling lock_api v0.4.13
Compiling hashbrown v0.16.1
Compiling hashbrown v0.17.1
Compiling log v0.4.27
Compiling smallvec v1.15.1
Compiling critical-section v1.2.0
Compiling convert_case v0.7.1
Compiling lazy_static v1.5.0
Compiling indoc v2.0.7
Compiling rustix v1.0.8
Compiling sharded-slab v0.1.7
Compiling thread_local v1.1.9
Compiling pin-project-lite v0.2.16
Compiling litrs v1.0.0
Compiling owo-colors v4.2.2
Compiling linux-raw-sys v0.9.4
Compiling castaway v0.2.4
Compiling num_threads v0.1.7
Compiling deranged v0.5.8
Compiling signal-hook-registry v1.4.6
Compiling compact_str v0.9.1
Compiling syn v2.0.117
Compiling mio v1.0.4
Compiling object v0.36.7
Compiling powerfmt v0.2.0
Compiling lru v0.18.0
Compiling num-conv v0.2.2
Compiling time-core v0.1.9
Compiling parking_lot v0.12.4
Compiling tracing-subscriber v0.3.23
Compiling document-features v0.2.12
Compiling unicode-truncate v2.0.1
Compiling tracing v0.1.44
Compiling line-clipping v0.3.7
Compiling color-spantrace v0.3.0
Compiling gimli v0.31.1
Compiling signal-hook-mio v0.2.4
Compiling adler2 v2.0.1
Compiling eyre v0.6.12
Compiling memchr v2.7.5
Compiling miniz_oxide v0.8.9
Compiling rustc-demangle v0.1.26
Compiling indenter v0.3.4
Compiling time v0.3.53
Compiling tracing-error v0.2.1
Compiling addr2line v0.24.2
Compiling darling_core v0.23.0
Compiling thiserror-impl v2.0.18
Compiling strum_macros v0.28.0
Compiling derive_more-impl v2.0.1
Compiling derive_more v2.0.1
Compiling crossterm v0.29.0
Compiling kasuari v0.4.12
Compiling backtrace v0.3.75
Compiling darling_macro v0.23.0
Compiling strum v0.28.0
Compiling ratatui-core v0.1.2 (/home/builder/Workspaces/rattui/ratatui/ratatui-core)
Compiling color-eyre v0.6.5
Compiling darling v0.23.0
Compiling ratatui-widgets v0.3.2 (/home/builder/Workspaces/rattui/ratatui/ratatui-widgets)
Compiling ratatui-crossterm v0.1.2 (/home/builder/Workspaces/rattui/ratatui/ratatui-crossterm)
Compiling ratatui-macros v0.7.2 (/home/builder/Workspaces/rattui/ratatui/ratatui-macros)
Compiling ratatui v0.30.2 (/home/builder/Workspaces/rattui/ratatui/ratatui)
Compiling anagram-tui v0.1.0 (/home/builder/Workspaces/rattui/ratatui/examples/apps/anagram-tui)
Finished `release` profile [optimized] target(s) in 18.21s
Running `target/release/anagram-tui`
But now we have a compiled app we could share with others
$ ls -lh target/release/anagram-tui
-rwxr-xr-x 2 builder builder 1.5M Jul 7 09:06 target/release/anagram-tui
Capturing with a screen recorder doesn’t really do it justice. But when i fire up ./target/release/anagram-tui it launches nearly instantly and is incredibly responsive. It doesn’t feel like a NodeJS app or even a python based TUI.
Summary
Ratatui is really quite an excellent Rust-based TUI framework. For those that use Rust, I could see this as a fantastic option.
Even those of us, like myself, who rather suck at Rust, can use it if we leverage some GenAI tooling. I fully admit I couldn’t have cranked out that Anagram TUI in under a month with my current skills. However, Rust is an easy language to read so it would not be hard to keep updating it after a GenAI tool created your TUI.
More interesting to me, however, was the VHS tool. That is just awesome for recording screens and I absolutely will be using that.
e.g. showing nslookup
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/vhs/nslookup.tape`
Output "target/nslookup.gif"
Set Theme "VibrantInk"
Set Width 800
Set Height 300
Type "nslookup harbor.freshbrewed.science"
Enter
Sleep 3s
Show
then run
builder@DESKTOP-QADGF36:~/Workspaces/rattui/ratatui$ vhs ./examples/vhs/nslookup.tape
File: ./examples/vhs/nslookup.tape
Output .gif target/nslookup.gif
Set Theme VibrantInk
Set Width 800
Set Height 300
Type nslookup harbor.freshbrewed.science
Enter 1
Sleep 3s
Show
Creating target/nslookup.gif...
Host your GIF on vhs.charm.sh: vhs publish <file>.gif
Now I have a nice GIF i could drop in a presentation
Or even recording Antigravity itself describing our app (I should have been able to use --cwd or --project for setting the directory but it was not working in WSL so i just used cd first)
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/vhs/agy.tape`
Output "target/agy.gif"
Set Theme "zenbones"
Set Width 1200
Set Height 600
Type "cd /home/builder/Workspaces/rattui/ratatui && agy --dangerously-skip-permissions -p 'describe the app in ./examples/anagram-tui'"
Enter
Sleep 30s
Show