mcpField
R
ratatui-tui

ratatui-tui

github.com/blacktop开发17737 次安装1 周前更新
代码仓库

关于 ratatui-tui

ratatui-tui 是面向 Ratatui TUI 开发者的技能,提供项目模板、实现模式和发布检查清单,并涵盖异步事件、图像与加载动画。

主要功能

  1. 01按复杂度选择项目模板
  2. 02TEA 架构与组件模式
  3. 03异步事件处理模式
  4. 04图像和加载动画集成
  5. 05终端安全与发布检查
  6. 06多 Agent TUI 评审工作流

适用场景

  1. 01搭建 Ratatui 示例或单屏工具
  2. 02开发含异步任务的 TUI
  3. 03组织多视图组件应用
  4. 04发布前检查 TUI 项目

安装

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

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

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

包含的文件

除 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
app.rs
config.rs
event.rs
logging.rs
main.rs
tui.rs
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
async-patterns.md
image-integration.md
style-guide.md
workflows/
tui-review.js

详细介绍

Ratatui TUI 开发

快速入门

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

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

    或从官方模板仓库生成:

    cargo install --locked cargo-generate
    cargo generate ratatui/templates
    
  2. 运行:

    cargo run
    

版本说明(0.30.x)

目标版本为 0.30.x(MSRV 1.88,edition 2024);运行 cargo info ratatui 查看当前补丁版本。

  • 模块化工作区:应用继续依赖 ratatui;组件库 应依赖 ratatui-core,以确保 API 稳定并减少依赖项。
  • ratatui::run(|terminal| ...):初始化终端,安装在发生 panic 时恢复终端的 钩子,运行闭包,并在退出时恢复终端。
  • Block::shadow(...)(0.30.1 新增):为块和弹出窗口添加阴影。
  • 自 0.29 起的破坏性变更:移除了 block::Title,将 layout::Alignment 重命名为 HorizontalAlignment,Flex::SpaceAround 现在符合 flexbox 语义 (旧行为请使用 Flex::SpaceEvenly),Marker 为非穷尽类型。
  • 性能:禁用 default-features 也会禁用 layout-cache; 请显式重新启用,否则布局性能会大幅下降。

模板选择

复杂度模板使用场景
最简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 (default features include chafa-dyn, which overrides chafa-static)
ratatui-image = { version = "11", default-features = false, features = ["crossterm", "image-defaults", "chafa-static"] }

# Optional: shimmer text animation
tui-shimmer = "0.1"
发布配置
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true

核心循环: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());
    }
}

样式规则

使用 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

组件模式

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 组件
  • TableState - 用于 Table 组件
  • 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

图像集成

目标版本为 ratatui-image 11.x;其 API 会随主版本变化,因此复用旧代码片段前请先查阅 docs.rs。

use ratatui::layout::Size;
use ratatui_image::{picker::Picker, protocol::Protocol, Image, Resize};

// Query protocol and font size once at startup; keep the picker on the app
let picker = Picker::from_query_stdio()?;

// Encode once, fitted inside a box of cells; do this outside `draw`
// (on a worker thread for large images)
let dyn_img = image::open("photo.png")?;
let protocol: Protocol = picker.new_protocol(dyn_img, Size::new(40, 20), Resize::Fit(None))?;

// In render, drawing a pre-encoded Protocol is cheap
frame.render_widget(Image::new(&protocol), area);

要点:

  • 若要构建可移植二进制文件,请使用 chafa-static 并设置 default-features = false; 同时启用两者时,默认的 chafa-dyn 优先
  • 协议只需查询一次,不要每帧都查询
  • Image 无状态且尺寸固定;所有编码都在 new_protocol 中完成
  • StatefulImage 会适应其渲染区域的尺寸,并在渲染时进行编码,这会造成阻塞;请通过 ratatui_image::thread::ThreadProtocol 驱动它(参见 crate 中的 examples/thread.rs 和 examples/tokio.rs)

参见:references/image-integration.md

闪烁 / 加载动画

tui-shimmer 会在文本上扫过一束高光——这是编码 Agent TUI 中使用的“加载中…”/“思考中…”效果。

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

要点:

  • 动画需要重绘:在事件循环中添加一个 tick 事件(约 80-120ms) (使用 tokio::time::interval 配合 select!,或设置 event::poll 超时)
  • 优先使用 _at_phase 变体,并将 phase 存储在 Model 中——这样可以保持渲染纯粹,并便于测试动画
  • 真彩色,并可针对受限终端自动回退
  • API 在 1.0 之前仍属实验性——请锁定版本并审查次要版本更新

错误处理

ratatui::run() / ratatui::init() 会安装一个 panic hook,在触发 panic 前恢复终端——不要手动编写。先安装 color-eyre,确保在报告输出前恢复终端:

use color_eyre::eyre::Result;

fn main() -> Result<()> {
    color_eyre::install()?;        // eyre hooks before terminal init
    // App::run is the app's own main loop (see templates), not a ratatui API
    let result = ratatui::run(|terminal| App::default().run(terminal));
    Ok(result?)
}

只有在不通过 ratatui::init(),而是手动构造 Terminal/Backend 时,才编写手动 panic hook。

错误传播:

// 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 构建

cargo build --release

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

大小优化 —— 当二进制文件大小比速度更重要时,用它替换上方的 Release Profile 配置块:

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

带投影(0.30.1+):

use ratatui::layout::Offset;
use ratatui::widgets::{Block, Shadow};

let popup = Block::bordered()
    .title("Confirm")
    .shadow(Shadow::dark_shade().offset(Offset::new(2, 1)));
按键绑定显示
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(),
]);

多 Agent TUI 审查工作流

workflows/tui-review.js 是一个适用于 Claude Code Workflow 工具的动态工作流模板。它针对每个 TUI 维度分派一名审查者——TEA 架构、终端安全、样式、事件处理、渲染性能——然后对每项发现进行对抗式验证再报告,因此最终只保留已确认的问题。对于没有 Workflow 工具的 Agent(Codex),请跳过该脚本,改为将这五个维度作为手动审查清单。

请将它视为模板,而不是可原样运行的脚本:根据代码库调整目标路径、维度和严重性阈值。它会分派多个 Agent,因此请在用户要求进行 TUI 审查或发布前检查时运行:

Workflow({
  scriptPath: "~/.agents/skills/ratatui-tui/workflows/tui-review.js",
  args: { path: "src/" },
})

或者询问:“运行 ratatui-tui 技能中的 TUI 审查工作流,审查 src/。”

检查清单

发布前:

  • cargo fmt
  • cargo clippy --all-features 无问题
  • 测试之外不使用 unwrap()
  • 所有退出路径都会恢复终端(ratatui::run() 或 init/restore)
  • cargo build --release 成功
  • 在目标终端上进行测试

评论

支持 Markdown