rust-ratatui
Skill0 starsChanged 35 days agoMITView on GitHub
Use when building terminal UIs with Ratatui.
- Installs packages
In this repository15 other files it gives its agents.
- AGENTS.mdAGENTS.md
- rustoriaSkill
- rust-debuggingSkill
- rust-dependenciesSkill
- rust-fundamentalsSkill
- rust-axumSkill
- rust-clapSkill
- rust-dieselSkill
- rust-rayonSkill
- rust-serdeSkill
- rust-tokioSkill
- rust-productionSkill
- rust-code-auditSkill
- rust-stdSkill
- rust-unsafe-checkerSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 2 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 30
Every Ratatui app follows this pattern:
- Distinctive: line 330
Use official templates to get started quickly:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Rust Ratatui — Terminal User Interfaces
Overview
Ratatui is a Rust crate for building terminal user interfaces (TUIs). It provides a simple, flexible way to create
text-based UIs for CLIs, dashboards, and interactive console apps. It uses crossterm for terminal control.
Installation
[dependencies]
ratatui = "0.30"
crossterm = "0.29" # ratatui 0.30.x pairs with crossterm 0.29
color-eyre = "0.6" # Optional: better error handlingCrate layout: since 0.30, ratatui's core types live in the separate
ratatui-corecrate, and each backend (crossterm, termion, termwiz) is its own crate. The umbrellaratatuicrate re-exports the crossterm backend, but the exact re-export path can shift between releases — verify against docs.rs for the exact version you pin.
Core Concepts
1. Application Structure
Every Ratatui app follows this pattern:
use color_eyre::Result;
use crossterm::event::{self, Event};
use ratatui::{DefaultTerminal, Frame};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let result = run(terminal);
ratatui::restore();
result
}
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(render)?;
if matches!(event::read()?, Event::Key(_)) {
break Ok(());
}
}
}
fn render(frame: &mut Frame) {
frame.render_widget("hello world", frame.area());
}2. The Render Loop
use color_eyre::Result;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use ratatui::{DefaultTerminal, Frame};
// `App` is defined in §3.
fn run(mut terminal: DefaultTerminal, app: &mut App) -> Result<()> {
loop {
// 1. Draw the UI
terminal.draw(|frame| render(frame, app))?;
// 2. Handle events
match event::read()? {
// crossterm reports Press, Repeat, and Release (Release/Repeat
// notably on Windows). Filtering for Press means one keystroke
// is handled once — not again on release or auto-repeat.
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Esc | KeyCode::Char('q') => break Ok(()),
KeyCode::Left => app.decrement(),
KeyCode::Right => app.increment(),
_ => {}
},
_ => {}
}
}
}
fn render(frame: &mut Frame, app: &App) {
frame.render_widget(format!("counter: {}", app.counter), frame.area());
}3. App State
struct App {
counter: i32,
should_quit: bool,
}
impl App {
fn new() -> Self {
Self { counter: 0, should_quit: false }
}
fn increment(&mut self) {
self.counter += 1;
}
fn decrement(&mut self) {
self.counter -= 1;
}
fn quit(&mut self) {
self.should_quit = true;
}
}4. Widgets
use ratatui::{
widgets::{Block, Borders, Paragraph},
Frame,
};
fn render(frame: &mut Frame, app: &App) {
let text = Paragraph::new(format!("Counter: {}", app.counter))
.block(Block::default().title("Counter App").borders(Borders::ALL));
frame.render_widget(text, frame.area());
}5. Layout
use ratatui::{
layout::{Constraint, Direction, Layout},
widgets::{Block, Borders, Paragraph},
Frame,
};
fn render(frame: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Header
Constraint::Min(1), // Content
Constraint::Length(3), // Footer
])
.split(frame.area());
// Header
let header = Paragraph::new("My App")
.block(Block::default().borders(Borders::ALL));
frame.render_widget(header, chunks[0]);
// Content
let content = Paragraph::new(format!("Counter: {}", app.counter))
.block(Block::default().borders(Borders::ALL));
frame.render_widget(content, chunks[1]);
// Footer
let footer = Paragraph::new("Press q to quit")
.block(Block::default().borders(Borders::ALL));
frame.render_widget(footer, chunks[2]);
}6. Styling
// Fragment — widget construction, to be rendered inside a render function.
use ratatui::{
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, Borders, Paragraph},
};
let styled = Paragraph::new("Hello")
.style(Style::default()
.fg(Color::Cyan)
.bg(Color::Black)
.add_modifier(Modifier::BOLD));
// Block with styled title
let block = Block::default()
.title(Span::styled(
"Title",
Style::default().fg(Color::White).bg(Color::Blue),
))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow));7. List Widget
// Fragment — `frame` and `area` come from the enclosing render function.
use ratatui::{
style::{Modifier, Style},
widgets::{Block, Borders, List, ListItem, ListState},
};
let items = vec![
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
];
let list = List::new(items)
.block(Block::default().title("Items").borders(Borders::ALL))
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("> ");
// With selection state
let mut state = ListState::default();
state.select(Some(0));
frame.render_stateful_widget(list, area, &mut state);8. Table Widget
// Fragment — construct inside a render function, then `frame.render_widget(table, area)`.
use ratatui::{
layout::Constraint,
style::{Color, Style},
widgets::{Block, Borders, Row, Table},
};
let header = Row::new(vec!["Name", "Age", "City"])
.style(Style::default().fg(Color::Yellow))
.height(1);
let rows = vec![
Row::new(vec!["Alice", "30", "NYC"]),
Row::new(vec!["Bob", "25", "LA"]),
];
let table = Table::new(rows, [Constraint::Length(10), Constraint::Length(5), Constraint::Length(10)])
.header(header)
.block(Block::default().title("Users").borders(Borders::ALL));9. Gauge Widget
// Fragment — construct inside a render function, then `frame.render_widget(gauge, area)`.
use ratatui::{
style::{Color, Style},
widgets::{Block, Borders, Gauge},
};
let gauge = Gauge::default()
.block(Block::default().title("Progress").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Cyan))
.ratio(0.5); // 50%10. Event Handling
use color_eyre::Result;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
// Returns Ok(true) when the app should quit. `App` is defined in §3.
fn handle_events(app: &mut App) -> Result<bool> {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => return Ok(true),
KeyCode::Left => app.decrement(),
KeyCode::Right => app.increment(),
_ => {}
}
}
Event::Mouse(_) => {}
Event::Resize(_, _) => {}
_ => {}
}
Ok(false)
}11. Project Structure
For larger apps, organize into modules:
src/
├── main.rs # Entry point, terminal init/restore
├── app.rs # App state and logic
├── ui.rs # Render functions
├── events.rs # Event handling
└── widgets.rs # Custom widgets// main.rs
mod app;
mod events;
mod ui;
use color_eyre::Result;
use ratatui::DefaultTerminal;
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let result = run(terminal);
ratatui::restore();
result
}
fn run(mut terminal: DefaultTerminal) -> Result<()> {
let mut app = app::App::new();
loop {
terminal.draw(|frame| ui::render(frame, &app))?;
if events::handle_events(&mut app)? {
break Ok(());
}
}
}12. Templates
Use official templates to get started quickly:
cargo install --locked cargo-generate
cargo generate ratatui/templatesAvailable templates:
hello-world— Minimal appcounter— Counter with statejson-editor— JSON editor TUI
Best Practices
- Use
ratatui::init()andratatui::restore()— handles terminal setup/teardown - Separate state from rendering —
app.rsfor state,ui.rsfor rendering - Use
color_eyrefor error handling — better backtraces in terminal apps - Filter
KeyEventKind::Press— crossterm emits Press/Repeat/Release (Release/Repeat notably on Windows); without the filter one keystroke gets handled multiple times - Use
Layoutfor responsive UIs — don't hardcode positions - Use
Constraintfor sizing —Length,Min,Max,Percentage,Ratio - Keep widgets pure — render functions should not mutate state
- Use
render_stateful_widgetfor selections — ListState, TableState - Clear before drawing popups — use
Clearwidget - Test app logic separately — state logic is testable without terminal
When to Use
- Terminal dashboards
- CLI tools with interactive UIs
- System monitors
- Text editors
- Any application that needs a rich terminal interface
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

