ink-terminal
v0.1.0-alpha.1
Published
High-performance React terminal renderer with double-buffering, Yoga layout, and streaming markdown — extracted from Claude Code
Maintainers
Readme
ink-terminal
高性能 React 终端渲染引擎,内含 27 项渲染优化技术。
从 Claude Code 内部渲染引擎提取而来。提供完整的 React 终端 UI 框架,包含双缓冲、硬件滚动、损伤追踪差分、原子输出 —— 对应用层完全透明。
核心特性
- 60fps 节流渲染 —
lodash.throttle+queueMicrotask,高频状态更新自动合并 - 双缓冲 — frontFrame/backFrame 交换,每帧零内存分配
- Blit 快速路径 — 未变化的子树通过
TypedArray.set()批量复制 (memcpy 级别) - 打包 Int32Array 屏幕缓冲区 — 每个 cell 2 words,
CharPool/StylePool池化 - 损伤追踪差分 — 只扫描变化的屏幕区域,而非整个缓冲区
- DECSTBM 硬件滚动 — 终端侧行移位,只绘制新增行
- BSU/ESU 原子输出 — DEC 2026 同步更新,消除画面撕裂
- 增量 Markdown — O(delta) 解析 + Token LRU 缓存 (配合 StreamingMarkdown 使用)
- 虚拟滚动 — 1000+ 消息只渲染可视区域
- 纯 TypeScript Yoga 布局 — Flexbox 布局,零 native/WASM 依赖
- 完整事件系统 — 键盘、鼠标点击、焦点、粘贴、窗口大小变化
- React 19 + 自定义 Reconciler — 终端中使用完整 React 组件模型
架构
ink-terminal/
├── core/ # 第 1 层: 屏幕缓冲区, Diff, 终端 I/O (无 React 依赖)
├── react/ # 第 2 层: React reconciler, 组件, Hooks
├── events/ # 事件系统 (输入, 点击, 焦点, 键盘, 粘贴, 窗口变化)
├── termio/ # 终端 I/O 原语 (ANSI, CSI, DEC, ESC, OSC, SGR)
├── layout/ # Flexbox 布局引擎 (纯 TS Yoga 实现)
├── providers/ # 插件/回调接口,用于应用层集成
└── internal/ # 内置工具 (intl, stringWidth, sliceAnsi)三层架构
| 层级 | 目录 | 需要 React | 用途 |
|------|------|-----------|------|
| Core | core/ | 否 | 屏幕缓冲区、差分引擎、终端能力检测 |
| React | react/ | 是 | Reconciler、组件、Hooks、Ink 主类 |
| 应用层 | (你的项目) | 是 | 主题、Markdown、消息列表、REPL |
安装
bun add ink-terminal react react-reconciler快速开始
最小示例 (5 行核心代码)
import { render, Box, Text } from 'ink-terminal'
const App = () => (
<Box flexDirection="column" padding={1}>
<Text bold color="green">Hello Terminal!</Text>
<Text>60fps 双缓冲渲染管线。</Text>
</Box>
)
await render(<App />)流式 Agent 输出
import { render, Box, Text, ScrollBox } from 'ink-terminal'
import { useState, useEffect } from 'react'
function AgentUI() {
const [text, setText] = useState('')
useEffect(() => {
;(async () => {
for await (const chunk of myAgent.stream('hello')) {
setText(t => t + chunk) // 自动 60fps 节流 + 双缓冲 + blit
}
})()
}, [])
return (
<Box flexDirection="column">
<ScrollBox overflow="scroll" stickyScroll>
<Text>{text}</Text>
</ScrollBox>
</Box>
)
}
await render(<AgentUI />)仅这 5 行核心代码,你的输出就已获得:
- 60fps 节流渲染(不会因 token 频率过高而卡死终端)
- 双缓冲 + blit(只重绘变化的 cells)
- 损伤追踪(只 diff 变化区域)
- BSU/ESU 原子输出(无撕裂)
交互式应用
import {
render, Box, Text, ScrollBox,
AlternateScreen, useInput, useApp
} from 'ink-terminal'
function App({ messages }) {
const { exit } = useApp()
useInput((input, key) => {
if (key.escape) exit()
})
return (
<AlternateScreen>
<Box flexDirection="column" height="100%">
<Box borderStyle="single" paddingX={1}>
<Text bold>My Agent</Text>
</Box>
<ScrollBox overflow="scroll" stickyScroll flexGrow={1}>
{messages.map(m => (
<Box key={m.id} paddingX={1}>
<Text>{m.content}</Text>
</Box>
))}
</ScrollBox>
</Box>
</AlternateScreen>
)
}仅使用 Core 层 (无 React)
import {
createScreen, CharPool, StylePool, HyperlinkPool,
diffEach, blitRegion
} from 'ink-terminal/core'
// 创建双缓冲
const pools = { style: new StylePool(), char: new CharPool(), hyperlink: new HyperlinkPool() }
const screen1 = createScreen(80, 24, pools.style, pools.char, pools.hyperlink)
const screen2 = createScreen(80, 24, pools.style, pools.char, pools.hyperlink)
// 写入 cells, 差分, 输出
diffEach(screen1, screen2, (x, y, removed, added) => {
// 处理 cell 变化
})配置
通过 configure() 注入应用特定的回调。所有回调都在冷路径(日志、生命周期)—— 热渲染路径零外部调用。
import { configure } from 'ink-terminal'
configure({
logForDebugging: (msg) => console.debug(`[ink] ${msg}`),
logError: (msg) => console.error(`[ink] ${msg}`),
terminalName: 'my-app',
onBeforeRender: () => { /* 刷新交互指标 */ },
onScrollActivity: () => { /* 标记滚动活动 */ },
onInteraction: () => { /* 更新交互时间戳 */ },
onFirstInput: () => { /* 停止捕获早期输入 */ },
isMouseClicksDisabled: () => false,
isEnvTruthy: (v) => v === '1' || v === 'true',
})组件一览
| 组件 | 说明 |
|------|------|
| Box | Flexbox 容器(方向、内边距、外边距、边框) |
| Text | 带样式的终端文本(粗体、斜体、颜色、下划线) |
| ScrollBox | 可滚动容器,支持硬件滚动 (DECSTBM) |
| AlternateScreen | 全屏备用缓冲区模式 |
| Button | 支持焦点的交互按钮 |
| Link | 可点击超链接 (OSC 8) |
| Newline | 换行符 |
| Spacer | Flex 空间填充 |
| NoSelect | 不可选择区域 |
| RawAnsi | 原始 ANSI 转义码直通 |
| Ansi | 解析后的 ANSI 文本 |
Hooks 一览
| Hook | 说明 |
|------|------|
| useInput(handler) | 键盘/鼠标输入处理 |
| useApp() | 访问应用实例(退出等) |
| useStdin() | 原始 stdin 访问 |
| useAnimationFrame(cb) | 帧同步动画回调 |
| useInterval(cb, ms) | 定时器 |
| useSelection() | 文本选择状态 |
| useTabStatus() | 终端标签页状态栏 |
| useTerminalFocus() | 终端焦点/失焦事件 |
| useTerminalTitle(title) | 设置终端标题 |
| useTerminalViewport() | 获取终端尺寸 |
子路径导出
import { ... } from 'ink-terminal' // 完整 API
import { ... } from 'ink-terminal/core' // 屏幕缓冲区, diff (无 React)
import { ... } from 'ink-terminal/react' // React 组件, hooks, reconciler
import { ... } from 'ink-terminal/events' // 事件类
import { ... } from 'ink-terminal/termio' // 终端转义序列
import { ... } from 'ink-terminal/layout' // Yoga 布局引擎渲染管线
React setState
│
▼
scheduleRender (lodash.throttle 16ms, leading+trailing)
│
▼
queueMicrotask (延迟到 useLayoutEffect 之后)
│
▼
onRender()
├── Yoga 布局计算
├── DFS 树遍历 → 屏幕缓冲区
│ ├── Blit 快速路径 (clean 节点 → TypedArray memcpy)
│ └── Dirty 节点 → 重渲染到 cells
├── Diff (损伤追踪, 每 cell 2 个 int 比较)
├── 优化 patches (合并光标移动, 去重超链接)
└── 写入终端 (BSU → ANSI 转义码 → ESU)27 项优化技术总览
| # | 层级 | 优化 | 核心原理 | |---|------|------|----------| | 1 | React | 60fps 节流渲染 | lodash throttle + queueMicrotask | | 2 | React | 双缓冲 | frontFrame/backFrame 交换 | | 3 | 树遍历 | Blit 快速路径 | clean 节点 TypedArray memcpy | | 4 | 树遍历 | 脏标记传播 | shallowEqual 守卫 + 祖先链 | | 5 | 缓冲区 | 打包 Int32Array | 每 cell 2 words | | 6 | 缓冲区 | CharPool ASCII 快速路径 | Int32Array 直接索引 | | 7 | 缓冲区 | StylePool 转换缓存 | 预序列化 ANSI diff | | 8 | Diff | 损伤矩形追踪 | 只 diff 变化区域 | | 9 | Diff | findNextDiff | 2 个 int 比较/cell, JIT 内联 | | 10 | Diff | visibleCellAtIndex | 跳过不可见空格 | | 11 | 终端 | DECSTBM 硬件滚动 | CSI 滚动区域 | | 12 | 终端 | BSU/ESU 原子写入 | DEC 2026 | | 13 | 终端 | Patch 优化器 | 合并光标移动/去重超链接 | | 14 | 输出 | charCache 跨帧持久化 | 16384 上限 | | 15 | Markdown | 流式增量解析 | stable/unstable 分割 | | 16 | Markdown | Token LRU 缓存 | 500 条, hash 键控 | | 17 | 虚拟滚动 | scrollTop 量化 | 40 行 bin | | 18 | 虚拟滚动 | Slide cap | 每 commit 最多挂载 25 项 | | 19 | 虚拟滚动 | useDeferredValue 时间切片 | React 并发特性 | | 20 | 虚拟滚动 | Resize 高度缩放 | 非清除 | | 21 | 滚动 | 自适应消耗 | xterm.js <=5 即时, >5 步进 | | 22 | 滚动 | 比例消耗 | 原生终端, 3/4 剩余量 | | 23 | 内存 | 池定期重置 | 每 5 分钟 CharPool+HyperlinkPool | | 24 | 渲染器 | Output 复用 | charCache 跨帧持久化 | | 25 | 渲染器 | 毒化检测 | prevScreen/absoluteRemoved | | 26 | 监控 | 分阶段帧计时 | renderer/diff/optimize/write/yoga | | 27 | 监控 | FPS P99 追踪 | 最坏 1% 帧 |
与原版 Ink 对比
| 特性 | 原版 Ink | ink-terminal | |------|---------|--------------| | 屏幕缓冲区 | 每帧字符串拼接 | 打包 Int32Array + CharPool/StylePool | | Diff 算法 | 逐行字符串比较 | 逐 Cell 整数比较 + 损伤追踪 | | 滚动 | 全量重绘 | DECSTBM 硬件滚动 + blit+shift | | 原子输出 | 无 | BSU/ESU (DEC 2026) | | 字符宽度 | 基础 | Unicode Grapheme Segmenter + bidi | | 布局引擎 | Yoga WASM | 纯 TS Yoga (零安装成本) | | 虚拟滚动 | 无 | 内置 (量化/slide cap/deferred) | | 帧性能 | 无监控 | 分阶段计时 + FPS P99 | | 组件数量 | ~10 | 50+ | | 事件系统 | 基础 keyboard | keyboard + click + focus + paste + resize |
Agent 集成指南
集成深度选择
| 需求 | 推荐方式 | 代码量 | 获得的优化 |
|------|----------|--------|-----------|
| 只显示流式文本 | setText(t => t + chunk) | 5 行 | 60fps + 双缓冲 + blit + BSU/ESU |
| 显示工具调用和结果 | Message 协议 + 事件分发 | 50 行 | 上述 + 工具渲染 + 进度显示 |
| 完整交互式终端 | REPL 级别消息循环 | 200 行 | 上述 + 虚拟滚动 + 输入处理 |
数据流
Agent 事件 → React setState → 自动渲染管线
│ │
▼ ▼
text_delta → setText(t + d) Ink 60fps 节流 → Yoga → Screen → Diff → Terminal
tool_use → setTools([...]) (对 agent 完全透明)
message → setMessages([...])详细的集成文档见 docs/ink-terminal-agent-integration-guide.md。
项目统计
- 113 个 TypeScript 文件,22,430 行代码
- 27 项渲染优化技术
- 0 个 TypeScript 错误 (
tsc --noEmit) - 纯 TypeScript Yoga 布局引擎 (~2,578 行,无 WASM/native addon)
- 18 个 npm 依赖,2 个 peer 依赖 (react, react-reconciler)
参考文档
| 文档 | 说明 |
|------|------|
| docs/terminal-rendering-guide.md | 渲染优化技术详解 (8 章节, 40+ 行号引用) |
| docs/terminal-rendering-exploration-report.md | 完整探索过程记录 |
| docs/ink-terminal-extraction-plan.md | 提取方案设计 (三层架构, 72 文件清单) |
| docs/ink-terminal-agent-integration-guide.md | Agent 适配指南 (3 种集成模式) |
| docs/ink-terminal-rust-integration.md | Rust 项目集成方案 (3 种方案对比) |
许可证
本项目包含从 Anthropic 的 Claude Code 提取的代码。许可条款请参阅原始项目。
