渲染器
CliRenderer 驱动 OpenTUI。它管理终端输出、处理输入事件、运行渲染循环,并为创建 Renderable 提供上下文。
创建渲染器
使用异步工厂函数创建渲染器:
import { createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer({
exitOnCtrlC: true,
targetFps: 30,
})
工厂函数做三件事:
- 加载原生 Zig 渲染库
- 配置终端设置(鼠标、键盘协议和选定的屏幕模式)
- 返回已初始化的
CliRenderer实例
配置选项
本页介绍大多数应用直接设置的选项。CliRendererConfig 还包括用于测试和调优的底层钩子,如 clock、postProcessFns、prependInputHandlers、bufferedOutput、useThread 和统计选项。
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
screenMode | ScreenMode | "alternate-screen" | 渲染器如何使用终端空间(详情) |
footerHeight | number | 12 | "split-footer" 模式下请求的页脚行数 |
stdin | NodeJS.ReadStream | process.stdin | 输入流;可用时使用原始模式 |
stdout | NodeJS.WriteStream | process.stdout | 原生帧和 stdout 拦截的输出流(详情) |
width | number | stdout.columns || 80 | 非 TTY/自定义 stdout 的回退列数 |
height | number | stdout.rows || 24 | 非 TTY/自定义 stdout 的回退行数 |
remote | boolean | custom: true; process: false | 将输出视为远程终端并跳过默认环境转发 |
externalOutputMode | ExternalOutputMode | 变化 | 如何处理通过 stdout.write 的写入(详情) |
consoleMode | ConsoleMode | "console-overlay" | 内置控制台覆盖层的行为(详情) |
exitOnCtrlC | boolean | true | 按下 Ctrl+C 时调用 renderer.destroy() |
exitSignals | NodeJS.Signals[] | 见下方 | 触发清理的信号(详情) |
clearOnShutdown | boolean | true | 在 suspend() 和 destroy() 时清除渲染器拥有的区域 |
targetFps | number | 30 | 渲染循环的目标帧率 |
maxFps | number | 60 | 即时重渲染的最大 FPS 上限 |
useMouse | boolean | true | 启用鼠标输入 |
autoFocus | boolean | true | 左键单击时聚焦最近的可聚焦 Renderable |
enableMouseMovement | boolean | true | 跟踪鼠标移动,而不仅仅是点击和滚动 |
useKittyKeyboard | KittyKeyboardOptions | null | {} | Kitty 键盘协议设置,或 null 禁用 |
backgroundColor | ColorInput | transparent | 渲染缓冲区的背景色 |
consoleOptions | ConsoleOptions | - | 转发给内置控制台覆盖层的选项 |
openConsoleOnError | boolean | true (dev) | 未捕获错误时打开控制台覆盖层 |
onDestroy | () => void | - | 渲染器完成清理后运行 |
你也可以在运行时通过匹配的 renderer 属性更改 screenMode、footerHeight、externalOutputMode 和 consoleMode。
屏幕模式
screenMode 选项控制 OpenTUI 是拥有备用屏幕还是主屏幕的保留区域。你也可以通过设置 renderer.screenMode 在运行时更改模式。
"alternate-screen"(默认)
切换到终端的备用屏幕缓冲区。原始 scrollback 内容在渲染器退出时被保留和恢复。这是全屏 TUI 应用程序的标准模式。
const renderer = await createCliRenderer({
screenMode: "alternate-screen",
})
"main-screen"
在终端主屏幕上渲染而不切换缓冲区。OpenTUI 仍然通过滚动终端内容保留渲染区域,因此这不是真正的 scrollback 原生内联或直接渲染器。当你希望 UI 留在主屏幕上进行测试、基准测试或短期工具时使用它。
const renderer = await createCliRenderer({
screenMode: "main-screen",
})
"split-footer"
将渲染器固定到终端底部的保留页脚区域。页脚上方的区域保持可用于正常程序输出。这是 OpenTUI 目前最接近直接渲染模式的方式,但它仍然使用相同的缓冲主屏幕渲染器而不是单独的内联后端。
footerHeight 选项控制页脚请求的行数(默认:12)。使用 split-footer 模式时,externalOutputMode 默认为 "capture-stdout",以便对 stdout.write 的写入可以在页脚上方重放而不是覆盖它。
const renderer = await createCliRenderer({
screenMode: "split-footer",
footerHeight: 20,
})
// 在运行时更改页脚高度
renderer.footerHeight = 15
Split-footer 的簿记工作在原生渲染器中完成。共享的 SplitScrollback 模型跟踪已发布的行和当前的尾列,因此页脚会随着 scrollback 的增长而固定在终端底部。捕获的 stdout 和 scrollback 写入器都产生带样式的 OptimizedBuffer 快照。原生端在页脚重绘时以一个原子帧发出这些 ANSI。渲染器在 resize、suspend、模式转换和 destroy 之前刷新待处理的输出。
可以重建 scrollback 的应用程序可以选择破坏性大小调整重放。renderer.resetSplitFooterForReplay({ clearSavedLines: true }) 清除可见视口和终端保存的行,重置 split-footer scrollback 簿记,并使页脚准备好接收新追加的快照。此方法仅在 split-footer 捕获模式下有效。
外部输出模式
externalOutputMode 选项控制在渲染器活动时通过渲染器配置的 stdout.write 进行的写入的处理方式。它不改变 stderr、内置控制台覆盖层或渲染器拥有的帧字节。
渲染器拥有的原生帧通过渲染器输出后端:默认为 process.stdout,或通过 NativeSpanFeed 的自定义 stdout。externalOutputMode 仅改变通过配置的 stdout.write 路径的应用程序写入。
"capture-stdout":拦截stdout.write,排队文本,并在 split-footer 渲染期间将其刷新到页脚上方。仅在screenMode为"split-footer"时有效。"passthrough":不改变stdout.write。输出直接发送到配置的stdout。
默认值取决于屏幕模式:split-footer 为 "capture-stdout",其他为 "passthrough"。
// 捕获的 stdout 出现在页脚上方
const renderer = await createCliRenderer({
screenMode: "split-footer",
externalOutputMode: "capture-stdout",
})
// 在运行时切换
renderer.externalOutputMode = "passthrough"
自定义流
传递 stdin 和 stdout 以通过其他传输运行 OpenTUI,如 SSH、pty 或基于 WebSocket 的 xterm.js 会话:
const renderer = await createCliRenderer({
stdin,
stdout,
width: cols,
height: rows,
exitOnCtrlC: false,
exitSignals: [],
})
初始大小来自 stdout.columns / stdout.rows,然后是 width / height,然后是 80x24。对于自定义 stdout,remote 默认为 true;仅当流的行为类似于本地终端并应接收默认终端环境转发时才设置 remote: false。
当外部终端大小改变时调用 renderer.resize(cols, rows)。SIGWINCH 仅为 process.stdout 注册。
每个 stdin 或 stdout 对象一次只能被一个渲染器拥有。destroy() 释放所有权并恢复 stdout.write。
控制台模式
consoleMode 选项控制内置控制台覆盖层(TerminalConsole)。
"console-overlay"(默认):捕获控制台输出(console.log、console.error 等)并在 TUI 内的可切换面板中渲染它。"disabled":隐藏并停用覆盖层面板。
当前注意事项:consoleMode 仅改变覆盖层面板。如果你需要普通的 console.* 行为,请设置 OTUI_USE_CONSOLE=false。
const renderer = await createCliRenderer({
screenMode: "split-footer",
externalOutputMode: "capture-stdout",
consoleMode: "disabled",
})
写入 Scrollback
在使用 externalOutputMode: "capture-stdout" 的 split-footer 模式下,渲染器还拥有页脚上方的编程追加路径。当你想要在 scrollback 中获得丰富的带样式输出(而不是原始 console.log 文本)时使用它 —— 颜色、换行、语法高亮代码和 markdown。每次追加都通过与捕获的 stdout 相同的 FIFO 队列,因此即使两个源交错,顺序也保持确定性。
两个 API 都需要 screenMode: "split-footer" 和 externalOutputMode: "capture-stdout"。否则会抛出异常。
renderer.writeToScrollback(writer)
将 Renderable 树渲染到离屏缓冲区并作为一次 scrollback 快照提交。
import { TextRenderable } from "@opentui/core"
renderer.writeToScrollback((ctx) => {
const root = new TextRenderable(ctx.renderContext, {
id: "api-response",
position: "absolute",
left: 0,
top: 0,
width: ctx.width,
height: 1,
content: "api responded in 12ms",
fg: "#8BD5CA",
})
return {
root,
width: ctx.width,
height: 1,
startOnNewLine: true,
trailingNewline: true,
}
})
写入器接收 ScrollbackRenderContext:
| 字段 | 类型 | 说明 |
|---|---|---|
width | number | 当前渲染器宽度 |
widthMethod | WidthMethod | 渲染器使用的字素宽度方法 |
tailColumn | number | 上一次 scrollback 提交结束的列 |
renderContext | RenderContext | 你在快照内传递给新 Renderable 的上下文 |
并返回 ScrollbackSnapshot:
| 字段 | 类型 | 说明 |
|---|---|---|
root | Renderable | 将绘制到快照缓冲区的顶级 Renderable |
width | number | 可选;默认为 root 的宽度,上限为渲染器宽度 |
height | number | 可选;默认为 root 的测量高度 |
rowColumns | number | 可选的显式宽度,用于尾列跟踪(用于部分行提交) |
startOnNewLine | boolean | 当上一次提交在行中间结束时,在此提交前插入换行(默认:true) |
trailingNewline | boolean | 在此提交的最后一行后追加换行(默认:true) |
teardown | () => void | 可选的清理钩子,在快照渲染后调用(例如,销毁 Solid 子树) |
使用 startOnNewLine: false 和 trailingNewline: false 进行内联输出 —— 例如,图标前缀后跟流式文本。
renderer.createScrollbackSurface(options?)
对于你想要在提交前渲染多次的流式输出(逐 token 的代码高亮、markdown 块在解析时稳定),使用 ScrollbackSurface。Surface 渲染到后备缓冲区。你可以就地重新渲染树,然后将特定行范围提交到 scrollback。
const surface = renderer.createScrollbackSurface({ startOnNewLine: true })
const code = new CodeRenderable(surface.renderContext, {
id: "streamed-code",
content: "",
filetype: "typescript",
syntaxStyle,
width: "100%",
streaming: true,
treeSitterClient,
})
surface.root.add(code)
code.content = "const x = 1"
await surface.settle() // 等待待处理的 Tree-sitter 高亮
// 提交已完成高亮的行;其余的可以继续重新渲染
surface.commitRows(0, surface.height)
// 稍后,当流完成时:
surface.destroy()
| 方法 | 说明 |
|---|---|
render() | 测量、布局并将当前树渲染到后备缓冲区 |
settle(timeoutMs?) | 渲染,然后等待所有进行中的 Tree-sitter 高亮完成后再返回 |
commitRows(start, endExclusive, options?) | 从后备缓冲区复制行范围并将其作为 scrollback 提交入队 |
destroy() | 销毁 surface 及其后备缓冲区 |
如果你在 render() 之前调用 commitRows,或者自上次 render() 以来渲染器的宽度或 widthMethod 发生了变化,commitRows 会抛出异常。在任一情况下,提交新行之前请重新渲染。
对于 React 和 Solid,请使用封装了 writeToScrollback 并提供 JSX 支持的绑定级别辅助工具。参阅 Solid 文档中的 createScrollbackWriter / writeSolidToScrollback。
根 Renderable
每个渲染器都有一个 root 属性。它是组件树顶部的特殊 RootRenderable:
import { Box, Text } from "@opentui/core"
// 向根添加组件
renderer.root.add(Box({ width: 40, height: 10, borderStyle: "rounded" }, Text({ content: "Hello, OpenTUI!" })))
根 Renderable 填充整个终端并在调整大小时自适应。
渲染循环控制
你可以使用以下控制模式:
自动模式(默认)
如果你不调用 start(),渲染器仅在组件树更改时重新渲染:
const renderer = await createCliRenderer()
renderer.root.add(Text({ content: "Static content" })) // 触发渲染
连续模式
调用 start() 以目标 FPS 连续运行渲染循环:
renderer.start() // 开始连续渲染
renderer.stop() // 停止渲染循环
你可以在运行时更改渲染循环的节奏。targetFps 设置稳态连续渲染速率。maxFps 限制 requestRender() 产生即时额外帧的频率:
renderer.targetFps = 60
renderer.maxFps = 120
实时渲染
对于动画,调用 requestLive() 启用连续渲染:
// 请求实时模式(增加内部计数器)
renderer.requestLive()
// 动画完成时,丢弃请求
renderer.dropLive()
多个组件可以同时请求动画。渲染器保持实时状态直到所有请求被丢弃。
等待空闲
idle() 在没有待处理的渲染通道或调度渲染时解析。使用 getSchedulerState() 进行诊断,特别是在测试和自定义输出会话中:
await renderer.idle()
const state = renderer.getSchedulerState()
暂停和挂起
renderer.pause() // 暂停渲染(使用 start() 或 requestLive() 再次运行)
renderer.suspend() // 完全挂起(禁用鼠标、输入和原始模式)
renderer.resume() // 从挂起状态恢复
默认情况下,挂起或销毁会清除 OpenTUI 在主屏幕上拥有的区域。如果你希望在关闭后保持该内容可见,请设置 clearOnShutdown: false:
const renderer = await createCliRenderer({
screenMode: "split-footer",
clearOnShutdown: false,
})
关键属性
| 属性 | 类型 | 说明 |
|---|---|---|
root | RootRenderable | 组件树的根 |
width | number | 当前渲染宽度(列数) |
height | number | 当前渲染高度(行数) |
console | TerminalConsole | 内置控制台覆盖层 |
keyInput | KeyHandler | 键盘输入处理器 |
isRunning | boolean | 渲染循环是否活跃 |
isDestroyed | boolean | 渲染器是否已被销毁 |
currentFocusedRenderable | Renderable | null | 当前聚焦的组件 |
screenMode | ScreenMode | 活动屏幕模式;可赋值以在运行时切换模式 |
footerHeight | number | Split-footer 高度;可赋值以调整页脚大小 |
externalOutputMode | ExternalOutputMode | 活动的 stdout 路由;可赋值以在捕获/直通之间切换 |
consoleMode | ConsoleMode | 活动的控制台覆盖层模式 |
themeMode | ThemeMode | null | 检测到的终端主题("dark" / "light") |
capabilities | TerminalCapabilities | null | 检测到的终端能力 |
targetFps | number | 目标渲染循环 FPS;可在运行时赋值 |
maxFps | number | 即时重渲染的上限 FPS;可在运行时赋值 |
事件
使用 renderer.on(event, callback) 订阅:
| 事件 | 载荷 | 说明 |
|---|---|---|
resize | (width: number, height: number) | 终端窗口大小调整 |
frame | (event: { frameId: number }) | 原生帧渲染成功 |
external_output | (event: CliRendererExternalOutputEvent) | Split-footer 捕获的输出已入队;如需要可复制快照 |
focus | () | 终端窗口获得焦点 |
blur | () | 终端窗口失去焦点 |
focused_renderable | (current, previous) | 聚焦的 Renderable 发生变化 |
focused_editor | (current, previous) | 聚焦的编辑器 Renderable 发生变化 |
theme_mode | (mode: "dark" | "light") | 终端配色方案更改。参阅主题模式部分。 |
palette | (colors: TerminalColors) | 终端调色板已刷新并更改 |
capabilities | (caps: TerminalCapabilities) | 终端能力已检测 |
selection | (selection: Selection) | 文本选择完成 |
destroy | () | 渲染器已销毁 |
memory:snapshot | (snapshot: MemorySnapshot) | 内存使用快照可用 |
debugOverlay:toggle | (enabled: boolean) | 调试覆盖层可见性更改 |
// 终端大小调整
renderer.on("resize", (width, height) => {
console.log(`Terminal size: ${width}x${height}`)
})
// 终端焦点事件
renderer.on("focus", () => {
console.log("Terminal gained focus")
})
renderer.on("blur", () => {
console.log("Terminal lost focus")
})
// 渲染器已销毁
renderer.on("destroy", () => {
console.log("Renderer destroyed")
})
// 文本选择完成
renderer.on("selection", (selection) => {
console.log("Selected text:", selection.getSelectedText())
})
主题模式
OpenTUI 通过两种机制检测终端的首选配色方案(深色或浅色):
- DEC 模式 2031(
CSI ? 997 ; ... n):支持的终端实时报告更改。 - OSC 10/11 回退:如果终端不支持 DEC 2031,OpenTUI 会检查终端的前景色和背景色,然后从背景亮度推导模式。
通过 renderer.themeMode("dark"、"light" 或检测完成前的 null)读取当前模式,并订阅 theme_mode 事件以获取后续更改。
import { type ThemeMode } from "@opentui/core"
const mode = renderer.themeMode
renderer.on("theme_mode", (nextMode: ThemeMode) => {
console.log("Theme mode changed:", nextMode)
})
DEC 2031 始终优先于 OSC 回退,因此开始时不支持 DEC 但后来获得支持的终端仍会报告正确的模式。
waitForThemeMode(timeoutMs?)
如果你需要在启动时短暂阻塞以进行主题检测 —— 例如,在第一次绘制前选择浅色或深色配色方案 —— 请使用 waitForThemeMode。它以检测到的模式解析,或者如果超时在任一源报告前到期则返回 null。
const mode = await renderer.waitForThemeMode(1000) // 默认为 1000 毫秒
传递 0 可同步读取当前模式而不等待。
终端集成
这些方法直接驱动终端模拟器,位于渲染缓冲区之外:
它们通过渲染器输出后端写入,因此自定义 stdout 会话与本地 TTY 会话接收相同的终端控制序列。
终端标题和背景
renderer.setTerminalTitle("OpenTUI – editing notes.md")
// 设置渲染缓冲区背景。OpenTUI 还会发出 OSC 11
// 以使终端自身的背景匹配。
renderer.setBackgroundColor("#0D1117")
// 通过 OSC 111 将终端背景重置为主题默认值
renderer.resetTerminalBgColor()
destroy() 和 suspend() 会为你调用 resetTerminalBgColor()。仅在渲染器不拥有的情况下自己调用 —— 例如,在使用 SIGTSTP 暂停之前。
OSC 52 剪贴板
OpenTUI 可以通过 OSC 52 设置和清除终端的剪贴板。这在 SSH 会话和远程编辑器中有效。选择退出的终端会静默忽略这些调用。
if (renderer.isOsc52Supported()) {
renderer.copyToClipboardOSC52("https://opentui.dev")
renderer.clearClipboardOSC52()
}
// 目标特定剪贴板
renderer.copyToClipboardOSC52("primary", "primary")
renderer.copyToClipboardOSC52("both", "clipboard-primary")
ClipboardTarget 的值为 "clipboard"、"primary" 或 "clipboard-primary"。如果渲染器无法写入 stdout(例如,在你销毁它之后),两个方法都返回 false。
通知
触发终端中介的桌面通知:
renderer.triggerNotification("Build finished", "OpenTUI")
当未检测到 OSC 通知协议时,该方法返回 false。详情请参阅通知。
原始 OSC 序列
订阅终端发出的原始 OSC 序列 —— 调色板查询、光标位置报告、自定义转义码。当你将 OpenTUI 与尚无专用 API 的终端功能集成时使用它。
const unsubscribe = renderer.subscribeOsc((sequence) => {
// sequence 是完整的 ESC ] ... BEL / ST 载荷
})
// 稍后
unsubscribe()
光标控制
使用这些方法控制光标位置和样式:
// 位置和可见性
renderer.setCursorPosition(10, 5, true)
// 光标样式
//
// 可用样式:"default"、"block"、"underline"、"line"
// 默认样式为 "default",保留终端的原生光标样式而不是覆盖它。
renderer.setCursorStyle({ style: "block", blinking: true }) // 闪烁块
renderer.setCursorStyle({ style: "underline", blinking: false }) // 稳定下划线
renderer.setCursorStyle({ style: "line", blinking: true }) // 闪烁线
renderer.setCursorStyle({ style: "default" }) // 重置为终端的原生光标
// 带颜色的光标样式
renderer.setCursorStyle({
style: "block",
blinking: true,
color: RGBA.fromHex("#FF0000"),
})
// 带鼠标指针的光标样式
//
// 可用的鼠标指针类型:"default"、"pointer"、"text"、"crosshair"、"move"、
// "not-allowed"
renderer.setCursorStyle({
style: "block",
blinking: false,
cursor: "pointer",
})
输入处理
添加自定义输入处理器:
renderer.addInputHandler((sequence) => {
if (sequence === "\x1b[A") {
// 上箭头 - 处理并消费
return true
}
return false // 让其他处理器处理
})
默认情况下,addInputHandler() 将处理器追加到链中并在内置处理器之后运行。使用 prependInputHandler() 在链的开头添加处理器并在内置处理器之前运行。
调试覆盖层
使用调试覆盖层显示 FPS、内存使用和其他统计信息:
renderer.toggleDebugOverlay()
// 你也可以配置它
import { DebugOverlayCorner } from "@opentui/core"
renderer.configureDebugOverlay({
enabled: true,
corner: DebugOverlayCorner.topRight,
})
清理
完成时始终销毁渲染器以恢复终端状态:
renderer.destroy()
销毁渲染器会将终端恢复到原始状态,禁用鼠标跟踪并清理资源。
对于自定义流传输,在关闭套接字或通道之前销毁渲染器。如果传输立即关闭,请允许一个微任务以便反馈的关闭字节可以刷新:
renderer.destroy()
await new Promise<void>((resolve) => queueMicrotask(resolve))
channel.close()
重要: OpenTUI 不会在 process.exit 或未处理的错误时自动清理。这种设计给你控制权。详情请参阅生命周期了解信号处理选项和最佳实践。
环境变量
完整列表请参阅环境变量参考。
你最可能关心的渲染器特定变量:
OTUI_USE_CONSOLE控制全局console.*捕获。SHOW_CONSOLE在启动时打开内置控制台覆盖层。OTUI_USE_ALTERNATE_SCREEN设置时强制screenMode为"alternate-screen"或"main-screen"。OTUI_OVERRIDE_STDOUT强制 stdout 路由:true仅在"split-footer"中捕获,false使用直通。OTUI_NO_NATIVE_RENDER跳过 Zig/原生帧渲染器。在"split-footer"模式下,当前的输出刷新路径仍可写入 ANSI。OTUI_DUMP_CAPTURES从渲染器退出处理器转储捕获的 stdout 和控制台缓存。