Claude
Skills
Sign in
Back

ratatui-tui

Included with Lifetime
$97 forever

Build terminal UIs with ratatui following 2026 Rust best practices. Use when: (1) Creating new TUI apps, (2) Adding widgets/layouts, (3) Keyboard navigation/state management, (4) Image integration via ratatui-image, (5) Async event handling, (6) Shimmer/loading animations via tui-shimmer, (7) Reviewing TUI code, (8) Release optimization. Covers v0.30.1 API, Elm Architecture, StatefulWidget, color-eyre.

Image & Videoassets

What this skill does


# Ratatui TUI Development

## Quick Start

1. **Copy template** to project:
   ```bash
   cp -r ~/.agents/skills/ratatui-tui/assets/templates/<template>/* .
   ```

   Or generate from the official templates repo:
   ```bash
   cargo install --locked cargo-generate
   cargo generate ratatui/templates
   ```

2. **Run**:
   ```bash
   cargo run
   ```

## Version Notes (0.30.x)

Current stable: **0.30.1** (2026-06-05, MSRV 1.88, edition 2024).

- **Modular workspace**: apps keep depending on `ratatui`; widget *libraries*
  should depend on `ratatui-core` for API stability and fewer dependencies.
- **`ratatui::run(|terminal| ...)`**: initializes the terminal, installs a
  panic hook that restores it, runs the closure, and restores on exit.
- **`Block::shadow(...)`** (new in 0.30.1): drop shadows for blocks/popups.
- **Breaking since 0.29**: `block::Title` removed, `layout::Alignment` renamed
  to `HorizontalAlignment`, `Flex::SpaceAround` now matches flexbox semantics
  (use `Flex::SpaceEvenly` for the old behavior), `Marker` is non-exhaustive.
- **Performance**: disabling `default-features` also disables `layout-cache`;
  re-enable it explicitly or layout performance drops sharply.

## 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
```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)
```toml
[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"] }

# Optional: shimmer text animation
tui-shimmer = "0.1"
```

### Release Profile
```toml
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
```

## Core Loop: TEA (The Elm Architecture)

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

```rust
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(&self, frame: &mut Frame) {
        let text = format!("Counter: {}", self.counter);
        frame.render_widget(Paragraph::new(text), frame.area());
    }
}
```

## Styling Rules

**Use Stylize trait helpers:**
```rust
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:**
```rust
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](references/style-guide.md)

## Widget Patterns

### StatefulWidget
```rust
struct MyList {
    items: Vec<String>,
}

struct MyListState {
    selected: usize,
}

impl StatefulWidget for MyList {
    type State = MyListState;

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

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

### Layout
```rust
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](references/architecture-patterns.md)

## Async Event Handling

```rust
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](references/async-patterns.md)

## Image Integration

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

// Query terminal protocol support once at startup; keep it on the app
let picker = Picker::from_query_stdio()?;

// Load and resize in a background thread (`area` is the target Rect
// from your layout; clone the picker so the original stays reusable)
let (tx, rx) = std::sync::mpsc::channel();
let mut worker = picker.clone();
thread::spawn(move || {
    let dyn_img = image::open("photo.png").unwrap();
    let protocol = worker.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](references/image-integration.md)

## Shimmer / Loading Animation

[tui-shimmer](https://github.com/vinhnx/tui-shimmer) sweeps a highlight
across text — the "Loading…"/"Thinking…" effect used by coding-agent TUIs.

```rust
use ratatui::style::Style;
use ratatui::text::Line;
use tui_shimmer::{shimmer_spans_with_style, shimmer_spans_with_style_at_phase};

// Time-driven (call every frame; re-render on a tick to animate)
let spans = shimmer_spans_with_style("Loading...", Style::new().cyan());
frame.render_widget(Line::from(spans), area);

// Deterministic: drive phase (0.0..1.0) from app state — testable, pausable
let phase = (self.start.elapsed().as_secs_f32() / 2.0) % 1.0;
let spans = shimmer_spans_with_style_at_phase("Working...", Style::new().cyan(), phase);
```

**Key points:**
- Animation needs redraws: add a tick event (~80-120ms) to the event loop
  (`select!` with `tokio::time::interval`, or `event::poll` timeout)
- Prefer the `_at_phase` variant with ph
Files: 21
Size: 69.8 KB
Complexity: 76/100
Category: Image & Video

Related in Image & Video