rs-ratatui-crate
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRatatui
Ratatui
Ratatui is an immediate-mode Rust library for building terminal UIs. It renders the entire UI each frame from application state — there is no persistent widget tree. The default backend is Crossterm.
- Crate:
ratatui = "0.30" - Docs: https://docs.rs/ratatui/latest/ratatui/
- MSRV: 1.86.0 (Rust 2024 edition)
- Widget reference: Read references/widgets.md for built-in widget details, styling, and custom widget implementation
- Architecture patterns: Read references/architecture.md for TEA, component, and monolithic patterns, event handling, layout, state management, and testing
Ratatui是一款用于构建终端UI的Rust即时模式库。它会根据应用状态逐帧渲染整个UI——不存在持久化的组件树。默认后端为Crossterm。
- Crate:
ratatui = "0.30" - 文档: https://docs.rs/ratatui/latest/ratatui/
- 最低支持Rust版本(MSRV): 1.86.0(Rust 2024版本)
- 组件参考: 查阅 references/widgets.md 获取内置组件的详细信息、样式设置及自定义组件实现方法
- 架构模式: 查阅 references/architecture.md 了解TEA、组件化和单体模式、事件处理、布局、状态管理及测试相关内容
Quick Start
快速开始
Minimal app with ratatui::run()
(v0.30+)
ratatui::run()使用ratatui::run()
的极简应用(v0.30+)
ratatui::run()rust
use ratatui::{widgets::{Block, Paragraph}, style::Stylize};
fn main() -> Result<(), Box<dyn std::error::Error>> {
ratatui::run(|terminal| {
loop {
terminal.draw(|frame| {
let greeting = Paragraph::new("Hello, Ratatui!")
.centered()
.yellow()
.block(Block::bordered().title("Welcome"));
frame.render_widget(greeting, frame.area());
})?;
if crossterm::event::read()?.is_key_press() {
break Ok(());
}
}
})
}ratatui::run()init()restore()rust
use ratatui::{widgets::{Block, Paragraph}, style::Stylize};
fn main() -> Result<(), Box<dyn std::error::Error>> {
ratatui::run(|terminal| {
loop {
terminal.draw(|frame| {
let greeting = Paragraph::new("Hello, Ratatui!")
.centered()
.yellow()
.block(Block::bordered().title("Welcome"));
frame.render_widget(greeting, frame.area());
})?;
if crossterm::event::read()?.is_key_press() {
break Ok(());
}
}
})
}ratatui::run()init()restore()App with init()
/restore()
(manual control)
init()restore()使用init()
/restore()
的应用(手动控制)
init()restore()rust
fn main() -> Result<()> {
color_eyre::install()?;
let mut terminal = ratatui::init();
let result = run(&mut terminal);
ratatui::restore();
result
}
fn run(terminal: &mut ratatui::DefaultTerminal) -> Result<()> {
loop {
terminal.draw(|frame| { /* render widgets */ })?;
if let Event::Key(key) = crossterm::event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break;
}
}
}
Ok(())
}rust
fn main() -> Result<()> {
color_eyre::install()?;
let mut terminal = ratatui::init();
let result = run(&mut terminal);
ratatui::restore();
result
}
fn run(terminal: &mut ratatui::DefaultTerminal) -> Result<()> {
loop {
terminal.draw(|frame| { /* 渲染组件 */ })?;
if let Event::Key(key) = crossterm::event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break;
}
}
}
Ok(())
}Cargo.toml
Cargo.toml
toml
[dependencies]
ratatui = "0.30"
crossterm = "0.29"
color-eyre = "0.6"toml
[dependencies]
ratatui = "0.30"
crossterm = "0.29"
color-eyre = "0.6"Core Concepts
核心概念
Rendering
渲染
Immediate-mode: call each tick. Build widgets from state and render — no retained widget tree.
terminal.draw(|frame| { ... })rust
terminal.draw(|frame| {
frame.render_widget(some_widget, frame.area());
frame.render_stateful_widget(stateful_widget, area, &mut state);
})?;即时模式:每一次循环都调用。根据状态构建组件并渲染——不存在保留的组件树。
terminal.draw(|frame| { ... })rust
terminal.draw(|frame| {
frame.render_widget(some_widget, frame.area());
frame.render_stateful_widget(stateful_widget, area, &mut state);
})?;Layout
布局
Use to split areas with constraints. Prefer for destructuring (v0.28+):
Layoutareas()rust
let [header, body, footer] = Layout::vertical([
Constraint::Length(3),
Constraint::Min(0),
Constraint::Length(1),
]).areas(frame.area());Centering with (v0.30+):
Rect::centered()rust
let popup_area = frame.area()
.centered(Constraint::Percentage(60), Constraint::Percentage(40));Or with :
Flex::Centerrust
let [area] = Layout::horizontal([Constraint::Length(40)])
.flex(Flex::Center)
.areas(frame.area());Constraint types: , , , , , .
Length(n)Min(n)Max(n)Percentage(n)Ratio(a, b)Fill(weight)使用通过约束条件拆分区域。推荐使用进行解构(v0.28+):
Layoutareas()rust
let [header, body, footer] = Layout::vertical([
Constraint::Length(3),
Constraint::Min(0),
Constraint::Length(1),
]).areas(frame.area());使用居中布局(v0.30+):
Rect::centered()rust
let popup_area = frame.area()
.centered(Constraint::Percentage(60), Constraint::Percentage(40));或使用:
Flex::Centerrust
let [area] = Layout::horizontal([Constraint::Length(40)])
.flex(Flex::Center)
.areas(frame.area());约束类型:、、、、、。
Length(n)Min(n)Max(n)Percentage(n)Ratio(a, b)Fill(weight)Widgets
组件
All widgets implement trait (). Stateful widgets use with an associated type.
Widgetfn render(self, area: Rect, buf: &mut Buffer)StatefulWidgetStateBuilt-in: , , , , , , , , , , , , , .
BlockParagraphListTableTabsGaugeLineGaugeBarChartChartCanvasSparklineScrollbarCalendarClearText primitives: , , — all implement .
SpanLineTextWidgetSee references/widgets.md for full API details.
所有组件都实现了 trait()。有状态组件使用及关联的类型。
Widgetfn render(self, area: Rect, buf: &mut Buffer)StatefulWidgetState内置组件:、、、、、、、、、、、、、。
BlockParagraphListTableTabsGaugeLineGaugeBarChartChartCanvasSparklineScrollbarCalendarClear文本原语:、、——均实现了 trait。
SpanLineTextWidget详见 references/widgets.md 获取完整API细节。
Event Handling
事件处理
Use Crossterm for input. Always check :
KeyEventKind::Pressrust
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => should_quit = true,
KeyCode::Up | KeyCode::Char('k') => scroll_up(),
KeyCode::Down | KeyCode::Char('j') => scroll_down(),
_ => {}
}
}
}使用Crossterm处理输入。务必检查:
KeyEventKind::Pressrust
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => should_quit = true,
KeyCode::Up | KeyCode::Char('k') => scroll_up(),
KeyCode::Down | KeyCode::Char('j') => scroll_down(),
_ => {}
}
}
}Terminal Setup & Panic Handling
终端初始化与Panic处理
With (simplest, v0.30+):
ratatui::run()rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
ratatui::run(|terminal| { /* app loop */ })
}With panic hook (recommended for /):
color-eyreinit()restore()rust
fn install_hooks() -> Result<()> {
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default().into_hooks();
let panic_hook = panic_hook.into_panic_hook();
std::panic::set_hook(Box::new(move |info| {
ratatui::restore();
panic_hook(info);
}));
eyre_hook.install()?;
Ok(())
}使用(最简单方式,v0.30+):
ratatui::run()rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
ratatui::run(|terminal| { /* 应用循环 */ })
}使用 panic钩子(推荐用于/模式):
color-eyreinit()restore()rust
fn install_hooks() -> Result<()> {
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default().into_hooks();
let panic_hook = panic_hook.into_panic_hook();
std::panic::set_hook(Box::new(move |info| {
ratatui::restore();
panic_hook(info);
}));
eyre_hook.install()?;
Ok(())
}Architecture
架构设计
Choose based on complexity. See references/architecture.md for full patterns with code.
| Complexity | Pattern | When |
|---|---|---|
| Simple | Monolithic | Single-screen, few key bindings, no async |
| Medium | TEA (The Elm Architecture) | Multiple modes, form-like interaction |
| Complex | Component | Multi-panel, reusable panes, plugin-like |
根据应用复杂度选择合适的模式。详见 references/architecture.md 获取带代码示例的完整模式说明。
| 复杂度 | 模式 | 适用场景 |
|---|---|---|
| 简单 | 单体模式 | 单屏幕、少量快捷键、无异步逻辑的应用 |
| 中等 | TEA(Elm架构) | 多模式、类表单交互的应用 |
| 复杂 | 组件化模式 | 多面板、可复用窗格、类插件的应用 |
TEA (The Elm Architecture) — Summary
TEA(Elm架构)—— 概述
rust
struct Model { counter: i32, running: bool }
enum Message { Increment, Decrement, Quit }
fn update(model: &mut Model, msg: Message) {
match msg {
Message::Increment => model.counter += 1,
Message::Decrement => model.counter -= 1,
Message::Quit => model.running = false,
}
}
fn view(model: &Model, frame: &mut Frame) {
let text = format!("Counter: {}", model.counter);
frame.render_widget(Paragraph::new(text), frame.area());
}rust
struct Model { counter: i32, running: bool }
enum Message { Increment, Decrement, Quit }
fn update(model: &mut Model, msg: Message) {
match msg {
Message::Increment => model.counter += 1,
Message::Decrement => model.counter -= 1,
Message::Quit => model.running = false,
}
}
fn view(model: &Model, frame: &mut Frame) {
let text = format!("Counter: {}", model.counter);
frame.render_widget(Paragraph::new(text), frame.area());
}Common Patterns
常见模式
List Navigation with Selection
带选择功能的列表导航
rust
let mut list_state = ListState::default().with_selected(Some(0));
// Update
match key.code {
KeyCode::Up => list_state.select_previous(),
KeyCode::Down => list_state.select_next(),
_ => {}
}
// Render
let list = List::new(items)
.block(Block::bordered().title("Items"))
.highlight_style(Style::new().reversed())
.highlight_symbol(Line::from(">> ").bold());
frame.render_stateful_widget(list, area, &mut list_state);rust
let mut list_state = ListState::default().with_selected(Some(0));
// 更新逻辑
match key.code {
KeyCode::Up => list_state.select_previous(),
KeyCode::Down => list_state.select_next(),
_ => {}
}
// 渲染逻辑
let list = List::new(items)
.block(Block::bordered().title("Items"))
.highlight_style(Style::new().reversed())
.highlight_symbol(Line::from(">> ").bold());
frame.render_stateful_widget(list, area, &mut list_state);Popup Overlay
弹窗覆盖层
rust
fn render_popup(frame: &mut Frame, title: &str, content: &str) {
let area = frame.area()
.centered(Constraint::Percentage(60), Constraint::Percentage(40));
frame.render_widget(Clear, area);
let popup = Paragraph::new(content)
.block(Block::bordered().title(title).border_type(BorderType::Rounded))
.wrap(Wrap { trim: true });
frame.render_widget(popup, area);
}rust
fn render_popup(frame: &mut Frame, title: &str, content: &str) {
let area = frame.area()
.centered(Constraint::Percentage(60), Constraint::Percentage(40));
frame.render_widget(Clear, area);
let popup = Paragraph::new(content)
.block(Block::bordered().title(title).border_type(BorderType::Rounded))
.wrap(Wrap { trim: true });
frame.render_widget(popup, area);
}Tabbed Interface
标签页界面
rust
let titles = vec!["Tab1", "Tab2", "Tab3"];
let tabs = Tabs::new(titles)
.block(Block::bordered())
.select(selected_tab)
.highlight_style(Style::new().bold().yellow());
frame.render_widget(tabs, tabs_area);rust
let titles = vec!["Tab1", "Tab2", "Tab3"];
let tabs = Tabs::new(titles)
.block(Block::bordered())
.select(selected_tab)
.highlight_style(Style::new().bold().yellow());
frame.render_widget(tabs, tabs_area);Custom Widget
自定义组件
rust
struct StatusBar { message: String }
impl Widget for StatusBar {
fn render(self, area: Rect, buf: &mut Buffer) {
Line::from(self.message)
.style(Style::new().bg(Color::DarkGray).fg(Color::White))
.render(area, buf);
}
}
// Implement for reference to avoid consuming the widget:
impl Widget for &StatusBar {
fn render(self, area: Rect, buf: &mut Buffer) {
Line::from(self.message.as_str())
.style(Style::new().bg(Color::DarkGray).fg(Color::White))
.render(area, buf);
}
}rust
struct StatusBar { message: String }
impl Widget for StatusBar {
fn render(self, area: Rect, buf: &mut Buffer) {
Line::from(self.message)
.style(Style::new().bg(Color::DarkGray).fg(Color::White))
.render(area, buf);
}
}
// 为避免渲染时消耗组件,实现引用类型的Widget:
impl Widget for &StatusBar {
fn render(self, area: Rect, buf: &mut Buffer) {
Line::from(self.message.as_str())
.style(Style::new().bg(Color::DarkGray).fg(Color::White))
.render(area, buf);
}
}Text Input with tui-input
使用tui-input实现文本输入
toml
[dependencies]
tui-input = "0.11"rust
use tui_input::Input;
use tui_input::backend::crossterm::EventHandler;
let mut input = Input::default();
// In event handler:
input.handle_event(&crossterm::event::Event::Key(key));
// In render:
let width = area.width.saturating_sub(2) as usize;
let scroll = input.visual_scroll(width);
let input_widget = Paragraph::new(input.value())
.scroll((0, scroll as u16))
.block(Block::bordered().title("Search"));
frame.render_widget(input_widget, area);
frame.set_cursor_position(Position::new(
area.x + (input.visual_cursor().max(scroll) - scroll) as u16 + 1,
area.y + 1,
));toml
[dependencies]
tui-input = "0.11"rust
use tui_input::Input;
use tui_input::backend::crossterm::EventHandler;
let mut input = Input::default();
// 事件处理逻辑:
input.handle_event(&crossterm::event::Event::Key(key));
// 渲染逻辑:
let width = area.width.saturating_sub(2) as usize;
let scroll = input.visual_scroll(width);
let input_widget = Paragraph::new(input.value())
.scroll((0, scroll as u16))
.block(Block::bordered().title("Search"));
frame.render_widget(input_widget, area);
frame.set_cursor_position(Position::new(
area.x + (input.visual_cursor().max(scroll) - scroll) as u16 + 1,
area.y + 1,
));Key Conventions
关键约定
- Always restore terminal — even on panic. Use or install a panic hook
ratatui::run() - Check on all key events
KeyEventKind::Press - Use as standard container
Block::bordered() - Prefer over
Layout::vertical/horizontal([...]).areas(rect).split(rect) - Use widget before rendering popups/overlays
Clear - Implement when the widget should not be consumed on render
Widget for &MyType - Use ,
ListState,TableStatefor scroll/selection trackingScrollbarState - Prefer for error handling in TUI apps
color-eyre - Use (v0.30+) for centering layouts instead of double
Rect::centered()Flex::Center
- 务必恢复终端状态——即使发生panic。使用或安装panic钩子
ratatui::run() - 所有键盘事件都要检查
KeyEventKind::Press - 使用作为标准容器
Block::bordered() - 优先使用而非
Layout::vertical/horizontal([...]).areas(rect).split(rect) - 渲染弹窗/覆盖层前使用组件
Clear - 当组件不应在渲染时被消耗时,实现
Widget for &MyType - 使用、
ListState、TableState跟踪滚动/选择状态ScrollbarState - 推荐在TUI应用中使用进行错误处理
color-eyre - 居中布局优先使用(v0.30+),而非两次使用
Rect::centered()Flex::Center