Loading...
Loading...
Build terminal user interfaces (TUIs) in Rust with Ratatui (v0.30). Use this skill whenever working with the ratatui crate for creating interactive terminal applications, including: (1) Setting up a new Ratatui project, (2) Creating or modifying terminal UI layouts, (3) Implementing widgets (lists, tables, charts, text, gauges, etc.), (4) Handling keyboard/mouse input and events, (5) Structuring TUI application architecture (TEA, component-based, or monolithic patterns), (6) Writing custom widgets, (7) Managing application state in a TUI context, (8) Terminal setup/teardown and panic handling, (9) Testing TUI rendering with TestBackend. Also triggers for questions about crossterm event handling in a Ratatui context, tui-input, tui-textarea, or any ratatui-* ecosystem crate.
npx skill4agent add padparadscho/skills rs-ratatui-crateratatui = "0.30"ratatui::run()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()init()restore()init()restore()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(())
}[dependencies]
ratatui = "0.30"
crossterm = "0.29"
color-eyre = "0.6"terminal.draw(|frame| { ... })terminal.draw(|frame| {
frame.render_widget(some_widget, frame.area());
frame.render_stateful_widget(stateful_widget, area, &mut state);
})?;Layoutareas()let [header, body, footer] = Layout::vertical([
Constraint::Length(3),
Constraint::Min(0),
Constraint::Length(1),
]).areas(frame.area());Rect::centered()let popup_area = frame.area()
.centered(Constraint::Percentage(60), Constraint::Percentage(40));Flex::Centerlet [area] = Layout::horizontal([Constraint::Length(40)])
.flex(Flex::Center)
.areas(frame.area());Length(n)Min(n)Max(n)Percentage(n)Ratio(a, b)Fill(weight)Widgetfn render(self, area: Rect, buf: &mut Buffer)StatefulWidgetStateBlockParagraphListTableTabsGaugeLineGaugeBarChartChartCanvasSparklineScrollbarCalendarClearSpanLineTextWidgetKeyEventKind::Pressuse 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(),
_ => {}
}
}
}ratatui::run()fn main() -> Result<(), Box<dyn std::error::Error>> {
ratatui::run(|terminal| { /* app loop */ })
}color-eyreinit()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(())
}| 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 |
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());
}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);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);
}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);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);
}
}[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,
));ratatui::run()KeyEventKind::PressBlock::bordered()Layout::vertical/horizontal([...]).areas(rect).split(rect)ClearWidget for &MyTypeListStateTableStateScrollbarStatecolor-eyreRect::centered()Flex::Center