ratatui-tui

Ratatui TUI Development

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "ratatui-tui" with this command: npx skills add blacktop/dotfiles/blacktop-dotfiles-ratatui-tui

Ratatui TUI Development

Quick Start

Copy template to project:

cp -r ~/.claude/skills/ratatui-tui/assets/templates/<template>/* .

Run:

cargo run

Template Selection

Complexity Template Use Case

Minimal hello-world

Learning, quick demos

Simple simple-app

Single-screen apps, tools

Async async-app

Background tasks, network

Full component-app

Multi-view, config, logging

Decision tree:

  • Need async/network? → async-app

  • Multiple screens/components? → component-app

  • Just a simple tool? → simple-app

  • Learning ratatui? → hello-world

Project Setup

Minimal Cargo.toml

[package] name = "my-tui" version = "0.1.0" edition = "2024"

[dependencies] ratatui = "0.30" crossterm = "0.29" color-eyre = "0.6"

Full Dependencies (component-app)

[dependencies] ratatui = "0.30" crossterm = { version = "0.29", features = ["event-stream"] } color-eyre = "0.6" tokio = { version = "1", features = ["full"] } futures = "0.3" clap = { version = "4", features = ["derive"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = { version = "1", features = ["derive"] } config = "0.15" dirs = "6"

Optional: image support

ratatui-image = { version = "5", features = ["chafa-static"] }

Release Profile

[profile.release] lto = true codegen-units = 1 panic = "abort" strip = true

Core Loop: TEA (The Elm Architecture)

Model → Message → Update → View ↑ | └─────────────────────────┘

struct App { counter: i32, should_quit: bool, }

enum Message { Increment, Decrement, Quit, }

impl App { fn update(&mut self, msg: Message) { match msg { Message::Increment => self.counter += 1, Message::Decrement => self.counter -= 1, Message::Quit => self.should_quit = true, } }

fn view(&#x26;self, frame: &#x26;mut Frame) {
    let text = format!("Counter: {}", self.counter);
    frame.render_widget(Paragraph::new(text), frame.area());
}

}

Styling Rules

Use Stylize trait helpers:

use ratatui::style::Stylize;

// Good "text".bold() "text".dim() "text".cyan() "text".on_dark_gray() "text".bold().cyan()

// Avoid Style::default().fg(Color::White) // hardcoded white Style::default().fg(Color::Black) // hardcoded black Style::new().add_modifier(Modifier::BOLD) // verbose

Color palette:

  • Primary: .cyan() , .green()

  • Error: .red()

  • Warning: .yellow() (sparingly)

  • Muted: .dim() , .dark_gray()

  • Accent: .magenta()

Text wrapping:

use textwrap::wrap; use ratatui::text::Line;

let wrapped: Vec<Line> = wrap(&long_text, width as usize) .into_iter() .map(|cow| Line::from(cow.into_owned())) .collect();

See: references/style-guide.md

Widget Patterns

StatefulWidget

struct MyList { items: Vec<String>, }

struct MyListState { selected: usize, }

impl StatefulWidget for MyList { type State = MyListState;

fn render(self, area: Rect, buf: &#x26;mut Buffer, state: &#x26;mut Self::State) {
    // render with state.selected
}

}

// Usage frame.render_stateful_widget(my_list, area, &mut state);

Layout

let [header, main, footer] = Layout::vertical([ Constraint::Length(1), Constraint::Fill(1), Constraint::Length(1), ]).areas(frame.area());

let [left, right] = Layout::horizontal([ Constraint::Percentage(30), Constraint::Fill(1), ]).areas(main);

Built-in State Types

  • ListState

  • for List widget

  • TableState

  • for Table widget

  • ScrollbarState

  • for Scrollbar

See: references/architecture-patterns.md

Async Event Handling

use crossterm::event::{EventStream, Event, KeyCode}; use futures::StreamExt; use tokio::select;

async fn run(mut app: App) -> Result<()> { let mut events = EventStream::new();

loop {
    // Render
    terminal.draw(|f| app.view(f))?;

    // Handle events
    select! {
        Some(Ok(event)) = events.next() => {
            if let Event::Key(key) = event {
                match key.code {
                    KeyCode::Char('q') => break,
                    KeyCode::Up => app.update(Message::Up),
                    KeyCode::Down => app.update(Message::Down),
                    _ => {}
                }
            }
        }
        // Add other channels here (background tasks, timers)
    }

    if app.should_quit {
        break;
    }
}
Ok(())

}

See: references/async-patterns.md

Image Integration

use ratatui_image::{picker::Picker, StatefulImage, Resize}; use std::thread;

// Query terminal protocol support once at startup let mut picker = Picker::from_query_stdio()?;

// Load and resize in background thread let (tx, rx) = std::sync::mpsc::channel(); thread::spawn(move || { let dyn_img = image::open("photo.png").unwrap(); let protocol = picker.new_protocol(dyn_img, area.into(), Resize::Fit(None)); tx.send(protocol).unwrap(); });

// In render, use StatefulImage for efficient redraw if let Ok(protocol) = rx.try_recv() { image_state = Some(protocol); } if let Some(ref mut img) = image_state { frame.render_stateful_widget(StatefulImage::default(), area, img); }

Key points:

  • Use chafa-static feature for portable binaries

  • Query protocol once, not per-frame

  • Offload resize/encode to background thread

  • Use StatefulImage to avoid re-encoding on redraws

See: references/image-integration.md

Error Handling

use color_eyre::eyre::Result;

fn main() -> Result<()> { // Install hooks before anything else color_eyre::install()?;

// Set panic hook to restore terminal
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
    let _ = crossterm::terminal::disable_raw_mode();
    let _ = crossterm::execute!(
        std::io::stdout(),
        crossterm::terminal::LeaveAlternateScreen
    );
    original_hook(panic_info);
}));

run()

}

Error propagation:

// Use ? for recoverable errors let file = std::fs::read_to_string(path)?;

// Use color_eyre context let config = load_config() .wrap_err("Failed to load configuration")?;

Release Build

cargo build --release

Binary at target/release/<name> .

Size optimization:

[profile.release] lto = true codegen-units = 1 panic = "abort" strip = true opt-level = "z" # size over speed

Templates Overview

hello-world (~25 lines)

Minimal ratatui demo using ratatui::run() .

simple-app (~80 lines)

Synchronous event loop, App struct, basic render.

async-app (~120 lines)

Tokio runtime, EventStream, select! pattern.

component-app (~300 lines)

Full modular structure:

  • main.rs

  • entry point

  • app.rs

  • App state, update logic

  • event.rs

  • event handling

  • ui.rs

  • rendering

  • action.rs

  • Action enum

  • tui.rs

  • terminal setup

  • config.rs

  • configuration with dirs

  • logging.rs

  • tracing setup

Common Patterns

Centered Popup

fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect { let [_, center, _] = Layout::vertical([ Constraint::Percentage((100 - percent_y) / 2), Constraint::Percentage(percent_y), Constraint::Percentage((100 - percent_y) / 2), ]).areas(area);

let [_, center, _] = Layout::horizontal([
    Constraint::Percentage((100 - percent_x) / 2),
    Constraint::Percentage(percent_x),
    Constraint::Percentage((100 - percent_x) / 2),
]).areas(center);

center

}

Key Bindings Display

let help = Line::from(vec![ " q ".bold().cyan(), "quit ".dim(), " ↑↓ ".bold().cyan(), "navigate ".dim(), " Enter ".bold().cyan(), "select ".dim(), ]);

Status Bar

let status = Line::from(vec![ " MODE ".bold().on_cyan(), format!(" {} items ", count).dim().into(), ]);

Checklist

Before shipping:

  • cargo fmt

  • cargo clippy --all-features clean

  • No unwrap() outside tests

  • Panic hook restores terminal

  • cargo build --release succeeds

  • Test on target terminal(s)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

code-quality

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

frontend-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

github

No summary provided by upstream source.

Repository SourceNeeds Review