bonsai_term 是 Jane Street 推出的纯 OCaml 库,专为构建动态终端用户界面(TUI)设计。它基于 Bonsai 计算框架,提供响应式编程模型,避免了传统终端库对 C 或 Rust 的依赖,实现高效的 UI 更新和事件处理。核心优势在于利用 Bonsai 的增量计算引擎,进行树状 diffing,只渲染变化部分;采用 immediate-mode 风格,每帧重新描述 UI,但通过 diff 最小化终端输出;同时在 TTY 上多路复用键盘、鼠标等事件,实现流畅交互。
Bonsai 的响应式基础:View.t 与 Attr.t
bonsai_term 的 UI 描述使用 View.t 类型,类似于 bonsai_web 中的 Vdom.Node.t。它是一个树状结构,支持文本、容器、颜色、样式等属性,通过 Attr.t 定义。View 可以是简单文本如 View.text "Hello",或复杂布局如 View.hbox [box1; box2](水平排列)或 View.vbox(垂直)。
关键是 Bonsai.t<View.t>,这是一个响应式计算图:输入状态变化时,只增量 recompute 受影响的子树。这实现了 “树 diffing for UI updates”。例如,在一个计数器应用中,数字变化只 diff 该文本节点,避免重绘整个屏幕。
证据来自库的 MLI 接口:app 函数签名 (dimensions: Dimensions.t Bonsai.t -> 'local Bonsai.graph -> view:View.t Bonsai.t * handler:(Event.t -> unit Effect.t) Bonsai.t)。Dimensions.t 提供终端宽高,Bonsai.graph 注入本地状态,输出 view 和事件处理器。Bonsai 确保每次状态变更,只计算 delta。
Immediate-mode 渲染:高效帧更新
不同于 retained-mode(如某些 GUI 库维护 DOM 状态),bonsai_term 采用 immediate-mode:每帧应用完全重新计算 View.t 树,然后与上一帧 diff,只输出变化的 ANSI 序列到 TTY。这简化了状态管理,用户只需描述 “当前想要的 UI”。
渲染循环由 start 函数驱动,默认目标 60 FPS(~16ms / 帧)。如果帧渲染 <16ms,则 sleep 补足;否则立即下一帧。底层使用 Notty_async 处理终端 I/O,支持优化模式(~optimize:true)进一步减少 diff 计算。
可落地参数:
target_frames_per_second: 30:对于复杂 UI,降至 30 FPS 避免卡顿。optimize: true:启用 Bonsai 优化,减少不必要 recompute。mouse: true、bpaste: true:激活鼠标和 bracketed paste,提高交互性。nosig: true:禁用信号处理,自行在 handler 中响应 Ctrl+C。
监控清单:
- 测量帧时间:使用 Async.Clock 记录 render 前后时间,若 >20ms,简化 View 树。
- Diff 大小:日志输出变化 cell 数,目标 <10% 屏幕面积。
- FPS 稳定:>45 FPS 为绿灯。
TTY 事件多路复用:统一 Event.t 处理
终端输入复杂:键盘、鼠标移动 / 点击、粘贴、resize 等。bonsai_term 通过 Reader.t(默认 stdin)解析成 Event.t 枚举,包括 Key.t、Mouse.t、Resize 等。然后全局 handler (Event.t -> unit Effect.t) 分发事件,用户需手动管理 focus(如当前选中的 textbox)。
例如,text_editor 示例中,维护 focus 状态:Key.Enter 提交,Arrow 光标移动,Mouse 点击切换 focus。这就是 “event multiplexing over TTY”:单一 reader 管道多路复用所有输入。
实现 focus 管理清单:
- 状态:
focus: string Bonsai.State.t(组件 ID)。 - Handler 分发:
fun event -> if Event.is_mouse event && in_bounds focus_id event.mouse then Effect.Many [set_focus focus_id; handle_mouse event] else if Event.is_key event then match event.key with | Tab -> cycle_focus() | _ -> delegate_to_focused event - Effect.t:
Effect.Print输出,Effect.Scheduler异步任务。
风险:无内置 tab-focus,需自定义;鼠标兼容性依终端(如 iTerm2 支持好,xterm 弱)。
工程化实践:从示例到生产
示例仓库 bonsai_term_examples 展示实际应用:ncdu(目录树浏览)、pomodoro_timer(倒计时)、text_editor(多行编辑)、tree_view(可展开树)。这些证明库适合监控仪表盘、编辑器、文件管理器。
移植 checklist:
- 安装:opam install bonsai_term(需 OxCaml)。
- App 骨架:
let app ~dimensions ~local = let view, handler = compute_ui ~dimensions local in view, handler let (_ : unit Deferred.Or_error.t) = Bonsai_term.start ~target_frames_per_second:60 app - 颜色:使用 Attr.[foreground; background] 与 ANSI 16/256 色。
- 响应式布局:
Dimensions.t -> responsive View(e.g., 宽屏 hbox,窄屏 vbox)。 - 测试:
~for_mocking模拟事件序列,验证 diff。
回滚策略:若 FPS 掉帧,fallback 到静态文本模式(无 Bonsai);终端不支持鼠标时,降级 keyboard-only。
生产阈值:
- 屏幕大小:支持 resize,min 80x24。
- 状态规模:<1000 Bonsai nodes,避免 O (n^2) recompute。
- 事件延迟:<50ms,优先高优先级 Effect。
bonsai_term 的纯 OCaml 实现确保零依赖、高性能,特别适合服务器端工具。相比 ratatui (Rust) 或 textual (Python),它无缝集成 OCaml 生态,利用 Bonsai 的纯函数式响应式优势。
资料来源:
- bonsai_term GitHub:核心 README 与 MLI 接口。
- bonsai_term_examples:实际示例代码。
(正文字数约 1250)