ratatui-tui

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Ratatui TUI Development

Ratatui TUI 开发

Quick Start

快速开始

  1. Copy template to project:
    bash
    cp -r ~/.claude/skills/ratatui-tui/assets/templates/<template>/* .
  2. Run:
    bash
    cargo run
  1. 复制模板到项目:
    bash
    cp -r ~/.claude/skills/ratatui-tui/assets/templates/<template>/* .
  2. 运行
    bash
    cargo run

Template Selection

模板选择

ComplexityTemplateUse 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
复杂度模板适用场景
极简
hello-world
学习、快速演示
简单
simple-app
单屏应用、工具开发
异步
async-app
后台任务、网络请求场景
全量
component-app
多视图、配置、日志需求的项目
决策逻辑:
  • 需要异步/网络支持?→
    async-app
  • 多页面/多组件?→
    component-app
  • 仅需简单工具?→
    simple-app
  • 学习ratatui?→
    hello-world

Project Setup

项目配置

Minimal Cargo.toml

最小化Cargo.toml配置

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

[dependencies]
ratatui = "0.30"
crossterm = "0.29"
color-eyre = "0.6"
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)

全量依赖(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"
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"] }
undefined
ratatui-image = { version = "5", features = ["chafa-static"] }
undefined

Release Profile

发布配置

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

Core Loop: TEA (The Elm Architecture)

核心循环:TEA(Elm架构)

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());
    }
}
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
使用Stylize trait辅助方法:
rust
use ratatui::style::Stylize;

// 推荐写法
"text".bold()
"text".dim()
"text".cyan()
"text".on_dark_gray()
"text".bold().cyan()

// 不推荐写法
Style::default().fg(Color::White)  // 硬编码白色
Style::default().fg(Color::Black)  // 硬编码黑色
Style::new().add_modifier(Modifier::BOLD)  // 过于冗长
配色规范:
  • 主色:
    .cyan()
    ,
    .green()
  • 错误色:
    .red()
  • 警告色:
    .yellow()
    (谨慎使用)
  • 弱化色:
    .dim()
    ,
    .dark_gray()
  • 强调色:
    .magenta()
文本换行:
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();
参考:references/style-guide.md

Widget Patterns

组件模式

StatefulWidget

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);
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
    }
}

// 使用方式
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);
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
  • ListState
    - 用于List组件
  • TableState
    - 用于Table组件
  • ScrollbarState
    - 用于滚动条
参考: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
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 {
        // 渲染
        terminal.draw(|f| app.view(f))?;

        // 处理事件
        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),
                        _ => {}
                    }
                }
            }
            // 在此处添加其他通道(后台任务、定时器等)
        }

        if app.should_quit {
            break;
        }
    }
    Ok(())
}
参考:references/async-patterns.md

Image Integration

图片集成

rust
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
rust
use ratatui_image::{picker::Picker, StatefulImage, Resize};
use std::thread;

// 启动时仅查询一次终端协议支持
let mut picker = Picker::from_query_stdio()?;

// 在后台线程加载并调整大小
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();
});

// 渲染时使用StatefulImage实现高效重绘
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);
}
核心要点:
  • 使用
    chafa-static
    特性实现二进制文件跨环境兼容
  • 仅查询一次协议,不要每帧都查询
  • 将尺寸调整/编码逻辑放到后台线程执行
  • 使用
    StatefulImage
    避免重绘时重复编码
参考:references/image-integration.md

Error Handling

错误处理

rust
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:
rust
// 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")?;
rust
use color_eyre::eyre::Result;

fn main() -> Result<()> {
    // 最先安装钩子
    color_eyre::install()?;

    // 设置panic钩子恢复终端状态
    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()
}
错误传播:
rust
// 可恢复错误使用?运算符
let file = std::fs::read_to_string(path)?;

// 使用color_eyre添加上下文信息
let config = load_config()
    .wrap_err("加载配置失败")?;

Release Build

发布构建

bash
cargo build --release
Binary at
target/release/<name>
.
Size optimization:
toml
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
opt-level = "z"  # size over speed
bash
cargo build --release
二进制文件路径:
target/release/<name>
体积优化:
toml
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
opt-level = "z"  # 优先优化体积而非速度

Templates Overview

模板概览

hello-world (~25 lines)

hello-world(~25行代码)

Minimal ratatui demo using
ratatui::run()
.
使用
ratatui::run()
实现的极简ratatui演示项目。

simple-app (~80 lines)

simple-app(~80行代码)

Synchronous event loop, App struct, basic render.
同步事件循环、App结构体、基础渲染能力。

async-app (~120 lines)

async-app(~120行代码)

Tokio runtime, EventStream,
select!
pattern.
Tokio运行时、EventStream、
select!
模式。

component-app (~300 lines)

component-app(~300行代码)

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
完整模块化结构:
  • main.rs
    - 入口文件
  • app.rs
    - App状态、更新逻辑
  • event.rs
    - 事件处理
  • ui.rs
    - 渲染逻辑
  • action.rs
    - Action枚举定义
  • tui.rs
    - 终端初始化配置
  • config.rs
    - 基于dirs的配置管理
  • logging.rs
    - tracing日志配置

Common Patterns

常用模式

Centered Popup

居中弹窗

rust
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
}
rust
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

快捷键展示

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

Status Bar

状态栏

rust
let status = Line::from(vec![
    " MODE ".bold().on_cyan(),
    format!(" {} items ", count).dim().into(),
]);
rust
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)
发布前确认:
  • 执行过
    cargo fmt
  • cargo clippy --all-features
    无警告
  • 测试外代码无
    unwrap()
    调用
  • Panic钩子可正常恢复终端状态
  • cargo build --release
    构建成功
  • 在目标终端上测试通过