ratatui-tui
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRatatui TUI Development
Ratatui TUI 开发
Quick Start
快速开始
-
Copy template to project:bash
cp -r ~/.claude/skills/ratatui-tui/assets/templates/<template>/* . -
Run:bash
cargo run
-
复制模板到项目:bash
cp -r ~/.claude/skills/ratatui-tui/assets/templates/<template>/* . -
运行:bash
cargo run
Template Selection
模板选择
| Complexity | Template | Use Case |
|---|---|---|
| Minimal | | Learning, quick demos |
| Simple | | Single-screen apps, tools |
| Async | | Background tasks, network |
| Full | | 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
| 复杂度 | 模板 | 适用场景 |
|---|---|---|
| 极简 | | 学习、快速演示 |
| 简单 | | 单屏应用、工具开发 |
| 异步 | | 后台任务、网络请求场景 |
| 全量 | | 多视图、配置、日志需求的项目 |
决策逻辑:
- 需要异步/网络支持?→
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"] }
undefinedratatui-image = { version = "5", features = ["chafa-static"] }
undefinedRelease Profile
发布配置
toml
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = truetoml
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = trueCore 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) // verboseColor palette:
- Primary: ,
.cyan().green() - Error:
.red() - Warning: (sparingly)
.yellow() - 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
内置状态类型
- - for List widget
ListState - - for Table widget
TableState - - for Scrollbar
ScrollbarState
See: references/architecture-patterns.md
- - 用于List组件
ListState - - 用于Table组件
TableState - - 用于滚动条
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 feature for portable binaries
chafa-static - Query protocol once, not per-frame
- Offload resize/encode to background thread
- Use to avoid re-encoding on redraws
StatefulImage
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 --releaseBinary at .
target/release/<name>Size optimization:
toml
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
opt-level = "z" # size over speedbash
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演示项目。
ratatui::run()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, pattern.
select!Tokio运行时、EventStream、模式。
select!component-app (~300 lines)
component-app(~300行代码)
Full modular structure:
- - entry point
main.rs - - App state, update logic
app.rs - - event handling
event.rs - - rendering
ui.rs - - Action enum
action.rs - - terminal setup
tui.rs - - configuration with dirs
config.rs - - tracing setup
logging.rs
完整模块化结构:
- - 入口文件
main.rs - - App状态、更新逻辑
app.rs - - 事件处理
event.rs - - 渲染逻辑
ui.rs - - Action枚举定义
action.rs - - 终端初始化配置
tui.rs - - 基于dirs的配置管理
config.rs - - tracing日志配置
logging.rs
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 - clean
cargo clippy --all-features - No outside tests
unwrap() - Panic hook restores terminal
- succeeds
cargo build --release - Test on target terminal(s)
发布前确认:
- 执行过
cargo fmt - 无警告
cargo clippy --all-features - 测试外代码无调用
unwrap() - Panic钩子可正常恢复终端状态
- 构建成功
cargo build --release - 在目标终端上测试通过