2134 added, 1932 removed, shown as one replacement because the pair was too large to align, first 3,000 lines shown. Audit A to A.
---
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.1"
- 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.30.1"
- ```
-
- ### Crate Modularization (v0.30+)
-
- v0.30 introduced a workspace structure. You can depend on specific crates:
-
- ```toml
- # 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:**
- ```toml
- [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)
-
- ## 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);
- ```
-
- ### Flex::SpaceEvenly (v0.30+)
-
- ```rust
- 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);
- ```
-
- ### Overlapping Layouts (v0.29+)
-
- Create layouts where segments share pixels (useful for border overlap):
-
- ```rust
- 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);
- ```
-
- ### Ergonomic Rect Methods (v0.30+)
-
- ```rust
- 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);
- ```
-
- ### 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));
- ```
-
- ### Block Border Merging (v0.30+)
-
- Overlapping borders automatically merge into clean single borders:
-
- ```rust
- 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);
- ```
-
- ### Block Shadow (v0.30.1+)
-
- ```rust
- 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)));
- ```
-
- ### 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);
- ```
-
- ### Table Column Selection (v0.29+)
-
- ```rust
- 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);
- ```
-
- ### Table Column Span (v0.30.1+)
-
- ```rust
- 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);
- ```
-
- ### 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);
- ```
-
- ### Sparkline absent values (v0.29+)
-
- ```rust
- 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
- ```
-
- ### 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);
- ```
-
- ### Canvas/Chart New Markers (v0.30+)
-
- ```rust
- 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'))]);
- ```
-
- ### Canvas/Chart Filled Areas (v0.30.1+)
-
- ```rust
- 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))]);
- ```
-
- ### RatatuiLogo (v0.29+)
-
- ```rust
- 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);
- ```
-
- ### RatatuiMascot (v0.30+)
-
- ```rust
- use ratatui::widgets::RatatuiMascot;
-
- let mascot = RatatuiMascot::default()
- .eye_color(Color::Yellow); // Customize eye color
-
- f.render_widget(mascot, area);
- ```
-
- ### Fill (v0.30.1+)
-
- ```rust
- 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.
- ```
-
- ## Input Handling
-
- ### Event Handling
-
- ```rust
- 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
- }
- _ => {}
- }
- }
- ```
-
- ### State Management
-
- ```rust
- 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;
- }
- }
- }
- ```
-
- ## 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)
-
- // 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
- ```
-
- ### Stylize Trait Methods (v0.30+)
-
- ```rust
- 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);
- ```
-
- ### 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(())
- }
- ```
-
- ## Breaking Changes (v0.29 - v0.30.1)
-
- ### v0.30 Breaking Changes
-
- - **Block::title() removed**: Use `Line` with alignment instead
- ```rust
- // 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`
- ```rust
- // 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**:
- - Requires associated `Error` type
- - Requires `clear_region()` method
-
- - **List::highlight_symbol()**: Now accepts `Into<Line>`
-
- ### v0.29 Breaking Changes
-
- - **Rect::area() returns u32**: Previously returned u16
-
- - **TableState serialization**: Now includes `selected_column` field
-
- - **Sparkline::data()**: No longer `const`
-
- ### Migration Tips
-
- ```rust
- // 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));
- ```
-
- ## Best Practices
-
- ### 1. Separate State
-
- ```rust
- // 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);
- ```
-
- ### 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<String>,
- last_modified: Instant,
- }
-
- // Reuse widget instances where possible
- static BUTTON_STYLE: Lazy<Style> = Lazy::new(|| Style::default().fg(Color::Blue));
- ```
-
- ## Complete Example
-
- ```rust
- 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(())
- }
- ```
-
- ## Advanced State Management Patterns
-
- ### Model-View-Update (MVU/Elm Architecture)
-
- Ideal for predictable data flow in complex TUIs:
-
- ```rust
- 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);
- }
- }
- }
- ```
-
- ### Flux Architecture Pattern
-
- For complex applications with multiple stores:
-
- ```rust
- 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
- }
- ```
-
- ### Component-Based Architecture
-
- Object-oriented approach with trait-based components:
-
- ```rust
- 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);
- }
- }
- }
- ```
-
- ## Widget Composition & Custom Recipes
-
- ### Composing Widgets
-
- Build complex UIs by combining simple widgets:
-
- ```rust
- 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...");
- ```
-
- ### Custom Widget: Progress Bar
-
- ```rust
- 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);
- ```
-
- ### Custom Widget: Modal Dialog
-
- ```rust
- 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());
- ```
-
- ### Reusable Layout Helpers
-
- ```rust
- 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());
- ```
-
- ### Styled Text Building Blocks
-
- ```rust
- 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"))
- }
- ```
-
- ## Ecosystem Libraries
-
- 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.
-
- ```toml
- # Cargo.toml
- [dependencies]
- tachyonfx = "0.2"
- ```
-
- Key features:
- - Compose and layer simple effects
- - Smooth transitions and visual polish
- - Interactive demo available at https://junkdog.github.io/tachyonfx-ftl/
-
- ### Mousefood
-
- An embedded-graphics backend for Ratatui. Supports `no_std` environments.
-
- ```toml
- # Cargo.toml
- [dependencies]
- mousefood = "0.1"
- ```
-
- Key features:
- - Use Ratatui with embedded displays
- - Works with various embedded-graphics draw targets
- - Example: Tuitar - guitar learning tool built with Ratatui & Mousefood
-
- ### Ratzilla
-
- Build terminal-themed web applications with Rust and WebAssembly.
-
- ```toml
- # Cargo.toml
- [dependencies]
- ratzilla = "0.1"
- ```
-
- Key features:
- - Run Ratatui apps in the browser
- - Demo available at https://ratatui.github.io/ratzilla/demo/
-
- ### tui-logger
-
- 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.
-
- ```toml
- # Cargo.toml
- [dependencies]
- tui-logger = "0.14"
- ```
-
- Initialize once at startup, then render the widget in your draw loop:
-
- ```rust
- 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:
- - Hot buffer (1000 entries) + main buffer (10000) so logging never blocks the UI thread — `move_events()` drains hot into main every 10ms
- - Per-target capture and display levels, toggled at runtime via the target selector widget
- - `slog` and `tracing-subscriber` support (features `slog-support`, `tracing-support`)
- - `wait()` / `wait_timeout()` (feature `waiter`) for event-loop-driven redraws without polling
- - Env-var config (`RUST_LOG`) via `set_env_filter_from_env()`
- - Custom formatters via `LogFormatter`
- - File logging alongside the widget
-
- Smart 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`
-
- - **Repository**: https://github.com/gin66/tui-logger
- - **Docs**: https://docs.rs/tui-logger/
- - **DeepWiki**: https://deepwiki.com/gin66/tui-logger
+ version: "1.1.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.30.1"
+ ```
+
+ ### Crate Modularization (v0.30+)
+
+ v0.30 introduced a workspace structure. You can depend on specific crates:
+
+ ```toml
+ # 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:**
+ ```toml
+ [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)
+
+ ## 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);
+ ```
+
+ ### Flex::SpaceEvenly (v0.30+)
+
+ ```rust
+ 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);
+ ```
+
+ ### Overlapping Layouts (v0.29+)
+
+ Create layouts where segments share pixels (useful for border overlap):
+
+ ```rust
+ 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);
+ ```
+
+ ### Ergonomic Rect Methods (v0.30+)
+
+ ```rust
+ 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);
+ ```
+
+ ### 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));
+ ```
+
+ ### Block Border Merging (v0.30+)
+
+ Overlapping borders automatically merge into clean single borders:
+
+ ```rust
+ 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);
+ ```
+
+ ### Block Shadow (v0.30.1+)
+
+ ```rust
+ 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)));
+ ```
+
+ ### 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);
+ ```
+
+ ### Table Column Selection (v0.29+)
+
+ ```rust
+ 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);
+ ```
+
+ ### Table Column Span (v0.30.1+)
+
+ ```rust
+ 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);
+ ```
+
+ ### 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);
+ ```
+
+ ### Sparkline absent values (v0.29+)
+
+ ```rust
+ 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
+ ```
+
+ ### 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);
+ ```
+
+ ### Canvas/Chart New Markers (v0.30+)
+
+ ```rust
+ 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'))]);
+ ```
+
+ ### Canvas/Chart Filled Areas (v0.30.1+)
+
+ ```rust
+ 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))]);
+ ```
+
+ ### RatatuiLogo (v0.29+)
+
+ ```rust
+ 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);
+ ```
+
+ ### RatatuiMascot (v0.30+)
+
+ ```rust
+ use ratatui::widgets::RatatuiMascot;
+
+ let mascot = RatatuiMascot::default()
+ .eye_color(Color::Yellow); // Customize eye color
+
+ f.render_widget(mascot, area);
+ ```
+
+ ### Fill (v0.30.1+)
+
+ ```rust
+ 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.
+ ```
+
+ ## Input Handling
+
+ ### Event Handling
+
+ ```rust
+ 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
+ }
+ _ => {}
+ }
+ }
+ ```
+
+ ### State Management
+
+ ```rust
+ 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;
+ }
+ }
+ }
+ ```
+
+ ## 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)
+
+ // 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
+ ```
+
+ ### Stylize Trait Methods (v0.30+)
+
+ ```rust
+ 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);
+ ```
+
+ ### 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(())
+ }
+ ```
+
+ ## Breaking Changes (v0.29 - v0.30.1)
+
+ ### v0.30 Breaking Changes
+
+ - **Block::title() removed**: Use `Line` with alignment instead
+ ```rust
+ // 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`
+ ```rust
+ // 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**:
+ - Requires associated `Error` type
+ - Requires `clear_region()` method
+
+ - **List::highlight_symbol()**: Now accepts `Into<Line>`
+
+ ### v0.29 Breaking Changes
+
+ - **Rect::area() returns u32**: Previously returned u16
+
+ - **TableState serialization**: Now includes `selected_column` field
+
+ - **Sparkline::data()**: No longer `const`
+
+ ### Migration Tips
+
+ ```rust
+ // 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));
+ ```
+
+ ## Best Practices
+
+ ### 1. Separate State
+
+ ```rust
+ // 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);
+ ```
+
+ ### 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:
+