重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
npx skills add https://github.com/blacktop/dotfiles --skill ratatui-tui复制模板到项目:
cp -r ~/.claude/skills/ratatui-tui/assets/templates/<template>/* .
运行:
cargo run
| 复杂度 | 模板 | 使用场景 |
|---|---|---|
| 极简 | hello-world | 学习、快速演示 |
| 简单 | simple-app | 单屏应用、工具 |
| 异步 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
async-app |
| 后台任务、网络 |
| 完整 | component-app | 多视图、配置、日志 |
决策树:
async-appcomponent-appsimple-apphello-world[package]
name = "my-tui"
version = "0.1.0"
edition = "2024"
[dependencies]
ratatui = "0.30"
crossterm = "0.29"
color-eyre = "0.6"
[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"
# 可选:图像支持
ratatui-image = { version = "5", features = ["chafa-static"] }
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
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;
// 推荐
"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()文本换行:
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();
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) {
// 使用 state.selected 进行渲染
}
}
// 用法
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 组件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(())
}
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 避免重绘时重新编码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()
}
错误传播:
// 对可恢复的错误使用 ?
let file = std::fs::read_to_string(path)?;
// 使用 color_eyre 上下文
let config = load_config()
.wrap_err("加载配置失败")?;
cargo build --release
二进制文件位于 target/release/<name>。
大小优化:
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
opt-level = "z" // 优先考虑大小而非速度
使用 ratatui::run() 的最小 ratatui 演示。
同步事件循环,App 结构体,基本渲染。
Tokio 运行时,EventStream,select! 模式。
完整的模块化结构:
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(),
"退出 ".dim(),
" ↑↓ ".bold().cyan(),
"导航 ".dim(),
" Enter ".bold().cyan(),
"选择 ".dim(),
]);
let status = Line::from(vec![
" 模式 ".bold().on_cyan(),
format!(" {} 项 ", count).dim().into(),
]);
发布前:
cargo fmtcargo clippy --all-features 检查通过unwrap()cargo build --release 构建成功每周安装量
32
仓库
GitHub 星标数
14
首次出现
2026年2月9日
安全审计
安装于
opencode31
gemini-cli31
github-copilot31
codex31
amp29
kimi-cli29
Copy template to project:
cp -r ~/.claude/skills/ratatui-tui/assets/templates/<template>/* .
Run :
cargo run
| Complexity | Template | Use 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:
async-appcomponent-appsimple-apphello-world[package]
name = "my-tui"
version = "0.1.0"
edition = "2024"
[dependencies]
ratatui = "0.30"
crossterm = "0.29"
color-eyre = "0.6"
[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
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());
}
}
Use Stylize trait helpers:
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:
.cyan(), .green().red().yellow() (sparingly).dim(), .dark_gray().magenta()Text wrapping:
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
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 - for List widgetTableState - for Table widgetScrollbarState - for ScrollbarSee: 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(())
}
See: 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);
}
Key points:
chafa-static feature for portable binariesStatefulImage to avoid re-encoding on redrawsSee: 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()
}
Error propagation:
// 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
Binary at target/release/<name>.
Size optimization:
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
opt-level = "z" # size over speed
Minimal ratatui demo using ratatui::run().
Synchronous event loop, App struct, basic render.
Tokio runtime, EventStream, select! pattern.
Full modular structure:
main.rs - entry pointapp.rs - App state, update logicevent.rs - event handlingui.rs - renderingaction.rs - Action enumtui.rs - terminal setupconfig.rs - configuration with dirslogging.rs - tracing setupfn 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(),
]);
Before shipping:
cargo fmtcargo clippy --all-features cleanunwrap() outside testscargo build --release succeedsWeekly Installs
32
Repository
GitHub Stars
14
First Seen
Feb 9, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykWarn
Installed on
opencode31
gemini-cli31
github-copilot31
codex31
amp29
kimi-cli29
Lark Skill Maker 教程:基于飞书CLI创建AI技能,自动化工作流与API调用指南
41,900 周安装