mcpField
R
ratatui-tui

ratatui-tui

github.com/pedronauck开发647224 次安装2 周前更新
代码仓库

关于 ratatui-tui

ratatui-tui 是面向 Rust 开发者的 ratatui 终端界面开发技能,提供从入门到模块化应用的模板与实现模式。内容还覆盖异步事件、样式、图像集成和错误处理。

主要功能

  1. 01提供四种复杂度的应用模板
  2. 02采用 TEA 架构组织状态与界面
  3. 03示范同步和异步事件处理
  4. 04提供布局、组件与样式模式
  5. 05说明图像集成和错误处理

适用场景

  1. 01快速搭建终端界面原型
  2. 02开发异步或网络终端应用
  3. 03构建多视图模块化 TUI
  4. 04检查发布前的构建与代码质量

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/pedronauck/skills --skill ratatui-tui

安装后在 Claude Code 中输入/ratatui-tui确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

assets/templates/async-app/
Cargo.lock
Cargo.toml
assets/templates/async-app/src/
main.rs
assets/templates/component-app/
Cargo.lock
Cargo.toml
assets/templates/component-app/src/
action.rs
app.rs
config.rs
event.rs
logging.rs
main.rs
tui.rs
ui.rs
assets/templates/hello-world/
Cargo.lock
Cargo.toml
assets/templates/hello-world/src/
main.rs
assets/templates/simple-app/
Cargo.lock
Cargo.toml
assets/templates/simple-app/src/
main.rs
references/
architecture-patterns.md
async-patterns.md
image-integration.md
style-guide.md

详细介绍

Ratatui TUI 开发

快速开始

  1. 将模板复制到项目中:

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

    cargo run
    

模板选择

复杂度模板使用场景
最简hello-world学习、快速演示
简单simple-app单屏应用、工具
异步async-app后台任务、网络
完整component-app多视图、配置、日志

决策树:

  • 需要异步或网络功能?→ async-app
  • 有多个屏幕或组件?→ component-app
  • 只是一个简单工具?→ simple-app
  • 正在学习 ratatui?→ hello-world

项目设置

最简 Cargo.toml
[package]
name = "my-tui"
version = "0.1.0"
edition = "2024"

[dependencies]
ratatui = "0.30"
crossterm = "0.29"
color-eyre = "0.6"
完整依赖项(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"] }
发布配置
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true

核心循环:TEA(Elm 架构)

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

样式规则

使用 Stylize trait 辅助方法:

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

颜色调色板:

  • 主色:.cyan()、.green()
  • 错误:.red()
  • 警告:.yellow()(谨慎使用)
  • 弱化:.dim()、.dark_gray()
  • 强调色:.magenta()

文本换行:

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 模式

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);
布局
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);
内置状态类型
  • ListState - 用于 List widget
  • TableState - 用于 Table widget
  • ScrollbarState - 用于 Scrollbar

参见:references/architecture-patterns.md

异步事件处理

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

参见:references/async-patterns.md

图像集成

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

要点:

  • 使用 chafa-static 功能生成可移植的二进制文件
  • 只查询一次协议,不要每帧都查询
  • 将调整大小和编码任务转移到后台线程
  • 使用 StatefulImage,避免在重绘时重新编码

参见:references/image-integration.md

错误处理

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

错误传播:

// 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")?;

发布构建

cargo build --release

二进制文件位于 target/release/<name>。

大小优化:

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

模板概览

hello-world(约 25 行)

使用 ratatui::run() 的最简 ratatui 演示。

simple-app(约 80 行)

同步事件循环、App 结构体、基础渲染。

async-app(约 120 行)

Tokio 运行时、EventStream、select! 模式。

component-app(约 300 行)

完整的模块化结构:

  • main.rs - 入口点
  • app.rs - App 状态、更新逻辑
  • event.rs - 事件处理
  • ui.rs - 渲染
  • action.rs - Action 枚举
  • tui.rs - 终端设置
  • config.rs - 使用 dirs 的配置
  • logging.rs - tracing 设置

常见模式

居中弹窗
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
}
按键绑定显示
let help = Line::from(vec![
    " q ".bold().cyan(),
    "quit ".dim(),
    " ↑↓ ".bold().cyan(),
    "navigate ".dim(),
    " Enter ".bold().cyan(),
    "select ".dim(),
]);
状态栏
let status = Line::from(vec![
    " MODE ".bold().on_cyan(),
    format!(" {} items ", count).dim().into(),
]);

检查清单

发布前:

  • cargo fmt
  • cargo clippy --all-features 检查通过
  • 测试之外不使用 unwrap()
  • Panic hook 会恢复终端状态
  • cargo build --release 构建成功
  • 在目标终端上进行测试

评论

支持 Markdown