rs-ratatui-crate

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Ratatui

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()
的极简应用(v0.30+)

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()
calls
init()
before and
restore()
after the closure — handles terminal setup/teardown automatically.
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()
的应用(手动控制)

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
terminal.draw(|frame| { ... })
each tick. Build widgets from state and render — no retained widget tree.
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
Layout
to split areas with constraints. Prefer
areas()
for destructuring (v0.28+):
rust
let [header, body, footer] = Layout::vertical([
    Constraint::Length(3),
    Constraint::Min(0),
    Constraint::Length(1),
]).areas(frame.area());
Centering with
Rect::centered()
(v0.30+):
rust
let popup_area = frame.area()
    .centered(Constraint::Percentage(60), Constraint::Percentage(40));
Or with
Flex::Center
:
rust
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)
.
使用
Layout
通过约束条件拆分区域。推荐使用
areas()
进行解构(v0.28+):
rust
let [header, body, footer] = Layout::vertical([
    Constraint::Length(3),
    Constraint::Min(0),
    Constraint::Length(1),
]).areas(frame.area());
使用
Rect::centered()
居中布局(v0.30+):
rust
let popup_area = frame.area()
    .centered(Constraint::Percentage(60), Constraint::Percentage(40));
或使用
Flex::Center
rust
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
Widget
trait (
fn render(self, area: Rect, buf: &mut Buffer)
). Stateful widgets use
StatefulWidget
with an associated
State
type.
Built-in:
Block
,
Paragraph
,
List
,
Table
,
Tabs
,
Gauge
,
LineGauge
,
BarChart
,
Chart
,
Canvas
,
Sparkline
,
Scrollbar
,
Calendar
,
Clear
.
Text primitives:
Span
,
Line
,
Text
— all implement
Widget
.
See references/widgets.md for full API details.
所有组件都实现了
Widget
trait(
fn render(self, area: Rect, buf: &mut Buffer)
)。有状态组件使用
StatefulWidget
及关联的
State
类型。
内置组件:
Block
Paragraph
List
Table
Tabs
Gauge
LineGauge
BarChart
Chart
Canvas
Sparkline
Scrollbar
Calendar
Clear
文本原语:
Span
Line
Text
——均实现了
Widget
trait。
详见 references/widgets.md 获取完整API细节。

Event Handling

事件处理

Use Crossterm for input. Always check
KeyEventKind::Press
:
rust
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::Press
rust
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
ratatui::run()
(simplest, v0.30+):
rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
    ratatui::run(|terminal| { /* app loop */ })
}
With
color-eyre
panic hook (recommended for
init()
/
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(())
}
使用
ratatui::run()
(最简单方式,v0.30+):
rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
    ratatui::run(|terminal| { /* 应用循环 */ })
}
使用
color-eyre
panic钩子(推荐用于
init()
/
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.
ComplexityPatternWhen
SimpleMonolithicSingle-screen, few key bindings, no async
MediumTEA (The Elm Architecture)Multiple modes, form-like interaction
ComplexComponentMulti-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
    ratatui::run()
    or install a panic hook
  • Check
    KeyEventKind::Press
    on all key events
  • Use
    Block::bordered()
    as standard container
  • Prefer
    Layout::vertical/horizontal([...]).areas(rect)
    over
    .split(rect)
  • Use
    Clear
    widget before rendering popups/overlays
  • Implement
    Widget for &MyType
    when the widget should not be consumed on render
  • Use
    ListState
    ,
    TableState
    ,
    ScrollbarState
    for scroll/selection tracking
  • Prefer
    color-eyre
    for error handling in TUI apps
  • Use
    Rect::centered()
    (v0.30+) for centering layouts instead of double
    Flex::Center
  • 务必恢复终端状态——即使发生panic。使用
    ratatui::run()
    或安装panic钩子
  • 所有键盘事件都要检查
    KeyEventKind::Press
  • 使用
    Block::bordered()
    作为标准容器
  • 优先使用
    Layout::vertical/horizontal([...]).areas(rect)
    而非
    .split(rect)
  • 渲染弹窗/覆盖层前使用
    Clear
    组件
  • 当组件不应在渲染时被消耗时,实现
    Widget for &MyType
  • 使用
    ListState
    TableState
    ScrollbarState
    跟踪滚动/选择状态
  • 推荐在TUI应用中使用
    color-eyre
    进行错误处理
  • 居中布局优先使用
    Rect::centered()
    (v0.30+),而非两次使用
    Flex::Center