关于 ratatui-tui
ratatui-tui 是面向 Ratatui TUI 开发者的技能,提供项目模板、实现模式和发布检查清单,并涵盖异步事件、图像与加载动画。
主要功能
- 01按复杂度选择项目模板
- 02TEA 架构与组件模式
- 03异步事件处理模式
- 04图像和加载动画集成
- 05终端安全与发布检查
- 06多 Agent TUI 评审工作流
适用场景
- 01搭建 Ratatui 示例或单屏工具
- 02开发含异步任务的 TUI
- 03组织多视图组件应用
- 04发布前检查 TUI 项目
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ blacktop/ dotfiles --skill ratatui-tui安装后在 Claude Code 中输入/ratatui-tui确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/templates/async-app/Cargo.tomlassets/templates/async-app/src/main.rsassets/templates/component-app/Cargo.tomlassets/templates/component-app/src/action.rsapp.rsconfig.rsevent.rslogging.rsmain.rstui.rsui.rsassets/templates/hello-world/Cargo.tomlassets/templates/hello-world/src/main.rsassets/templates/simple-app/Cargo.tomlassets/templates/simple-app/src/main.rsreferences/architecture-patterns.mdasync-patterns.mdimage-integration.mdstyle-guide.mdworkflows/tui-review.js详细介绍
Ratatui TUI 开发
快速入门
-
将模板复制到项目中:
cp -r ~/.agents/skills/ratatui-tui/assets/templates/<template>/* .或从官方模板仓库生成:
cargo install --locked cargo-generate cargo generate ratatui/templates -
运行:
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();
组件模式
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成功 - 在目标终端上进行测试