声明式语法
JSX 风格的过程宏:if / for / match 在子块里是一等公民,{ expr } 内嵌任意 Rust 表达式,widget() 桥接原生 ratatui widget。
组件、Hooks、输入层、路由与全局状态——构建在 Ratatui 与 Tokio 之上。你只描述 UI,渲染循环交给运行时。
ready
Delete task?
process exited · terminal restored
press enter to restart todo_app
左边是纯 Ratatui:终端、循环、计时器、退出都归你管。右边是 ratatui-kit:只剩状态和 UI——其余都是运行时的事。
纯 ratatui · 21 行 ratatui-kit · 17 行
fn main() -> std::io::Result<()> {
let mut terminal = ratatui::init();
let mut count = 0_u64;
let mut last = Instant::now();
loop {
terminal.draw(|f| ui(f, count))?;
if last.elapsed() >= Duration::from_secs(1) {
count += 1;
last = Instant::now();
}
if event::poll(Duration::from_millis(50))? {
if let Event::Key(k) = event::read()? {
if k.code == KeyCode::Char('q') {
break;
}
}
}
}
ratatui::restore();
Ok(())
} #[component]
fn Counter(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let mut count = hooks.use_state(|| 0_u64);
hooks.use_future(async move {
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
count += 1;
}
});
element!(
Border(border_style: Style::new().yellow()) {
Text(text: Line::from(format!("Counter: {:02}", count.get())))
}
)
} 默认不启用任何 feature——按需打开 router、atom、input、tree。
use_state 存状态,use_future 跑异步,element! 声明组件树;组件实例跨帧保留。
终端进入全屏,计数器每秒 +1,按 q 退出并自动恢复终端。
ratatui-kit 不替代 Ratatui——它补上成长中的终端应用需要的组件、状态与事件层。
JSX 风格的过程宏:if / for / match 在子块里是一等公民,{ expr } 内嵌任意 Rust 表达式,widget() 桥接原生 ratatui widget。
use_state、use_future、use_effect 与自定义 hook 按调用顺序组织。状态句柄是 Copy 的,支持 count += 1,组件卸载时自动释放。
事件路由到激活的输入层,而不是广播给每个组件。打开弹窗或进入编辑态,背景自动让出键盘。
状态写入唤醒渲染循环——只在真实变化时重绘,不靠定时器,也不靠手动。
一份共享 Palette 就是全部颜色的唯一来源。换掉它即可重绘整棵组件树,也可以用 Atom 驱动运行时换肤。
带历史栈的 Router 管理多个页面;Atom 管理跨页面共享的业务状态。
模态打开或进入编辑时,框架把键盘交给该层——背景组件彻底收不到按键。
阅读输入层参考 →按键事件 ─▶ 第 2 层 · 模态——背景一无所获
// Request an input layer when entering edit mode
let edit_layer = hooks.use_input_layer(editing.get(), true);
// The handler only receives key events while this layer is active
hooks.use_event_handler(
EventScope::Layer(edit_layer),
EventPriority::High,
move |event| {
let Event::Key(key) = event else {
return EventResult::Ignored;
};
match key.code {
KeyCode::Enter => editing.set(false),
KeyCode::Char(c) => draft.write().push(c),
_ => {}
}
// The background list never sees these keys
EventResult::Consumed
},
); 一次赋值,所有依赖自动重渲染——没有重绘调用,没有定时器。这个按钮就是全部思想:
Counter
├─ Border ───────── count: 07
└─ Text ─────────── count: 07 14 个与业务无关的组件——布局、输入、弹层、选择、虚拟列表——每个都有可运行示例和真实录屏。点击左侧列表,或聚焦后按 j / k 浏览。
弹性容器、带边框分组与段落渲染——把组件树切成可预期终端区域的原语。
查看文档 →View(flex_direction: Direction::Vertical, gap: 1) {
Text(text: "header")
View(height: Constraint::Fill(1)) {
Text(text: "body")
}
Text(text: "footer")
} 超出视口的内容先渲染进更大的缓冲区,再按偏移显示——自带 j/k、翻页与滚动条。
查看文档 →ScrollView(
flex_direction: Direction::Vertical,
block: Block::bordered(),
) {
for (i, row) in rows.iter().enumerate() {
View(key: i, height: Constraint::Length(1)) {
Text(text: row)
}
}
}
按给定宽度硬换行,再把真实行数写回布局高度,让 ScrollView 正确计算滚动范围。
查看文档 →ScrollView(scroll_view_state: scroll_state, block: Block::bordered()) {
WrappedText(
text: BODY,
wrap_width: 72,
style: Style::new().white(),
)
}
渲染 tui_input 状态的单行输入框;写入、提交与退出由页面 handler 驱动。
查看文档 →Border(height: Constraint::Length(3)) {
Input(
input: input.read().clone(),
placeholder: "Type and press Enter".to_string(),
cursor_style: Style::new().bg(Color::Yellow),
)
}
带编辑态的搜索框:按 s 进入输入并开启独占层——激活、提交、校验与互斥全包。
查看文档 →SearchInput(
value: query.read().to_string(),
placeholder: "Press s to search".to_string(),
on_change: move |next: String| query.set(next),
on_submit: move |value: String| {
submitted.set(value);
true
},
)
底层弹层:画背景遮罩、定位内容,并向子树提供当前输入层。
查看文档 →Modal(
open: open.get(),
width: Constraint::Length(68),
height: Constraint::Length(12),
) {
Border(top_title: Line::from("Details").centered()) {
Text(text: Line::from("Modal content"))
}
}
内置独占输入层的确认弹窗:确认/取消、按钮焦点与互斥,由 open 控制。
查看文档 →ConfirmModal(
open: confirm_open.get(),
title: Line::from("Delete release?"),
content: format!("Remove {selected_label}?"),
confirm_text: "Delete".to_string(),
on_confirm: move |_: ()| confirm_open.set(false),
on_cancel: move |_: ()| confirm_open.set(false),
)
无按钮的通知弹窗:展示消息、触发 on_close,期间拦截下层输入。
查看文档 →AlertModal(
open: alert_open.get(),
title: Line::from("Workspace is current"),
message: format!("{selected_label} is synced."),
on_close: move |_: ()| alert_open.set(false),
)
按分组的快捷键帮助弹窗:内容超出时内部滚动,同时保持输入互斥。
查看文档 →ShortcutInfoModal(
open: shortcuts_open.get(),
title: Line::from("Shortcut reference"),
sections: vec![ShortcutInfoSection::new(
"Navigation",
[("Down", "j"), ("Up", "k")],
)],
on_close: move |_: ()| shortcuts_open.set(false),
)
内部持有 ListState 的单选列表:j/k、方向键、Home/End 与 Enter 变成导航和回调。
查看文档 →Select<&'static str>(
items: items,
default_index: Some(1),
highlight_symbol: "> ",
empty_message: "No environments",
on_select: move |item: &'static str| {
selected.set(item);
},
)
两级交互的多选列表:Space 切换草稿选择(on_change),Enter 提交(on_select)。
查看文档 →MultiSelect<&'static str>(
items: items,
highlight_symbol: "> ",
on_change: move |items: Vec<&'static str>| {
selected_count.set(items.len());
},
on_select: move |items: Vec<&'static str>| {
submitted.set(items.len());
},
)
基于 tui-tree-widget 的树形选择(tree feature):折叠展开、路径式选择与输入互斥。
查看文档 →TreeSelect<&'static str>(
active: true,
items: demo_items(),
default_selection: vec!["components", "select"],
node_open_symbol: "- ",
node_closed_symbol: "+ ",
on_select: move |id: &'static str| {
submitted.set(id);
},
)
长列表(virtual-list feature):只渲染可视窗口——数万行数据配合自定义行渲染。
查看文档 →VirtualList<Line<'static>>(
item_count: 10_000,
default_index: Some(42),
render_item: |ctx: &ListBuildContext| {
let style = if ctx.is_selected {
Style::new().black().on_green()
} else {
Style::new()
};
(Line::styled(format!("row {:05}", ctx.index + 1), style), 1u16)
},
)
长列表多选的组合范式:VirtualList 管游标与窗口渲染,业务侧维护 HashSet。
查看文档 →VirtualList<Line<'static>>(
state: list_state,
item_count: item_count,
render_item: move |ctx: &ListBuildContext| {
let mark = if selected.contains(&ctx.index) { "[x]" } else { "[ ]" };
(Line::styled(format!("{mark} Row {:05}", ctx.index + 1), Style::default()), 1u16)
},
)
然后叠加异步状态、输入层与路由。首页的使命到此为止——接下来交给文档。