--- name: ratatui description: "Rust terminal UI framework - widgets, components, layouts, events, input handling, and state management for TUI apps" metadata: author: mte90 version: "1.0.0" tags: - rust - tui - terminal - cli - user-interface - ratatui - ecosystem - tachyonfx - mousefood - ratzilla --- # Ratatui Rust terminal UI framework. ## Overview 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:** - Multiple layout systems (blocks, flex, horizontal, vertical) - Built-in widgets (buttons, checkboxes, calendars, charts, tables) - Event-driven input handling - Cross-platform support - Mouse support - ANSI escape sequences - Multiple buffer rendering ### Installation ```toml # Cargo.toml [dependencies] ratatui = "0.28" ``` ## Quick Start ### Basic Application ```rust 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(()) } ``` ## Layout System ### Block Layout ```rust 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); ``` ### Flex Layout ```rust use ratatui::layout::Flex; let chunks = Layout::default() .direction(Direction::Horizontal) .flex(Flex::Center) // Center content .constraints([Constraint::Length(20)]) .split(area); ``` ### Nested Layouts ```rust 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]); ``` ## Widgets ### Paragraph ```rust 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); ``` ### Block ```rust 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)); ``` ### Button ```rust 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); ``` ### Checkbox ```rust 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); ``` ### List ```rust 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); ``` ### Table ```rust 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); ``` ### Gauge ```rust use ratatui::widgets::Gauge; let gauge = Gauge::default() .label("Progress") .gauge_style(Style::default().fg(Color::Green)) .percent(75); f.render_widget(gauge, area); ``` ### Sparkline ```rust 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); ``` ### Calendar ```rust 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); ``` ### Chart ```rust 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); ``` ## Input Handling ### Event Handling ```rust use ratatui::event::{Event, EventHandler, KeyEvent, MouseEvent}; fn handle_events(events: &mut EventHandler) -> Option { // 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 } _ => {} } } ``` ### State Management ```rust use ratatui::widgets::ListState; struct AppState { items: Vec, selected: usize, list_state: ListState, } impl AppState { fn new(items: Vec) -> 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; } } } ``` ## Styling ### Styles ```rust 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); ``` ### Color Palette ```rust // 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) ``` ### Modifiers ```rust use ratatui::style::Modifier; // Text modifiers Modifier::BOLD Modifier::DIM Modifier::ITALIC Modifier::UNDERLINED Modifier::REVERSED Modifier::HIDDEN Modifier::CROSSED_OUT ``` ## Mouse Support ```rust 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 })?; ``` ## Example: Interactive List ```rust 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(()) } ``` ## Best Practices ### 1. Separate State ```rust // Good: Separate state from view struct App { items: Vec, selected: usize, // ... state } // In draw f.render_stateful_widget(list, area, &mut self.list_state); ``` ### 2. Handle Resize ```rust use ratatui::event::Event; if let Ok(Event::Resize(width, height)) = term.read_event() { term.resize(width, height)?; } ``` ### 3. Panic Hook ```rust // Restore terminal on panic std::panic::set_hook(Box::new(|_| { let _ = ratatui::restore(); })); ``` ### 4. Buffered Rendering ```rust // Render to buffer first for complex UIs let mut terminal = Terminal::new(CrosstermBackend::new(io::BufWriter::new(buf)))?; ``` ## TUI Design Principles ### Keyboard-First Interaction TUIs should prioritize keyboard navigation over mouse interaction: ```rust // 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:** - Display hotkeys prominently in status bars or help sections - Use Vim-like bindings where appropriate (j/k for up/down) - Make destructive actions require confirmation (e.g., 'd' then 'y' to confirm) - Provide context-sensitive help per view ### Visual Hierarchy Use contrast and positioning to guide users: ```rust // 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:** - Primary actions: Bright colors (Cyan, Yellow, Green) - Secondary info: Muted colors (Gray, DarkGray) - Errors/Warnings: Red/Orange with bold modifier - Selected focus: High contrast (inverse or bright bg) - Use borders to separate logical sections ### Immediate Visual Feedback Users need instant feedback on every interaction: ```rust // 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:** - **Progress indicators**: Spinners, progress bars for long operations - **Status messages**: Temporary toast notifications for actions - **Selection highlighting**: Always show what's currently focused - **Mode indicators**: Clear visual distinction between modes (normal/insert) - **Error states**: Red borders, shake animations, or error dialogs ### Responsive Layouts Design for various terminal sizes (80, 132, 256 columns): ```rust // 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:** - Always use `Min()` for minimum readable width - Hide non-essential panels on small terminals - Stack vertically when horizontal space is limited - Test at 80x24, 120x40, and 200x60 ## Usability & Accessibility ### Color Contrast Guidelines Ensure readability across terminal emulators: ```rust // 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:** - Foreground should be significantly brighter than background - Test with grayscale conversion (remove all color, check contrast) - Provide themes for different terminal backgrounds (light/dark) - Avoid red/green combinations (color blindness) - Use text modifiers (bold, underline) as secondary indicators ### Screen Reader Support TUIs have limited screen reader compatibility, but can improve: ```rust // 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:** - Offer a pure CLI fallback mode for screen reader users - Use clear, descriptive labels (not just icons) - Maintain consistent element ordering - Provide verbose help text that explains context - Document keyboard shortcuts in help section ### Discoverability Make features findable without memorization: ```rust // 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:** - Show most-used shortcuts in status bar - Implement command palette (Ctrl+K or /) to search commands - Provide tooltips on hover (mouse support) - Contextual help that changes per view - Progressive disclosure (basic help → full help) ### Error Handling & Recovery Design forgiving interfaces: ```rust // 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:** - Require confirmation for destructive actions - Provide undo/redo where possible - Show preview before committing changes - Clear error messages with recovery steps - Auto-save work in progress ## Performance Optimization ### Minimize Redraws Only update changed regions: ```rust // 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); ``` ### Efficient Event Handling ```rust // 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(); } ``` ### Memory Management ```rust // Pre-allocate buffers for repeated rendering struct RenderCache { buffer: Vec, last_modified: Instant, } // Reuse widget instances where possible static BUTTON_STYLE: Lazy