用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/Mte90/dotfiles --skill ratatui命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
| name | ratatui |
| description | Rust terminal UI framework - widgets, components, layouts, events, input handling, and state management for TUI apps |
| metadata | {"author":"mte90","version":"1.1.0","tags":["rust","tui","terminal","cli","user-interface","ratatui","ecosystem","tachyonfx","mousefood","ratzilla"]} |
Rust terminal UI framework.
Ratatui is a Rust library for building terminal user interfaces (TUI). It provides a set of widgets and tools for creating interactive command-line applications.
Key Features:
# Cargo.toml
[dependencies]
ratatui = "0.30.1"
v0.30 introduced a workspace structure. You can depend on specific crates:
# Cargo.toml
[dependencies]
# Full crate (recommended for most users)
ratatui = "0.30.1"
# Or individual crates for more control
ratatui-core = "0.30.1" # Core types, traits, utilities
ratatui-widgets = "0.30.1" # Built-in widgets
ratatui-crossterm = "0.30.1" # Crossterm backend
ratatui-termion = "0.30.1" # Termion backend
ratatui-termwiz = "0.30.1" # Termwiz backend
ratatui-macros = "0.30.1" # Macro utilities
Feature flags:
[dependencies]
ratatui = { version = "0.30.1", default-features = false, features = [
"crossterm_0_28", # Use crossterm 0.28 (default: 0.29)
"layout-cache", # Enable layout caching (default: enabled)
"palette", # HSLuv color support
"anstyle", # anstyle conversions
] }
MSRV: 1.88.0 (v0.30.1)
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, Paragraph},
Frame, Terminal,
};
use std::io;
fn main() -> io::Result<()> {
// Initialize terminal
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
// Main loop
loop {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(0)])
.split(f.area());
let title = Paragraph::new("Hello, Ratatui!")
.block(Block::bordered().title("Welcome"))
.style(Style::default().fg(Color::Cyan));
f.render_widget(title, chunks[0]);
let instructions = Paragraph::new("Press 'q' to quit")
.block(Block::bordered().title("Instructions"));
f.render_widget(instructions, chunks[1]);
})?;
// Handle events (add your own event handling)
break; // Exit for now
}
Ok(())
}
use ratatui::layout::{Constraint, Direction, Layout};
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(30), // 30%
Constraint::Length(50), // 50 characters
Constraint::Min(10), // At least 10
Constraint::Ratio(1, 4), // 1/4 of remaining
])
.split(area);
use ratatui::layout::Flex;
let chunks = Layout::default()
.direction(Direction::Horizontal)
.flex(Flex::Center) // Center content
.constraints([Constraint::Length(20)])
.split(area);
use ratatui::layout::Flex;
// v0.30+: SpaceEvenly - equal spacing including edges
let chunks = Layout::default()
.direction(Direction::Horizontal)
.flex(Flex::SpaceEvenly)
.constraints([Constraint::Length(20), Constraint::Length(20), Constraint::Length(20)])
.split(area);
// SpaceAround - middle spacers twice the size of edges (CSS-like, v0.30+)
let chunks = Layout::default()
.direction(Direction::Horizontal)
.flex(Flex::SpaceAround)
.constraints([Constraint::Length(20), Constraint::Length(20)])
.split(area);
Create layouts where segments share pixels (useful for border overlap):
use ratatui::layout::Spacing;
// Overlap layouts by -1 spacing
let chunks = Layout::default()
.direction(Direction::Horizontal)
.spacing(Spacing::Overlap) // or .spacing(-1)
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
])
.split(area);
// Example: stacked borders
let stacked = Layout::default()
.direction(Direction::Vertical)
.spacing(Spacing::Overlap)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
])
.split(area);
use ratatui::layout::Rect;
// Center a rect within another
let centered = area.centered(); // Both dimensions
let centered_h = area.centered_horizontally();
let centered_v = area.centered_vertically();
// Create rect outside current with margin
let outer = area.outer(Offset::new(1, 0)); // 1 cell to the right
// Split with compile-time array (v0.30+)
let [left, right] = area.layout::<2>(Direction::Horizontal, &constraints);
let [top, middle, bottom] = area.layout::<3>(Direction::Vertical, &constraints);
// Try versions return Result
let result = area.try_layout::<2>(Direction::Horizontal, &constraints);
let vec = area.layout_vec(Direction::Horizontal, &constraints);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(0),
])
.split(area);
let sub_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[1]);
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
let paragraph = Paragraph::new("Your text here")
.block(Block::bordered().title("Title"))
.style(Style::default().fg(Color::White))
.wrap(Wrap { trim: true });
// Render
f.render_widget(paragraph, area);
use ratatui::widgets::{Block, BorderType, Borders};
let block = Block::bordered()
.title("My Block")
.title_style(Style::default().fg(Color::Yellow))
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Blue));
let inner = Paragraph::new("Content");
f.render_widget(block.inner(area), area);
f.render_widget(inner, block.inner(area));
Overlapping borders automatically merge into clean single borders:
use ratatui::widgets::{Block, BorderType, MergeStrategy};
// Use MergeStrategy to control behavior
let block = Block::bordered()
.title("Merged")
.merge_strategy(MergeStrategy::Merge);
// New BorderType variants (v0.30+)
let block = Block::bordered()
.border_type(BorderType::LightDoubleDashed)
.border_type(BorderType::HeavyDoubleDashed)
.border_type(BorderType::LightTripleDashed)
.border_type(BorderType::HeavyTripleDashed)
.border_type(BorderType::LightQuadrupleDashed)
.border_type(BorderType::HeavyQuadrupleDashed);
use ratatui::widgets::{Block, Shadow};
use ratatui::layout::Offset;
let block = Block::bordered()
.title("Popup")
.shadow(Shadow::dark_shade() // Preset: dark shade effect
.black() // Shadow color
.on_white() // Background color
.offset(Offset::new(2, 1))); // Offset x, y
// Custom shadow
let block = Block::bordered()
.shadow(Shadow::default()
.symbol('░')
.style(Style::default().fg(Color::DarkGray))
.offset(Offset::new(1, 1)));
use ratatui::widgets::Button;
let button = Button::default()
.text("Click Me")
.style(Style::default().fg(Color::White).bg(Color::Blue))
.pressed_style(Style::default().fg(Color::Blue).bg(Color::White));
f.render_widget(button, area);
use ratatui::widgets::Checkbox;
let checkbox = Checkbox::new("Enable feature", true)
.style(Style::default().fg(Color::White))
.check_style(Style::default().fg(Color::Green));
f.render_widget(checkbox, area);
use ratatui::widgets::List, ListItem;
let items = [
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
];
let list = List::new(items)
.block(Block::bordered().title("Items"))
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().fg(Color::Yellow))
.highlight_symbol(">> ");
f.render_widget(list, area);
use ratatui::widgets::{Table, Row, Cell};
let rows = vec![
Row::new(vec!["Row1", "Data1"]),
Row::new(vec!["Row2", "Data2"]),
];
let table = Table::new(
rows,
// Column widths
&[Constraint::Length(10), Constraint::Min(20)],
)
.block(Block::bordered().title("Table"))
.header_style(Style::default().fg(Color::Yellow))
.widths(&[Constraint::Length(10), Constraint::Min(20)]);
f.render_widget(table, area);
use ratatui::widgets::{Table, Row, Cell, TableState};
let mut table_state = TableState::default();
// Column selection methods
table.select_column(2); // Select column 2
table.select_first_column();
table.select_next_column();
table.select_previous_column();
table.select_last_column();
// Cell selection (v0.29+)
table.select_cell();
// Scrolling
table.scroll_right_by(2);
table.scroll_left_by(1);
// Styling
table.column_highlight_style(Style::default().fg(Color::Yellow).bg(Color::DarkGray));
table.cell_highlight_style(Style::default().fg(Color::White).bg(Color::Blue));
f.render_stateful_widget(table, area, &mut table_state);
use ratatui::widgets::{Table, Row, Cell};
let rows = vec![
Row::new(vec![
Cell::new("Name").column_span(2), // Span 2 columns
Cell::new("Score"),
]),
Row::new(vec![
Cell::new("Long Name").column_span(3), // Span 3 columns
]),
];
let table = Table::new(rows, &[Constraint::Length(10), Constraint::Length(10), Constraint::Length(10)])
.block(Block::bordered());
f.render_widget(table, area);
use ratatui::widgets::Gauge;
let gauge = Gauge::default()
.label("Progress")
.gauge_style(Style::default().fg(Color::Green))
.percent(75);
f.render_widget(gauge, area);
use ratatui::widgets::Sparkline;
let data = vec![1, 5, 3, 7, 2, 8, 5, 3, 6, 4];
let sparkline = Sparkline::default()
.data(&data)
.style(Style::default().fg(Color::Cyan))
.bar_set(" ▎▏");
f.render_widget(sparkline, area);
use ratatui::widgets::Sparkline;
// Handle missing/None values distinctly from zero
let data = vec![Some(1), Some(5), None, Some(3), Some(0), None];
let sparkline = Sparkline::default()
.data(&data)
.absent_value_style(Style::default().fg(Color::DarkGray)) // For None
.absent_value_symbol('·'); // Symbol for absent values
use ratatui::widgets::{Calendar, Chrono};
let calendar = Calendar::default()
.block(Block::bordered().title("2024"))
.chrono(Chrono::Monthly)
.show_months(true);
f.render_widget(calendar, area);
use ratatui::widgets::{Chart, Axis, Dataset};
let data = vec![
(0.0, 1.0),
(1.0, 3.0),
(2.0, 2.0),
(3.0, 5.0),
];
let chart = Chart::new(vec![Dataset::default()
.data(&data)
.name("Series")
.style(Style::default().fg(Color::Cyan))])
.block(Block::bordered().title("Chart"))
.x_axis(Axis::default().bounds([0.0, 4.0]))
.y_axis(Axis::default().bounds([0.0, 6.0]));
f.render_widget(chart, area);
use ratatui::widgets::Marker;
// New marker types (v0.30+)
let canvas = Canvas::default()
.marker(Marker::Quadrant) // 2x2 pseudo-pixel
.marker(Marker::Sextant) // 2x3 resolution
.marker(Marker::Octant); // 2x4 resolution (alternative to Braille)
// Custom marker (v0.30.1+)
let canvas = Canvas::default()
.marker(Marker::Custom('+'));
let chart = Chart::new(vec![Dataset::default()
.marker(Marker::Custom('x'))]);
use ratatui::widgets::{Canvas, FilledLine, Marker};
// Canvas: use FilledLine to fill area under line
let canvas = Canvas::default()
.paint(|ctx| {
ctx.draw(&FilledLine {
x1: 0.0,
y1: 0.0,
x2: 10.0,
y2: 5.0,
color: Color::Blue,
});
});
// Chart: use GraphType::Area with Dataset::fill_to_y
use ratatui::widgets::GraphType;
let chart = Chart::new(vec![Dataset::default()
.data(&data)
.graph_type(GraphType::Area)
.fill_to_y(0.0) // Fill area down to y=0
.style(Style::default().fg(Color::Cyan))]);
use ratatui::widgets::RatatuiLogo;
let logo = RatatuiLogo::default();
// Sizes: tiny (2x15), small (2x27)
let logo = RatatuiLogo::tiny();
let logo = RatatuiLogo::small();
f.render_widget(logo, area);
use ratatui::widgets::RatatuiMascot;
let mascot = RatatuiMascot::default()
.eye_color(Color::Yellow); // Customize eye color
f.render_widget(mascot, area);
use ratatui::widgets::Fill;
// Paint entire area with same symbol and style
let fill = Fill::new("█")
.style(Style::default().fg(Color::Blue).bg(Color::Black));
f.render_widget(fill, area);
// Useful for backgrounds, separators, etc.
use ratatui::event::{Event, EventHandler, KeyEvent, MouseEvent};
fn handle_events(events: &mut EventHandler) -> Option<Event> {
// Try to read event (non-blocking)
if let Ok(event) = events.try_read() {
return Some(event);
}
None
}
// Key events
if let Some(Event::Key(key)) = handle_events(&mut handler) {
match key.code {
KeyCode::Char('q') => break,
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => break,
_ => {}
}
}
// Mouse events
if let Some(Event::Mouse(mouse)) = handle_events(&mut handler) {
match mouse.kind {
MouseEventKind::LeftClick => {
// Handle click at mouse.column, mouse.row
}
MouseEventKind::ScrollDown => {
// Handle scroll
}
_ => {}
}
}
use ratatui::widgets::ListState;
struct AppState {
items: Vec<String>,
selected: usize,
list_state: ListState,
}
impl AppState {
fn new(items: Vec<String>) -> Self {
let mut list_state = ListState::default();
list_state.select(Some(0));
Self { items, selected: 0, list_state }
}
fn next(&mut self) {
if let Some(selected) = self.list_state.selected {
let next = (selected + 1) % self.items.len();
self.list_state.select(Some(next));
self.selected = next;
}
}
fn previous(&mut self) {
if let Some(selected) = self.list_state.selected {
let prev = if selected == 0 {
self.items.len() - 1
} else {
selected - 1
};
self.list_state.select(Some(prev));
self.selected = prev;
}
}
}
use ratatui::style::{Color, Modifier, Style, Stylize};
let style = Style::default()
.fg(Color::White)
.bg(Color::Black)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC);
// Apply to widget
let paragraph = Paragraph::new("Styled text")
.style(style);
// Terminal colors
Color::Reset // Reset to terminal default
Color::Black
Color::Red
Color::Green
Color::Yellow
Color::Blue
Color::Magenta
Color::Cyan
Color::White
// Bright variants
Color::DarkGray
Color::LightRed
Color::LightGreen
Color::LightYellow
Color::LightBlue
Color::LightMagenta
Color::LightCyan
Color::Gray
// Indexed colors (256-color)
Color::Indexed(42)
// RGB colors
Color::Rgb(255, 128, 0)
// HSLuv colors (v0.29+) - perceptually uniform
// Requires "palette" feature
Color::from_hsluv(Hsluv::new(0.0, 100.0, 50.0)) // Red
// Tuple conversions (v0.30+)
Color::from([255, 0, 0]); // RGB array
Color::from((255, 0, 0)); // RGB tuple
Color::from((255, 0, 0, 255)); // RGBA tuple
use ratatui::style::Stylize;
// Methods directly on Style
let style = Style::new().blue().on_black().bold();
// Styled for primitives (v0.30+)
let styled: Text = "hello".yellow();
let styled: Span = "world".blue().bold();
let styled: Line = "text".red().italic();
// From anstyle (v0.30+)
use ratatui::anstyle::AnsiColor;
let color = Color::from(AnsiColor::Blue);
use ratatui::style::Modifier;
// Text modifiers
Modifier::BOLD
Modifier::DIM
Modifier::ITALIC
Modifier::UNDERLINED
Modifier::REVERSED
Modifier::HIDDEN
Modifier::CROSSED_OUT
use ratatui::event::{Event, EventKind, MouseEventKind};
terminal.draw(|f| {
// Enable mouse handling
let event = Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
column: 10,
row: 5,
..
});
// Handle in event loop
})?;
use ratatui::{
backend::CrosstermBackend,
event::{Event, KeyCode, KeyEventKind},
layout::Constraint,
style::Stylize,
widgets::{Block, Borders, List, ListItem, ListState},
Frame, Terminal,
};
use std::io;
fn main() -> io::Result<()> {
let items = vec![
ListItem::new("Option 1"),
ListItem::new("Option 2"),
ListItem::new("Option 3"),
ListItem::new("Option 4"),
];
let mut list_state = ListState::default();
list_state.select(Some(0));
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
loop {
terminal.draw(|f| {
let list = List::new(items.clone())
.block(Block::bordered().title("Select Option"))
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().fg(Color::Yellow).add_modifier(ratatui::style::Modifier::BOLD))
.highlight_symbol(">> ");
f.render_stateful_widget(list, f.area(), &mut list_state);
})?;
// Handle input
if let Event::Key(key) = terminal.peek_event()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Down => {
if let Some(i) = list_state.selected {
list_state.select(Some((i + 1) % items.len()));
}
}
KeyCode::Up => {
if let Some(i) = list_state.selected {
list_state.select(Some(if i == 0 { items.len() - 1 } else { i - 1 }));
}
}
KeyCode::Enter => {
if let Some(i) = list_state.selected {
println!("Selected: {}", items[i]);
}
}
KeyCode::Char('q') => break,
_ => {}
}
}
}
}
Ok(())
}
Block::title() removed: Use Line with alignment instead
// Old (removed)
Block::new().title("foo")
// New (v0.30+)
Block::new().title(Line::from("foo"))
block::Title deprecated: Use Line directly (will be removed in v0.31)
Style no longer implements Styled: Use methods directly on Style
// Old
let style = Style::default().fg(Color::Blue).apply_to(widget);
// New (v0.30+)
let style = Style::default().blue();
widget.style(style);
Table::highlight_style() deprecated: Use row_highlight_style()
Marker is #[non_exhaustive]: Use Marker::Custom() for custom markers
Backend trait changes:
Error typeclear_region() methodList::highlight_symbol(): Now accepts Into<Line>
Rect::area() returns u32: Previously returned u16
TableState serialization: Now includes selected_column field
Sparkline::data(): No longer const
// Migrate from Block::title() to Line
let block = Block::bordered()
.title(Line::from("Title").centered())
.title_top(Line::from("Subtitle").left_aligned());
// Migrate Table highlight style
table = table.row_highlight_style(Style::default().fg(Color::Yellow));
// Good: Separate state from view
struct App {
items: Vec<Item>,
selected: usize,
// ... state
}
// In draw
f.render_stateful_widget(list, area, &mut self.list_state);
use ratatui::event::Event;
if let Ok(Event::Resize(width, height)) = term.read_event() {
term.resize(width, height)?;
}
// Restore terminal on panic
std::panic::set_hook(Box::new(|_| {
let _ = ratatui::restore();
}));
// Render to buffer first for complex UIs
let mut terminal = Terminal::new(CrosstermBackend::new(io::BufWriter::new(buf)))?;
TUIs should prioritize keyboard navigation over mouse interaction:
// Consistent keybindings across views
match key.code {
// Navigation
KeyCode::Up | KeyCode::Char('k') => move_previous(),
KeyCode::Down | KeyCode::Char('j') => move_next(),
KeyCode::Left | KeyCode::Char('h') => move_left(),
KeyCode::Right | KeyCode::Char('l') => move_right(),
// Actions
KeyCode::Char('a') => add_item(),
KeyCode::Char('d') => delete_item(),
KeyCode::Char('e') => edit_item(),
KeyCode::Enter => select_item(),
KeyCode::Escape => go_back(),
KeyCode::Char('q') => quit(),
// Help
KeyCode::Char('?') | KeyCode::F(1) => show_help(),
_ => {}
}
Key Principles:
Use contrast and positioning to guide users:
// High contrast for important elements
let title = Paragraph::new("Critical Alert")
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD));
// Muted styles for secondary information
let hint = Paragraph::new("Press 'q' to quit")
.style(Style::default().fg(Color::DarkGray));
// Highlight selected items
let selected_style = Style::default()
.fg(Color::Black)
.bg(Color::Yellow)
.add_modifier(Modifier::BOLD);
Design Rules:
Users need instant feedback on every interaction:
// Show loading state
if app.is_loading {
let spinner = ["\\", "|", "/", "-"][app.spinner_frame % 4];
let loading = Paragraph::new(format!("{} Loading...", spinner))
.style(Style::default().fg(Color::Cyan));
f.render_widget(loading, status_area);
app.spinner_frame += 1;
}
// Show confirmation messages
if let Some(message) = app.last_action {
let toast = Paragraph::new(message)
.style(Style::default().fg(Color::Green))
.alignment(Alignment::Center);
f.render_widget(toast, toast_area);
}
Feedback Types:
Design for various terminal sizes (80, 132, 256 columns):
// Use flexible constraints
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Min(20), // Minimum width for sidebar
Constraint::Percentage(50), // Flexible main content
Constraint::Max(40), // Optional info panel
])
.split(area);
// Hide optional panels on small screens
let show_sidebar = width > 100;
let show_info = width > 140 && height > 25;
Responsive Patterns:
Min() for minimum readable widthEnsure readability across terminal emulators:
// Safe color combinations (high contrast)
let good_combo = Style::default().fg(Color::Yellow).bg(Color::Black);
let good_combo2 = Style::default().fg(Color::Cyan).bg(Color::Blue);
// Avoid low-contrast combinations
let bad_combo = Style::default().fg(Color::Green).bg(Color::Blue); // Hard to read
let bad_combo2 = Style::default().fg(Color::DarkGray).bg(Color::Black); // Too dim
Color Best Practices:
TUIs have limited screen reader compatibility, but can improve:
// Provide text alternatives
let aria_label = format!("List of {} items, {} selected", items.len(), selected);
let descriptive_text = Paragraph::new(aria_label)
.style(Style::default().fg(Color::DarkGray));
// Logical reading order (top-to-bottom, left-to-right)
// Avoid complex multi-pane layouts that confuse screen readers
Accessibility Tips:
Make features findable without memorization:
// Context-sensitive help
fn render_help(f: &mut Frame, current_view: &str) {
let help_text = match current_view {
"list" => vec![
"↑/k - Move up",
"↓/j - Move down",
"Enter - Select",
"d - Delete",
"a - Add new item",
"? - Show all shortcuts",
],
"editor" => vec![
"i - Insert mode",
"Esc - Normal mode",
"dd - Delete line",
"yy - Yank line",
"p - Paste",
],
_ => vec!["? - Show available commands"],
};
let help = List::new(help_text)
.block(Block::bordered().title("Shortcuts"));
f.render_widget(help, help_area);
}
Discoverability Patterns:
Design forgiving interfaces:
// Confirmation for destructive actions
if action == Action::Delete && !app.confirmed {
let dialog = ConfirmDialog::new("Delete this item?")
.yes_label("Yes, delete")
.no_label("Cancel")
.danger();
f.render_widget(dialog, popup_area);
return; // Wait for confirmation
}
// Undo support
app.history.push(current_state.clone());
if action == Action::Undo {
app.current_state = app.history.pop().unwrap();
}
Error Prevention:
Only update changed regions:
// Track what changed
if app.state_changed {
terminal.draw(|f| render_app(f, &app))?;
app.state_changed = false;
}
// Use Clear widget for popups to prevent bleeding
use ratatui::widgets::Clear;
Clear.render(popup_area, buf);
// Debounce rapid events
let mut last_render = Instant::now();
let render_interval = Duration::from_millis(16); // ~60fps
if key_event.is_some() || last_render.elapsed() > render_interval {
terminal.draw(|f| render_app(f, &app))?;
last_render = Instant::now();
}
// Pre-allocate buffers for repeated rendering
struct RenderCache {
buffer: Vec<String>,
last_modified: Instant,
}
// Reuse widget instances where possible
static BUTTON_STYLE: Lazy<Style> = Lazy::new(|| Style::default().fg(Color::Blue));
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Stylize},
widgets::{Block, Borders, Paragraph},
Frame, Terminal,
};
use std::io;
struct App {
counter: i32,
}
impl App {
fn new() -> Self {
Self { counter: 0 }
}
fn increment(&mut self) {
self.counter += 1;
}
fn decrement(&mut self) {
self.counter -= 1;
}
fn draw(&self, f: &mut Frame) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(0),
])
.split(f.area());
let title = Paragraph::new(format!("Counter: {}", self.counter))
.block(Block::bordered().title("Counter App"))
.style(Style::default().fg(Color::Cyan))
.centered();
let instructions = Paragraph::new("Use UP/DOWN arrows, 'q' to quit")
.block(Block::bordered().title("Instructions"))
.style(Color::Gray)
.centered();
f.render_widget(title, chunks[0]);
f.render_widget(instructions, chunks[1]);
}
}
fn main() -> io::Result<()> {
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
let mut app = App::new();
loop {
app.draw(&mut terminal);
if let Ok(event) = terminal.read_event() {
use ratatui::event::{Event, KeyCode, KeyEventKind};
if let Event::Key(key) = event {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Up => app.increment(),
KeyCode::Down => app.decrement(),
KeyCode::Char('q') => break,
_ => {}
}
}
}
}
}
Ok(())
}
Ideal for predictable data flow in complex TUIs:
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
// MODEL: Application state
#[derive(Default)]
struct App {
counter: i32,
mode: AppMode,
items: Vec<String>,
selected: Option<usize>,
}
enum AppMode {
Normal,
Insert,
Help,
}
// MESSAGES: Actions that trigger state changes
enum Msg {
Increment,
Decrement,
AddItem(String),
DeleteSelected,
ToggleMode,
Quit,
}
// UPDATE: State transformation logic
fn update(app: &mut App, msg: Msg) {
match msg {
Msg::Increment => app.counter += 1,
Msg::Decrement => app.counter -= 1,
Msg::AddItem(name) => {
app.items.push(name);
if app.selected.is_none() {
app.selected = Some(0);
}
},
Msg::DeleteSelected => {
if let Some(idx) = app.selected {
app.items.remove(idx);
app.selected = if app.items.is_empty() {
None
} else {
Some(idx.min(app.items.len() - 1))
};
}
},
Msg::ToggleMode => {
app.mode = match app.mode {
AppMode::Normal => AppMode::Help,
AppMode::Help => AppMode::Normal,
AppMode::Insert => AppMode::Normal,
};
},
Msg::Quit => std::process::exit(0),
}
}
// VIEW: Render function (pure, no side effects)
fn view(app: &App, frame: &mut ratatui::Frame) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(0),
Constraint::Length(3),
])
.split(frame.area());
// Counter display
let counter_text = format!("Counter: {}", app.counter);
let counter = Paragraph::new(counter_text)
.style(Style::default().fg(Color::Cyan))
.block(Block::bordered().title("Counter"));
frame.render_widget(counter, chunks[0]);
// Item list
let items: Vec<ListItem> = app.items
.iter()
.map(|i| ListItem::new(i.as_str()))
.collect();
let list = List::new(items)
.block(Block::bordered().title("Items"))
.highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
.highlight_symbol(">> ");
frame.render_stateful_widget(
list,
chunks[1],
&mut ListState::default().with_selected(app.selected),
);
// Mode indicator
let mode_text = match app.mode {
AppMode::Normal => "Mode: Normal (↑/↓ to navigate, a to add, d to delete, ? for help)",
AppMode::Help => "Mode: Help (Press '?' to close)",
AppMode::Insert => "Mode: Insert (Not implemented)",
};
let mode = Paragraph::new(mode_text)
.style(Style::default().fg(Color::Green))
.block(Block::bordered().title("Status"));
frame.render_widget(mode, chunks[2]);
}
// MAIN LOOP: Event handling and message dispatch
fn main() -> io::Result<()> {
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
let mut app = App::default();
loop {
terminal.draw(|f| view(&app, f))?;
if let Event::Key(key) = terminal.read_event()? {
let msg = match key.code {
KeyCode::Char('q') => Msg::Quit,
KeyCode::Up | KeyCode::Char('k') => {
if let Some(selected) = app.selected {
app.selected = Some(if selected == 0 {
app.items.len().saturating_sub(1)
} else {
selected - 1
});
continue; // No message, direct state update
}
continue;
},
KeyCode::Down | KeyCode::Char('j') => {
if let Some(selected) = app.selected {
app.selected = Some((selected + 1) % app.items.len().max(1));
continue;
}
continue;
},
KeyCode::Char('a') => Msg::AddItem("New Item".to_string()),
KeyCode::Char('d') => Msg::DeleteSelected,
KeyCode::Char('?') => Msg::ToggleMode,
_ => continue,
};
update(&mut app, msg);
}
}
}
For complex applications with multiple stores:
use std::sync::{Arc, Mutex};
use crossbeam::channel::{unbounded, Sender, Receiver};
// Dispatcher: Central hub for all actions
struct Dispatcher {
sender: Sender<Action>,
subscribers: Vec<Box<dyn Fn(Action) + Send>>,
}
impl Dispatcher {
fn new() -> Self {
let (sender, receiver) = unbounded();
let dispatcher = Self {
sender,
subscribers: Vec::new(),
};
// Spawn listener thread
std::thread::spawn(move || {
for action in receiver {
// Broadcast to all subscribers
// (simplified - real implementation needs proper synchronization)
}
});
dispatcher
}
fn dispatch(&self, action: Action) {
self.sender.send(action).unwrap();
}
fn subscribe(&mut self, callback: Box<dyn Fn(Action) + Send>) {
self.subscribers.push(callback);
}
}
// Actions: Describe what happened
enum Action {
UserPressedKey(KeyCode),
DataLoaded(Vec<String>),
ErrorOccurred(String),
TimerTick,
}
// Stores: Hold application state
struct ItemStore {
items: Vec<String>,
selected: Option<usize>,
}
impl ItemStore {
fn on_action(&mut self, action: &Action) {
match action {
Action::DataLoaded(new_items) => {
self.items = new_items.clone();
self.selected = Some(0);
},
Action::UserPressedKey(KeyCode::Char('d')) => {
if let Some(idx) = self.selected {
self.items.remove(idx);
}
},
_ => {}
}
}
}
// Views: Render based on store state
fn render_items(store: &ItemStore, frame: &mut Frame) {
// Render logic here
}
Object-oriented approach with trait-based components:
trait Component {
fn render(&mut self, frame: &mut Frame, area: Rect);
fn handle_events(&mut self, event: &Event) -> Option<Action>;
fn update(&mut self, action: Action);
}
struct Sidebar {
items: Vec<String>,
selected: usize,
}
impl Component for Sidebar {
fn render(&mut self, frame: &mut Frame, area: Rect) {
let list = List::new(self.items.clone())
.block(Block::bordered().title("Sidebar"));
frame.render_stateful_widget(
list,
area,
&mut ListState::default().with_selected(Some(self.selected)),
);
}
fn handle_events(&mut self, event: &Event) -> Option<Action> {
if let Event::Key(key) = event {
match key.code {
KeyCode::Up => {
self.selected = self.selected.saturating_sub(1);
},
KeyCode::Down => {
self.selected = (self.selected + 1) % self.items.len().max(1);
},
_ => {}
}
}
None
}
fn update(&mut self, _action: Action) {
// Handle state updates
}
}
struct MainContent {
// ...
}
impl Component for MainContent {
// ...
}
struct App {
sidebar: Sidebar,
main: MainContent,
}
impl App {
fn render(&mut self, frame: &mut Frame) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(20), Constraint::Min(0)])
.split(frame.area());
self.sidebar.render(frame, chunks[0]);
self.main.render(frame, chunks[1]);
}
fn handle_event(&mut self, event: Event) {
if let Some(action) = self.sidebar.handle_events(&event) {
self.sidebar.update(action.clone());
self.main.update(action);
}
}
}
Build complex UIs by combining simple widgets:
fn render_card(frame: &mut Frame, area: Rect, title: &str, content: &str) {
let block = Block::bordered()
.title(title)
.border_style(Style::default().fg(Color::Blue))
.border_type(BorderType::Rounded);
let inner = block.inner(area);
let paragraph = Paragraph::new(content)
.style(Style::default().fg(Color::White))
.wrap(Wrap { trim: true });
frame.render_widget(block, area);
frame.render_widget(paragraph, inner);
}
// Usage
render_card(frame, area, "Info", "Some important data here...");
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
widgets::{Widget, Block},
};
struct ProgressBar {
percentage: u16,
label: String,
block: Option<Block<'static>>,
}
impl ProgressBar {
fn new(percentage: u16) -> Self {
Self {
percentage,
label: String::new(),
block: None,
}
}
fn label(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
fn block(mut self, block: Block<'static>) -> Self {
self.block = Some(block);
self
}
}
impl Widget for ProgressBar {
fn render(self, area: Rect, buf: &mut Buffer) {
let inner = self.block.map_or(area, |b| {
let inner = b.inner(area);
b.render(area, buf);
inner
});
if inner.width < 2 || inner.height < 1 {
return;
}
// Draw bar
let bar_width = inner.width.saturating_sub(2) as u16;
let filled = (bar_width * self.percentage) / 100;
let mut x = inner.x + 1;
for i in 0..bar_width {
let cell = if i < filled { "█" } else { "░" };
let style = if i < filled {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::DarkGray)
};
buf.set_string(x, inner.y, cell, style);
x += 1;
}
// Draw label
if !self.label.is_empty() {
let label = format!(" {}% ", self.percentage);
buf.set_string(
inner.x + bar_width + 1,
inner.y,
&label,
Style::default().fg(Color::White),
);
}
}
}
// Usage
let progress = ProgressBar::new(75)
.label("Loading")
.block(Block::bordered().title("Progress"));
frame.render_widget(progress, area);
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
widgets::{Block, Borders, Clear, Paragraph, Widget},
};
struct Modal {
title: String,
message: String,
width: u16,
height: u16,
}
impl Modal {
fn new(title: impl Into<String>, message: impl Into<String>) -> Self {
Self {
title: title.into(),
message: message.into(),
width: 60,
height: 10,
}
}
}
impl Widget for Modal {
fn render(self, area: Rect, buf: &mut Buffer) {
// Calculate centered position
let x = area.x + (area.width.saturating_sub(self.width)) / 2;
let y = area.y + (area.height.saturating_sub(self.height)) / 2;
let modal_area = Rect::new(x, y, self.width, self.height);
// Clear area to prevent content bleeding
Clear.render(modal_area, buf);
// Render modal content
let block = Block::default()
.title(self.title)
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow))
.style(Style::default().bg(Color::Black));
let inner = block.inner(modal_area);
block.render(modal_area, buf);
let paragraph = Paragraph::new(self.message)
.style(Style::default().fg(Color::White))
.wrap(Wrap { trim: true });
paragraph.render(inner, buf);
}
}
// Usage
let modal = Modal::new("Alert", "Operation completed successfully.");
frame.render_widget(modal, frame.area());
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}
// Usage for popups
let popup_area = centered_rect(60, 40, frame.area());
use ratatui::{
style::{Color, Modifier, Style},
text::{Line, Span, Text},
widgets::Paragraph,
};
fn build_styled_header(title: &str, subtitle: &str) -> Paragraph {
let title_line = Line::from(vec![
Span::styled(title, Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)),
Span::raw(" - "),
Span::styled(subtitle, Style::default()
.fg(Color::Gray)
.add_modifier(Modifier::DIM)),
]);
let text = Text::from(vec![title_line]);
Paragraph::new(text)
.alignment(Alignment::Center)
.block(Block::bordered().title("Header"))
}
An effects and animation library for Ratatui applications. Build complex animations by composing and layering simple effects, bringing smooth transitions and visual polish to the terminal.
# Cargo.toml
[dependencies]
tachyonfx = "0.2"
Key features:
An embedded-graphics backend for Ratatui. Supports no_std environments.
# Cargo.toml
[dependencies]
mousefood = "0.1"
Key features:
Build terminal-themed web applications with Rust and WebAssembly.
# Cargo.toml
[dependencies]
ratzilla = "0.1"
Key features:
A logger and smart widget for ratatui — captures log records into a circular
buffer and renders them in a scrollable pane with a per-target level selector.
# Cargo.toml
[dependencies]
tui-logger = "0.14"
Initialize once at startup, then render the widget in your draw loop:
use tui_logger::{init_logger, TuiLoggerWidget};
fn main() {
init_logger(log::LevelFilter::Trace).unwrap();
tui_logger::set_default_level(log::LevelFilter::Debug);
// ... app loop ...
}
// In the draw closure:
frame.render_widget(
TuiLoggerWidget::default()
.block(Block::bordered().title("Logs")),
area,
);
Key features:
move_events() drains hot into main every 10msslog and tracing-subscriber support (features slog-support, tracing-support)wait() / wait_timeout() (feature waiter) for event-loop-driven redraws without pollingRUST_LOG) via set_env_filter_from_env()LogFormatterSmart widget key commands (driven by feeding TuiWidgetEvent to TuiWidgetState::transition()):
| Key | Action |
|---|---|
h | Toggle target selector visibility |
f | Focus selected target only |
+ / - | Increase / decrease captured level |
Right / Left | Increase / decrease shown level |
PageUp / PageDown | Page mode scroll through history |
Esc | Exit page mode |
Space | Hide targets with no enabled level |
Run the demo: cargo run --example demo --features crossterm
A customizable, interactive hierarchical breadcrumb navigation widget for Ratatui. Renders clickable path segments with truncation strategies and ancestor-element dropdowns.
# Cargo.toml
[dependencies]
tui-breadcrumb = "0.1"
Key features:
Separator presets (single/double >, →, /, custom)
Five truncation strategies: Middle, Start, ShortenNames, End, None
Interactive: keyboard (arrows/Tab/Enter) and mouse (click, scroll) navigation
Breadcrumb::from_path() builds from std::path::Path
Unicode-aware via unicode-width
Repository: https://github.com/shadowmkj/tui-breadcrumb
A shadcn-inspired, themeable component library for Ratatui with a small interaction runtime for focus, hover, and event routing. It is a toolkit, not a framework — nothing takes over your app loop.
# Cargo.toml
[dependencies]
ratcn = { version = "0.0.1", features = ["crossterm"] }
Key features:
Button, List, Select, Tabs, Dialog, ToasterWidget, BarChartWidget, Tooltip, ScrollArea, Checkbox, Cycle, ProgressWidgetWidget half + an interactive half; integrates at two call sites (Ratcn::render, Ratcn::handle_event)Theme); Theme::adaptive solves colors from terminal fg/bgratzilla (WASM) in addition to crosstermPreview status (0.0.1) — the API will break; pin exact versions.
A TUI-based, vim-inspired text editor widget for Ratatui. Embeds an editor buffer with vim keybindings, theming, and optional syntax highlighting.
# Cargo.toml
[dependencies]
edtui = "0.11"
Key features:
Vim keybindings by default; Emacs mode available
Custom keybindings and theming
Mouse events (enabled by default)
Copy/paste to system clipboard
Line wrapping and line numbers (absolute or relative)
Single-line mode
Syntax highlighting via syntect
system-editor feature: opens selected text in your $EDITOR
Repository: https://github.com/preiter93/edtui
Terminal plotting library for Rust — "a small grammar of marks, honest axes, millions of points." With the ratatui feature, plot.widget() drops any chart into a Ratatui TUI.
# Cargo.toml
[dependencies]
malevich = { version = "1.17", features = ["ratatui"] }
Key features:
8 marks (line, points, bars, area, cells, range, rule, text) with a statistical layer; presets: scatter, bar, hist, stairs, ecdf, heatmap, density, box_plot, violin, error_bars, trend
Statistical layer other terminal libs lack: box plots (Tukey whiskers), KDE violins, trend lines with R² + confidence band, ECDFs with DKW bands, 2D densities
~10M points in tens of ms via M4 (min/max/first/last) per-column aggregation
Extended-Wilkinson tick placement, exact-decimal labels, SI prefixes, log/calendar/band scales
Rendering ladder: truecolor → 256 → 16 → plain ASCII fallback; pixel feature renders real sixel/kitty/iTerm2 inline images
CLI tool kaz (crate malevich-cli)
Repository: https://github.com/shergin/malevich
crates.io: https://crates.io/crates/malevich
A small but highly customizable file explorer widget for Ratatui — an embeddable file tree/browser with input handling and theming.
# Cargo.toml
[dependencies]
ratatui-explorer = "0.3"
Key features:
Input handling via crossterm (default), termion, or termwiz features
Theming/customization of the explorer display
Toggle hidden files (Ctrl+h)
Configurable keyboard bindings
Repository: https://github.com/tatounee/ratatui-explorer
A simple yet powerful multi-line text editor widget for Ratatui. Embeddable as part of any TUI application as a stateful widget. Now maintained under the ratatui organization.
# Cargo.toml
[dependencies]
ratatui-textarea = "0.9"
Key features:
Multi-line editing with cursor, selection, clipboard
Backend features: crossterm (default), termion, termwiz
search feature (regex), serde, arbitrary
Repository: https://github.com/ratatui/ratatui-textarea
crates.io: https://crates.io/crates/ratatui-textarea
A code editor widget for Ratatui with syntax highlighting powered by Tree-sitter and a ropey text buffer.
# Cargo.toml
[dependencies]
ratatui-code-editor = { version = "0.0.6", features = ["crossterm"] }
Key features:
Tree-sitter syntax highlighting: only visible region highlighted per render, cached per region; 16 languages (Rust, Python, JS/TS, C/C++, Go, C#, Java, HTML, CSS, YAML, TOML, JSON, Bash, Markdown)
Mouse support: clicks, scroll, selection with word/line snapping
Copy/paste via system clipboard (arboard)
Undo/redo, visual text selection (keyboard + mouse)
Customizable themes (vesper default)
Diff views: added/deleted lines, focused diffs with expandable unchanged sections
Code folding via Tree-sitter fold queries
Emoji/Unicode (grapheme-cluster) aware widths, cursor, selection
Repository: https://github.com/vipmax/ratatui-code-editor
Markdown rendering for Ratatui — renders markdown to styled ratatui::text::Lines, plus Mermaid diagrams, syntax highlighting, collapsible trees, and rich scroll widgets.
# Cargo.toml
[dependencies]
ratatui-markdown = { version = "0.3", features = ["preview"] }
Key features:
mermaid)highlight-lang-*)ImageResolver trait (feature image)RenderHooks to override rendering of any block typeMarkdownPreview / MarkdownViewer unified widgetsRichTheme theming (15+ color slots); CJK-aware wrapping; TOML frontmatter strippingLicensed under the Synthetic Source License (SySL) 1.0 — not MIT/Apache; verify compatibility before use.
A menu widget for Ratatui with nested submenu groups.
# Cargo.toml
[dependencies]
tui-menu = "0.3"
Key features:
Nested submenu groups (MenuItem::group)
Intuitive keyboard movement
Generic item data — any Clone type (enums, strings, custom state)
Stateful rendering + event draining (MenuEvent::Selected, menu.reset())
Repository: https://github.com/shuoli84/tui-menu
"ratatui widgets extended edition" — a widget library for ratatui, part of the rat-salsa monorepo which also includes rat-salsa (an event loop for ratatui) and a family of purpose-built sibling crates: rat-widget-extra, rat-cursor, rat-dialog, rat-event, rat-focus, rat-ftable, rat-markdown, rat-menu, rat-popup, rat-reloc, rat-scrolled, rat-text, rat-theme4.
# Cargo.toml
[dependencies]
rat-widget = "3.2"
Key features:
Extended widget library for ratatui
Companion event-loop crate (rat-salsa)
Family of sibling widgets (menus, dialogs, popups, markdown, ftable, scrolled, focus, text, theme)
Repository: https://github.com/thscharler/rat-salsa
crates.io: https://crates.io/crates/rat-widget
Ratatui has a vibrant ecosystem of third-party widgets:
<textarea>Use better-panic for pretty backtraces and human-panic for user-friendly error handling:
# Cargo.toml
[dependencies]
better-panic = "0.3"
human-panic = "1.2"
color-eyre = "0.6"
libc = "1.0"
strip-ansi-escapes = "0.2"
use better_panic::Settings;
pub fn initialize_panic_handler() {
std::panic::set_hook(Box::new(|panic_info| {
// Exit terminal cleanly
crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen).unwrap();
crossterm::terminal::disable_raw_mode().unwrap();
// Show pretty backtrace
Settings::auto()
.most_recent_first(false)
.lineno_suffix(true)
.create_panic_handler()(panic_info);
}));
}
For release builds, use human-panic for user-friendly messages:
use human_panic::{handle_dump, print_msg, Metadata};
pub fn initialize_panic_handler() -> Result<()> {
std::panic::set_hook(Box::new(move |panic_info| {
let meta = Metadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
.authors(format!("authored by {}", env!("CARGO_PKG_AUTHORS")))
.support(format!("You can open a support request at {}", env!("CARGO_PKG_REPOSITORY")));
let file_path = handle_dump(&meta, panic_info);
print_msg(file_path, &meta).expect("human-panic: printing error message failed");
std::process::exit(libc::EXIT_FAILURE);
}));
Ok(())
}
Use color_eyre for beautiful error reports:
# Cargo.toml
[dependencies]
color-eyre = "0.6"
use color_eyre::Result;
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
let terminal = tui::init()?;
let result = run(terminal).wrap_err("run failed");
if let Err(err) = tui::restore() {
eprintln!("failed to restore terminal: {err}");
}
result
}
fn set_panic_hook() {
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let _ = restore();
hook(panic_info);
}));
}
Create a reusable Tui struct with Terminal and EventHandler:
# Cargo.toml
[dependencies]
ratatui = "0.28"
tokio = { version = "1", features = ["sync", "task", "time"] }
tokio-util = "0.7"
futures = "0.3"
color-eyre = "0.6"
use std::ops::{Deref, DerefMut};
use std::time::Duration;
use color_eyre::eyre::Result;
use futures::{FutureExt, StreamExt};
use ratatui::backend::CrosstermBackend as Backend;
use ratatui::crossterm::{
cursor,
event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, Event as CrosstermEvent, KeyEvent, KeyEventKind, MouseEvent},
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
use serde::{Deserialize, Serialize};
use tokio::{sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, task::JoinHandle};
use tokio_util::sync::CancellationToken;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Event {
Init, Quit, Error, Closed, Tick, Render,
FocusGained, FocusLost, Paste(String),
Key(KeyEvent), Mouse(MouseEvent), Resize(u16, u16),
}
pub struct Tui {
pub terminal: ratatui::Terminal<Backend<std::io::Stderr>>,
pub task: JoinHandle<()>,
pub cancellation_token: CancellationToken,
pub event_rx: UnboundedReceiver<Event>,
pub event_tx: UnboundedSender<Event>,
}
impl Tui {
pub fn new() -> Result<Self> {
let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?;
let (event_tx, event_rx) = mpsc::unbounded_channel();
let cancellation_token = CancellationToken::new();
let task = tokio::spawn(async {});
Ok(Self { terminal, task, cancellation_token, event_tx, event_rx })
}
pub fn enter(&mut self) -> Result<()> {
crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?;
if self.mouse {
crossterm::execute!(std::io::stderr(), EnableMouseCapture)?;
}
self.start();
Ok(())
}
pub fn exit(&mut self) -> Result<()> {
self.stop()?;
if crossterm::terminal::is_raw_mode_enabled()? {
self.flush()?;
if self.mouse {
crossterm::execute!(std::io::stderr(), DisableMouseCapture)?;
}
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
crossterm::terminal::disable_raw_mode()?;
}
Ok(())
}
pub async fn next(&mut self) -> Option<Event> {
self.event_rx.recv().await
}
}
impl Drop for Tui {
fn drop(&mut self) {
self.exit().unwrap();
}
}
Use clap for command-line argument parsing:
# Cargo.toml
[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;
#[derive(Parser, Debug)]
#[command(version = version(), about = "My TUI App")]
struct Args {
/// App tick rate in milliseconds
#[arg(short, long, default_value_t = 1000)]
tick_rate: u64,
/// Enable mouse support
#[arg(short, long)]
mouse: bool,
}
fn main() {
let args = Args::parse();
// Use args.tick_rate, args.mouse, etc.
}
Create custom widgets by implementing the Widget trait:
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget, style::Color};
pub struct MyWidget {
content: String,
}
impl Widget for MyWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.left(), area.top(), &self.content, Style::default().fg(Color::Green));
}
}
For stateful widgets, use StatefulWidget:
use ratatui::{buffer::Buffer, layout::Rect, widgets::StatefulWidget, style::Style};
pub struct ListWidget {
items: Vec<String>,
}
pub struct ListState {
selected: usize,
}
impl StatefulWidget for ListWidget {
type State = ListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
// Render items, highlight selected one based on state.selected
}
}
Use Block for framing and titling widgets:
use ratatui::widgets::{Block, BorderType, Borders};
let block = Block::default()
.title("Header")
.borders(Borders::ALL);
// Multiple titles with alignment
let block = Block::default()
.title(Line::from("Left").left_aligned())
.title(Line::from("Center").centered())
.title(Line::from("Right").right_aligned())
.border_style(Style::default().fg(Color::Magenta))
.border_type(BorderType::Rounded)
.borders(Borders::ALL);
f.render_widget(block, area);
Display text with wrapping, alignment, and styling:
use ratatui::widgets::{Block, Borders, Paragraph, Wrap, Alignment};
// Basic usage
let p = Paragraph::new("Hello, World!");
// With styling and borders
let p = Paragraph::new("Hello, World!")
.style(Style::default().fg(Color::Yellow))
.block(Block::default()
.borders(Borders::ALL)
.title("Title")
.border_type(BorderType::Rounded));
// Wrapping
let p = Paragraph::new("A very long text...")
.wrap(Wrap { trim: true });
// Alignment
let p = Paragraph::new("Centered Text")
.alignment(Alignment::Center);
// Styled text with Spans
let p = Paragraph::new(Text::from(vec![
Line::from(vec![
Span::styled("Hello ", Style::default().fg(Color::Yellow)),
Span::styled("World", Style::default().fg(Color::Blue).bg(Color::White)),
])
]));
// Scrolling
let mut p = Paragraph::new("Long content...")
.scroll((1, 0)); // Vertical, horizontal scroll
Use the Clear widget to prevent content bleeding:
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget};
struct Popup {
title: String,
content: String,
}
impl Widget for Popup {
fn render(self, area: Rect, buf: &mut Buffer) {
// Clear area first to avoid leaking content
Clear.render(area, buf);
let block = Block::new()
.title(self.title)
.borders(Borders::ALL);
Paragraph::new(self.content)
.block(block)
.render(area, buf);
}
}
// Usage
frame.render_widget(popup, popup_area);
Use Span, Line, and Text for styled text:
use ratatui::{prelude::*, widgets::*};
// Span - styled text segment
let span = Span::raw("unstyled");
let span = Span::styled("styled", Style::default().fg(Color::Yellow));
let span = "using stylize trait".yellow(); // via Stylize trait
// Line - collection of Spans
let line = Line::from(vec![
"hello".red(),
" ".into(),
"world".red().bold()
]);
let line = Line::from("hello world");
let line: Line = "hello world".yellow().into();
let line = Line::from("hello world").centered();
// Text - collection of Lines
let text = Text::from(vec![
Line::from("line 1"),
Line::from("line 2").blue(),
]);
let text = Text::from("multi\nline\ntext");
// Use with Paragraph
f.render_widget(Paragraph::new(text).block(Block::bordered()), area);
Ratatui supports multiple backends for terminal interaction:
# Cargo.toml
[dependencies]
ratatui = { version = "0.28", default-features = false, features = ["crossterm"] }
crossterm = "0.28"
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::io::stdout;
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
# Cargo.toml
[dependencies]
ratatui = { version = "0.28", features = ["termion"] }
termion = "1.5"
use ratatui::backend::TermionBackend;
use ratatui::Terminal;
use std::io::stdout;
let backend = TermionBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
# Cargo.toml
[dependencies]
ratatui = { version = "0.28", features = ["termwiz"] }
termwiz = "0.22"
use ratatui::backend::TermwizBackend;
use ratatui::Terminal;
use termwiz::caps::Caps;
let backend = TermwizBackend::new(Caps::new()?);
let mut terminal = Terminal::new(backend)?;
Useful for unit testing:
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let backend = TestBackend::new(80, 20);
let mut terminal = Terminal::new(backend);
// Render and check output
terminal.draw(|frame| {
frame.render_widget(Paragraph::new("Test"), frame.area());
}).unwrap();
// Assert on terminal.backend() content
Full no_std compilation for embedded targets (ESP32, STM32H7, PSP, UEFI):
# Cargo.toml
[dependencies]
ratatui = { version = "0.30", default-features = false }
# For embedded without allocator, also add:
# Use custom backend like mousefood
mousefood = "0.1"
# For atomic types (v0.30.1+)
ratatui = { version = "0.30.1", default-features = false, features = ["layout-cache"] }
// In your lib.rs
#![no_std]
extern crate alloc;
// Use with custom backend for no_std
use ratatui::backend::Backend;
Requirements for no_std:
alloc)portable-atomic feature if needed)Simplified terminal lifecycle with ratatui::run():
use ratatui::{ratatui, Terminal};
ratatui::run(|terminal| {
// Your app loop
loop {
terminal.draw(|frame| {
// Render your app
})?;
// Handle events...
break; // Exit
}
Ok(())
})?;
Manual lifecycle with init() and restore():
use ratatui::Terminal;
let backend = CrosstermBackend::new(std::io::stdout());
ratatui::init()?; // Initialize terminal (alternate screen, raw mode)
let terminal = Terminal::new(backend)?;
// ... your app ...
ratatui::restore()?; // Restore terminal (leave alternate screen)
Each backend handles mouse capture differently. Enable mouse events:
use ratatui::crossterm::event::{EnableMouseCapture, DisableMouseCapture};
// Enable mouse capture
crossterm::execute!(stderr(), EnableMouseCapture)?;
// In your event handling
if let Event::Mouse(mouse_event) = event {
match mouse_event.kind {
MouseEventKind::LeftClick { column, row } => { /* handle click */ }
MouseEventKind::ScrollDown => { /* handle scroll */ }
_ => {}
}
}
// Disable on exit
crossterm::execute!(stderr(), DisableMouseCapture)?;
Use insta and TestBackend for snapshot testing:
# Cargo.toml
[dev-dependencies]
insta = "1.39"
use insta::assert_snapshot;
use ratatui::{backend::TestBackend, Terminal, widgets::Paragraph};
#[test]
fn test_render_app() {
let app = App::default();
let mut terminal = Terminal::new(TestBackend::new(80, 20)).unwrap();
terminal
.draw(|frame| frame.render_widget(&app, frame.area()))
.unwrap();
assert_snapshot!(terminal.backend());
}
Run tests and accept snapshots:
cargo test
cargo insta review # Review and accept changes
Render debug info for development:
struct AppState {
show_debug: bool,
// your app state
}
fn render(frame: &mut Frame, state: &AppState) {
// Create area for debug view (0 width when disabled)
let debug_width = u16::from(state.show_debug);
let [main, debug] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Fill(debug_width)
]).areas(frame.area());
// Render main content
frame.render_widget(&state.content, main);
// Render debug info when enabled
if state.show_debug {
let debug_text = Text::from(format!("state: {state:#?}"));
frame.render_widget(debug_text, debug);
}
}
// Toggle with a key (e.g., 'd' key)
KeyCode::Char('d') => state.show_debug = !state.show_debug,