--- name: rs-ratatui-crate description: "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." --- # 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**: - **MSRV**: 1.86.0 (Rust 2024 edition) - **Widget reference**: Read [references/widgets.md](references/widgets.md) for built-in widget details, styling, and custom widget implementation - **Architecture patterns**: Read [references/architecture.md](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+) ```rust use ratatui::{widgets::{Block, Paragraph}, style::Stylize}; fn main() -> Result<(), Box> { 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) ```rust 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 ```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. ```rust 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+): ```rust let [header, body, footer] = Layout::vertical([ Constraint::Length(3), Constraint::Min(0), Constraint::Length(1), ]).areas(frame.area()); ``` Centering with `Rect::centered()` (v0.30+): ```rust let popup_area = frame.area() .centered(Constraint::Percentage(60), Constraint::Percentage(40)); ``` Or with `Flex::Center`: ```rust 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](references/widgets.md) for full API details. ### Event Handling Use Crossterm for input. Always check `KeyEventKind::Press`: ```rust 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+): ```rust fn main() -> Result<(), Box> { ratatui::run(|terminal| { /* app loop */ }) } ``` With `color-eyre` panic hook (recommended for `init()`/`restore()`): ```rust 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](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 ```rust 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 ```rust 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 ```rust 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 ```rust 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 ```rust 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 ```toml [dependencies] tui-input = "0.11" ``` ```rust 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::Press` on all key events - Use `Block::bordered()` as standard container - Prefer `Layout::vertical/horizontal([...]).areas(rect)` over `.split(rect)` - Use `Clear` widget before rendering popups/overlays - Implement `Widget for &MyType` when the widget should not be consumed on render - Use `ListState`, `TableState`, `ScrollbarState` for scroll/selection tracking - Prefer `color-eyre` for error handling in TUI apps - Use `Rect::centered()` (v0.30+) for centering layouts instead of double `Flex::Center`