rs-ratatui-crate
Ratatui
Ratatui is an immediate-mode Rust library for building terminal UIs. It renders the entire UI each frame from application state — there is no persistent widget tree. The default backend is Crossterm.
- Crate:
ratatui = "0.30" - Docs: https://docs.rs/ratatui/latest/ratatui/
- MSRV: 1.86.0 (Rust 2024 edition)
- Widget reference: Read references/widgets.md for built-in widget details, styling, and custom widget implementation
- Architecture patterns: Read references/architecture.md for TEA, component, and monolithic patterns, event handling, layout, state management, and testing
Quick Start
Minimal app with ratatui::run() (v0.30+)
use ratatui::{widgets::{Block, Paragraph}, style::Stylize};
fn main() -> Result<(), Box<dyn std::error::Error>> {
ratatui::run(|terminal| {
loop {
terminal.draw(|frame| {
let greeting = Paragraph::new("Hello, Ratatui!")
.centered()
.yellow()
.block(Block::bordered().title("Welcome"));
frame.render_widget(greeting, frame.area());
})?;
if crossterm::event::read()?.is_key_press() {
break Ok(());
}
}
})
}
ratatui::run() calls init() before and restore() after the closure — handles terminal setup/teardown automatically.
App with init()/restore() (manual control)
fn main() -> Result<()> {
color_eyre::install()?;
let mut terminal = ratatui::init();
let result = run(&mut terminal);
ratatui::restore();
result
}
fn run(terminal: &mut ratatui::DefaultTerminal) -> Result<()> {
loop {
terminal.draw(|frame| { /* render widgets */ })?;
if let Event::Key(key) = crossterm::event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break;
}
}
}
Ok(())
}
Cargo.toml
[dependencies]
ratatui = "0.30"
crossterm = "0.29"
color-eyre = "0.6"
Core Concepts
Rendering
Immediate-mode: call terminal.draw(|frame| { ... }) each tick. Build widgets from state and render — no retained widget tree.
terminal.draw(|frame| {
frame.render_widget(some_widget, frame.area());
frame.render_stateful_widget(stateful_widget, area, &mut state);
})?;
Layout
Use Layout to split areas with constraints. Prefer areas() for destructuring (v0.28+):
let [header, body, footer] = Layout::vertical([
Constraint::Length(3),
Constraint::Min(0),
Constraint::Length(1),
]).areas(frame.area());
Centering with Rect::centered() (v0.30+):
let popup_area = frame.area()
.centered(Constraint::Percentage(60), Constraint::Percentage(40));
Or with Flex::Center:
let [area] = Layout::horizontal([Constraint::Length(40)])
.flex(Flex::Center)
.areas(frame.area());
Constraint types: Length(n), Min(n), Max(n), Percentage(n), Ratio(a, b), Fill(weight).
Widgets
All widgets implement Widget trait (fn render(self, area: Rect, buf: &mut Buffer)). Stateful widgets use StatefulWidget with an associated State type.
Built-in: Block, Paragraph, List, Table, Tabs, Gauge, LineGauge, BarChart, Chart, Canvas, Sparkline, Scrollbar, Calendar, Clear.
Text primitives: Span, Line, Text — all implement Widget.
See references/widgets.md for full API details.
Event Handling
Use Crossterm for input. Always check KeyEventKind::Press:
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => should_quit = true,
KeyCode::Up | KeyCode::Char('k') => scroll_up(),
KeyCode::Down | KeyCode::Char('j') => scroll_down(),
_ => {}
}
}
}
Terminal Setup & Panic Handling
With ratatui::run() (simplest, v0.30+):
fn main() -> Result<(), Box<dyn std::error::Error>> {
ratatui::run(|terminal| { /* app loop */ })
}
With color-eyre panic hook (recommended for init()/restore()):
fn install_hooks() -> Result<()> {
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default().into_hooks();
let panic_hook = panic_hook.into_panic_hook();
std::panic::set_hook(Box::new(move |info| {
ratatui::restore();
panic_hook(info);
}));
eyre_hook.install()?;
Ok(())
}
Architecture
Choose based on complexity. See references/architecture.md for full patterns with code.
| Complexity | Pattern | When |
|---|---|---|
| Simple | Monolithic | Single-screen, few key bindings, no async |
| Medium | TEA (The Elm Architecture) | Multiple modes, form-like interaction |
| Complex | Component | Multi-panel, reusable panes, plugin-like |
TEA (The Elm Architecture) — Summary
struct Model { counter: i32, running: bool }
enum Message { Increment, Decrement, Quit }
fn update(model: &mut Model, msg: Message) {
match msg {
Message::Increment => model.counter += 1,
Message::Decrement => model.counter -= 1,
Message::Quit => model.running = false,
}
}
fn view(model: &Model, frame: &mut Frame) {
let text = format!("Counter: {}", model.counter);
frame.render_widget(Paragraph::new(text), frame.area());
}
Common Patterns
List Navigation with Selection
let mut list_state = ListState::default().with_selected(Some(0));
// Update
match key.code {
KeyCode::Up => list_state.select_previous(),
KeyCode::Down => list_state.select_next(),
_ => {}
}
// Render
let list = List::new(items)
.block(Block::bordered().title("Items"))
.highlight_style(Style::new().reversed())
.highlight_symbol(Line::from(">> ").bold());
frame.render_stateful_widget(list, area, &mut list_state);
Popup Overlay
fn render_popup(frame: &mut Frame, title: &str, content: &str) {
let area = frame.area()
.centered(Constraint::Percentage(60), Constraint::Percentage(40));
frame.render_widget(Clear, area);
let popup = Paragraph::new(content)
.block(Block::bordered().title(title).border_type(BorderType::Rounded))
.wrap(Wrap { trim: true });
frame.render_widget(popup, area);
}
Tabbed Interface
let titles = vec!["Tab1", "Tab2", "Tab3"];
let tabs = Tabs::new(titles)
.block(Block::bordered())
.select(selected_tab)
.highlight_style(Style::new().bold().yellow());
frame.render_widget(tabs, tabs_area);
Custom Widget
struct StatusBar { message: String }
impl Widget for StatusBar {
fn render(self, area: Rect, buf: &mut Buffer) {
Line::from(self.message)
.style(Style::new().bg(Color::DarkGray).fg(Color::White))
.render(area, buf);
}
}
// Implement for reference to avoid consuming the widget:
impl Widget for &StatusBar {
fn render(self, area: Rect, buf: &mut Buffer) {
Line::from(self.message.as_str())
.style(Style::new().bg(Color::DarkGray).fg(Color::White))
.render(area, buf);
}
}
Text Input with tui-input
[dependencies]
tui-input = "0.11"
use tui_input::Input;
use tui_input::backend::crossterm::EventHandler;
let mut input = Input::default();
// In event handler:
input.handle_event(&crossterm::event::Event::Key(key));
// In render:
let width = area.width.saturating_sub(2) as usize;
let scroll = input.visual_scroll(width);
let input_widget = Paragraph::new(input.value())
.scroll((0, scroll as u16))
.block(Block::bordered().title("Search"));
frame.render_widget(input_widget, area);
frame.set_cursor_position(Position::new(
area.x + (input.visual_cursor().max(scroll) - scroll) as u16 + 1,
area.y + 1,
));
Key Conventions
- Always restore terminal — even on panic. Use
ratatui::run()or install a panic hook - Check
KeyEventKind::Presson all key events - Use
Block::bordered()as standard container - Prefer
Layout::vertical/horizontal([...]).areas(rect)over.split(rect) - Use
Clearwidget before rendering popups/overlays - Implement
Widget for &MyTypewhen the widget should not be consumed on render - Use
ListState,TableState,ScrollbarStatefor scroll/selection tracking - Prefer
color-eyrefor error handling in TUI apps - Use
Rect::centered()(v0.30+) for centering layouts instead of doubleFlex::Center
More from padparadscho/skills
js-gnome-extensions
Build, debug, and maintain GNOME Shell extensions using GJS (GNOME JavaScript). Covers extension anatomy (metadata.json, extension.js, prefs.js, stylesheet.css), ESModule imports, GSettings preferences, popup menus, quick settings, panel indicators, dialogs, notifications, search providers, translations, and session modes. Use when the user wants to: (1) Create a new GNOME Shell extension, (2) Add UI elements like panel buttons, popup menus, quick settings toggles/sliders, or modal dialogs, (3) Implement extension preferences with GTK4/Adwaita, (4) Debug or test an extension, (5) Port an extension to a newer GNOME Shell version (45-49+), (6) Prepare an extension for submission to extensions.gnome.org, (7) Work with GNOME Shell internal APIs (Clutter, St, Meta, Shell, Main).
7js-gnome-apps
Build native GNOME desktop applications using JavaScript (GJS) with GTK 4, Libadwaita, and the GNOME platform. Use when the user wants to create, modify, or debug a GNOME app written in JavaScript/GJS, including UI design with XML or Blueprint, GObject subclassing, Meson build setup, Flatpak packaging, or any task involving GJS bindings for GLib/GIO/GTK4/Adw libraries. Also use when working with `.ui` files, `meson.build`, GResource XML, GSettings schemas, `.desktop` files, or Flatpak manifests in a GJS project context.
6js-stellar-sdk
Guide for building applications with the Stellar JS SDK (@stellar/stellar-sdk). Use when working with the Stellar blockchain in JavaScript/TypeScript — including sending payments, creating accounts, issuing assets, managing trustlines, trading on the DEX, querying Horizon, interacting with Stellar RPC, streaming events, building and signing transactions, multisig, claimable balances, sponsored reserves, SEP-10 auth, federation, fee-bump transactions, and interacting with Soroban smart contracts via the JS SDK. Covers all @stellar/stellar-sdk usage patterns (Horizon module, rpc module, contract module, TransactionBuilder, Keypair, Operation, Asset, etc.).
5rs-soroban-sdk
Expert guidance for building smart contracts on Stellar using the Soroban Rust SDK. Use this skill when working with Soroban smart contracts for tasks including (1) creating new contracts with [contract] and [contractimpl] attributes, (2) implementing storage with Persistent, Temporary, or Instance storage types, (3) working with auth contexts and authorization, (4) handling tokens and Stellar Asset Contracts, (5) writing tests with testutils, (6) deploying contracts, (7) working with events and logging, (8) using crypto functions, (9) debugging contract errors, (10) security best practices and vulnerability prevention, (11) avoiding common security pitfalls like missing authorization, integer overflow, or reinitialization attacks.
5js-stronghold-sdk
Guide for integrating and building with the Stronghold Pay JS SDK and REST API for payment processing. Use when working with Stronghold Pay payment integration, accepting ACH/bank debit payments, linking bank accounts, creating charges/tips, generating PayLinks, or building checkout flows — in sandbox or live environments. Covers Stronghold.Pay.JS drop-in UI, REST API v2 endpoints, PayLink hosted payment pages, customer token management, and payment source handling.
3rs-yew-crate
Expert guidance for building Rust + WebAssembly frontend web applications using the Yew framework (v0.22). Use when creating, modifying, debugging, or architecting Yew applications — including function components, hooks, props, routing, contexts, events, server-side rendering, agents, and Suspense. Covers project setup with Trunk, the html! macro, state management, data fetching, and integration with the broader Yew/WASM ecosystem (yew-router, gloo, wasm-bindgen, web-sys, stylist, yewdux).
2