渲染器

CliRenderer 驱动 OpenTUI。它管理终端输出、处理输入事件、运行渲染循环,并为创建 Renderable 提供上下文。

创建渲染器

使用异步工厂函数创建渲染器:

import { createCliRenderer } from "@opentui/core"

const renderer = await createCliRenderer({
  exitOnCtrlC: true,
  targetFps: 30,
})

工厂函数做三件事:

  1. 加载原生 Zig 渲染库
  2. 配置终端设置(鼠标、键盘协议和选定的屏幕模式)
  3. 返回已初始化的 CliRenderer 实例

配置选项

本页介绍大多数应用直接设置的选项。CliRendererConfig 还包括用于测试和调优的底层钩子,如 clockpostProcessFnsprependInputHandlersbufferedOutputuseThread 和统计选项。

选项类型默认值说明
screenModeScreenMode"alternate-screen"渲染器如何使用终端空间(详情
footerHeightnumber12"split-footer" 模式下请求的页脚行数
stdinNodeJS.ReadStreamprocess.stdin输入流;可用时使用原始模式
stdoutNodeJS.WriteStreamprocess.stdout原生帧和 stdout 拦截的输出流(详情
widthnumberstdout.columns || 80非 TTY/自定义 stdout 的回退列数
heightnumberstdout.rows || 24非 TTY/自定义 stdout 的回退行数
remotebooleancustom: true; process: false将输出视为远程终端并跳过默认环境转发
externalOutputModeExternalOutputMode变化如何处理通过 stdout.write 的写入(详情
consoleModeConsoleMode"console-overlay"内置控制台覆盖层的行为(详情
exitOnCtrlCbooleantrue按下 Ctrl+C 时调用 renderer.destroy()
exitSignalsNodeJS.Signals[]见下方触发清理的信号(详情
clearOnShutdownbooleantruesuspend()destroy() 时清除渲染器拥有的区域
targetFpsnumber30渲染循环的目标帧率
maxFpsnumber60即时重渲染的最大 FPS 上限
useMousebooleantrue启用鼠标输入
autoFocusbooleantrue左键单击时聚焦最近的可聚焦 Renderable
enableMouseMovementbooleantrue跟踪鼠标移动,而不仅仅是点击和滚动
useKittyKeyboardKittyKeyboardOptions | null{}Kitty 键盘协议设置,或 null 禁用
backgroundColorColorInputtransparent渲染缓冲区的背景色
consoleOptionsConsoleOptions-转发给内置控制台覆盖层的选项
openConsoleOnErrorbooleantrue (dev)未捕获错误时打开控制台覆盖层
onDestroy() => void-渲染器完成清理后运行

你也可以在运行时通过匹配的 renderer 属性更改 screenModefooterHeightexternalOutputModeconsoleMode

屏幕模式

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",
})

将渲染器固定到终端底部的保留页脚区域。页脚上方的区域保持可用于正常程序输出。这是 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。渲染器在 resizesuspend、模式转换和 destroy 之前刷新待处理的输出。

可以重建 scrollback 的应用程序可以选择破坏性大小调整重放。renderer.resetSplitFooterForReplay({ clearSavedLines: true }) 清除可见视口和终端保存的行,重置 split-footer scrollback 簿记,并使页脚准备好接收新追加的快照。此方法仅在 split-footer 捕获模式下有效。

外部输出模式

externalOutputMode 选项控制在渲染器活动时通过渲染器配置的 stdout.write 进行的写入的处理方式。它不改变 stderr、内置控制台覆盖层或渲染器拥有的帧字节。

渲染器拥有的原生帧通过渲染器输出后端:默认为 process.stdout,或通过 NativeSpanFeed 的自定义 stdoutexternalOutputMode 仅改变通过配置的 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"

自定义流

传递 stdinstdout 以通过其他传输运行 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。对于自定义 stdoutremote 默认为 true;仅当流的行为类似于本地终端并应接收默认终端环境转发时才设置 remote: false

当外部终端大小改变时调用 renderer.resize(cols, rows)SIGWINCH 仅为 process.stdout 注册。

每个 stdinstdout 对象一次只能被一个渲染器拥有。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

字段类型说明
widthnumber当前渲染器宽度
widthMethodWidthMethod渲染器使用的字素宽度方法
tailColumnnumber上一次 scrollback 提交结束的列
renderContextRenderContext你在快照内传递给新 Renderable 的上下文

并返回 ScrollbackSnapshot

字段类型说明
rootRenderable将绘制到快照缓冲区的顶级 Renderable
widthnumber可选;默认为 root 的宽度,上限为渲染器宽度
heightnumber可选;默认为 root 的测量高度
rowColumnsnumber可选的显式宽度,用于尾列跟踪(用于部分行提交)
startOnNewLineboolean当上一次提交在行中间结束时,在此提交前插入换行(默认:true
trailingNewlineboolean在此提交的最后一行后追加换行(默认:true
teardown() => void可选的清理钩子,在快照渲染后调用(例如,销毁 Solid 子树)

使用 startOnNewLine: falsetrailingNewline: 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,
})

关键属性

属性类型说明
rootRootRenderable组件树的根
widthnumber当前渲染宽度(列数)
heightnumber当前渲染高度(行数)
consoleTerminalConsole内置控制台覆盖层
keyInputKeyHandler键盘输入处理器
isRunningboolean渲染循环是否活跃
isDestroyedboolean渲染器是否已被销毁
currentFocusedRenderableRenderable | null当前聚焦的组件
screenModeScreenMode活动屏幕模式;可赋值以在运行时切换模式
footerHeightnumberSplit-footer 高度;可赋值以调整页脚大小
externalOutputModeExternalOutputMode活动的 stdout 路由;可赋值以在捕获/直通之间切换
consoleModeConsoleMode活动的控制台覆盖层模式
themeModeThemeMode | null检测到的终端主题("dark" / "light"
capabilitiesTerminalCapabilities | null检测到的终端能力
targetFpsnumber目标渲染循环 FPS;可在运行时赋值
maxFpsnumber即时重渲染的上限 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 通过两种机制检测终端的首选配色方案(深色或浅色):

  1. DEC 模式 2031CSI ? 997 ; ... n):支持的终端实时报告更改。
  2. 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 和控制台缓存。