Specification
Skill ID
blacktop/dotfiles/ratatui-tui
Publisher
blacktop
Repository
dotfiles
Installs
0
Files
21
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

blacktop/dotfiles/ratatui-tuiInstalls these files
  • SKILL.md
  • assets/templates/async-app/Cargo.toml
  • assets/templates/async-app/src/main.rs
  • assets/templates/component-app/Cargo.toml
  • assets/templates/component-app/src/action.rs
  • assets/templates/component-app/src/app.rs
  • assets/templates/component-app/src/config.rs
  • assets/templates/component-app/src/event.rs
  • assets/templates/component-app/src/logging.rs
  • assets/templates/component-app/src/main.rs
  • assets/templates/component-app/src/tui.rs
  • assets/templates/component-app/src/ui.rs
  • assets/templates/hello-world/Cargo.toml
  • assets/templates/hello-world/src/main.rs
  • assets/templates/simple-app/Cargo.toml
  • assets/templates/simple-app/src/main.rs
  • references/architecture-patterns.md
  • references/async-patterns.md
  • references/image-integration.md
  • references/style-guide.md
  • workflows/tui-review.js

What this skill tells the agent

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:

   cargo install --locked cargo-generate
   cargo generate ratatui/templates
  1. 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

ComplexityTemplateUse Case
Minimalhello-worldLearning, quick demos
Simplesimple-appSingle-screen apps, tools
Asyncasync-appBackground tasks, network
Fullcomponent-appMulti-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"] }

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

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(&self, frame: &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: &mut Buffer, state: &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; 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

Shimmer / Loading Animation

tui-shimmer sweeps a highlight across text — the "Loading…"/"Thinking…" effect used by coding-agent TUIs.

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)

Use this skill in a project

Open any RiverX project, search the Skills panel for blacktop/dotfiles/ratatui-tui, and install it in one click.

Use this skill