Appearance
11 — TUI 渲染引擎 — Ink
概述
Claude Code 使用一个 深度定制的 Ink 终端渲染引擎。它不是 npm 上的 ink 库,而是基于相同理念的完全重写版:用 React 编写 TUI 组件,通过 Yoga (Flexbox) 计算布局,最终输出 ANSI 转义序列到终端。
为什么自研而不用现成的 Ink?因为 Claude Code 需要:
- 虚拟滚动:数千行对话消息不能全量渲染
- 鼠标点击事件和命中测试:支持链接点击、按钮交互
- 双向文本 (BiDi):支持阿拉伯语、希伯来语等
- ANSI 感知的文本测量:正确处理 CJK 字符宽度、emoji、嵌套样式
- 差量刷新:只重绘变化的行,避免大面积闪烁
- 焦点管理和选择系统:支持键盘导航、文本选择
这些需求超出了社区版 Ink 的能力范围。
渲染管线
从 React 组件到终端像素(字符),经历五个阶段:
① React JSX → ② Reconciler
reconciler.ts 实现了一个自定义的 React Reconciler(类似 react-dom,但目标是终端)。它:
- 将 JSX 元素映射到
DOMElement虚拟节点(dom.ts) - 管理组件的挂载、更新、卸载生命周期
- 支持 Hooks(useState, useEffect, useRef 等全部可用)
③ Yoga Layout
布局引擎使用 Yoga(Facebook 开源的 Flexbox 实现),但不是 C++ 绑定版本,而是 native-ts/yoga-layout/ 中的纯 TypeScript 移植版。
layout/yoga.ts:Yoga 绑定层layout/engine.ts:布局引擎(遍历组件树,调用 Yoga 计算)layout/geometry.ts:几何信息(位置、尺寸)layout/node.ts:布局节点(连接 DOM 节点和 Yoga 节点)
支持的 Flexbox 属性:flexDirection, justifyContent, alignItems, padding, margin, width, height, flexGrow, flexShrink, minWidth, maxWidth 等。
④ renderNodeToOutput
render-node-to-output.ts 将带有几何信息的节点树"绘制"到一个二维字符矩阵(output.ts)中:
- 文本节点:按行写入字符
- 边框节点:调用
render-border.ts绘制 Unicode 边框字符 - 嵌套节点:递归处理,子节点覆盖父节点
⑤ logUpdate — 差量刷新
log-update.ts 比较新旧两帧的字符矩阵,只重绘变化的行。这是 Claude Code 不闪烁的关键——如果只有底部状态栏变化,顶部几百行消息不会被重新写入 stdout。
目录结构
src/ink/
├── ink.tsx # Ink 入口 (createRoot, render, 导出公共 API)
├── reconciler.ts # React Reconciler 实现
├── root.ts # Root 容器管理
├── renderer.ts # 渲染器 (协调 layout → render → output)
├── dom.ts # 虚拟 DOM 节点定义
│
├── layout/ # 📐 布局系统
│ ├── yoga.ts # Yoga Flexbox 绑定
│ ├── engine.ts # 布局引擎 (遍历 + 计算)
│ ├── geometry.ts # 几何信息
│ └── node.ts # 布局节点
│
├── render-node-to-output.ts # 🎨 节点 → 字符矩阵
├── render-border.ts # 边框渲染
├── render-to-screen.ts # 矩阵 → ANSI 字符串
├── output.ts # 输出缓冲区 (二维字符矩阵)
├── log-update.ts # 差量屏幕更新
├── frame.ts # 帧管理 (requestAnimationFrame 模拟)
├── screen.ts # 屏幕抽象
│
├── components/ # 🧱 基础组件
│ ├── Box.tsx # Flexbox 容器 (最常用的组件)
│ ├── Text.tsx # 文本节点 (支持颜色、加粗、斜体等)
│ ├── ScrollBox.tsx # 可滚动容器 (虚拟滚动)
│ ├── Button.tsx # 可交互按钮
│ ├── Link.tsx # 超链接 (OSC 8 协议)
│ ├── Spacer.tsx # 弹性空白
│ ├── Newline.tsx # 换行
│ ├── RawAnsi.tsx # 原始 ANSI 透传
│ ├── AlternateScreen.tsx # 备用屏幕 (全屏接管)
│ ├── NoSelect.tsx # 不可选中区域
│ ├── ErrorOverview.tsx # 错误展示
│ ├── App.tsx # Ink 级 App 容器
│ └── *Context.ts # 各种 Context (App, Stdin, Size, Focus, Clock, Cursor)
│
├── hooks/ # 🪝 Ink 专属 Hooks
│ ├── use-input.ts # 键盘输入监听
│ ├── use-stdin.ts # 原始 stdin 访问
│ ├── use-selection.ts # 文本选择状态
│ ├── use-terminal-viewport.ts # 终端视口信息
│ ├── use-declared-cursor.ts # 声明式光标位置
│ ├── use-animation-frame.ts # 动画帧回调
│ ├── use-interval.ts # 定时器
│ ├── use-terminal-title.ts # 设置终端标题
│ ├── use-terminal-focus.ts # 终端焦点状态
│ ├── use-tab-status.ts # Tab 状态 (前台/后台)
│ └── use-search-highlight.ts # 搜索高亮
│
├── events/ # ⚡ 事件系统
│ ├── dispatcher.ts # 事件分发器 (捕获 → 冒泡)
│ ├── emitter.ts # 事件发射器
│ ├── event.ts # 基础事件类
│ ├── input-event.ts # 输入事件
│ ├── keyboard-event.ts # 键盘事件
│ ├── click-event.ts # 鼠标点击事件
│ ├── focus-event.ts # 焦点事件
│ ├── terminal-event.ts # 终端事件 (resize 等)
│ ├── terminal-focus-event.ts # 终端焦点事件
│ └── event-handlers.ts # 事件处理器注册
│
├── termio/ # 📡 终端 IO 解析
│ ├── tokenize.ts # 输入流 Token 化
│ ├── parser.ts # Token → 结构化事件
│ ├── ansi.ts # ANSI 序列识别
│ ├── csi.ts # CSI (Control Sequence Introducer)
│ ├── sgr.ts # SGR (Select Graphic Rendition) — 颜色/样式
│ ├── osc.ts # OSC (Operating System Command) — 超链接等
│ ├── esc.ts # ESC 序列
│ ├── dec.ts # DEC 私有序列 (光标显隐等)
│ └── types.ts # 类型定义
│
├── parse-keypress.ts # 按键序列 → 结构化按键对象
├── terminal.ts # 终端能力检测 (颜色深度, 超链接, 鼠标等)
├── terminal-querier.ts # 终端查询 (DA1, DA2 等)
├── focus.ts # 焦点管理 (Tab/Shift-Tab 导航)
├── selection.ts # 文本选择系统
├── hit-test.ts # 鼠标点击命中测试
├── optimizer.ts # 输出优化器 (合并相同样式)
├── colorize.ts # 颜色处理
├── styles.ts # 样式属性定义
├── stringWidth.ts # 字符宽度计算 (CJK, emoji 感知)
├── measure-text.ts # 文本尺寸测量
├── measure-element.ts # 元素尺寸测量
├── wrap-text.ts # 文本换行 (word-wrap, ANSI 感知)
├── wrapAnsi.ts # ANSI 安全的换行
├── tabstops.ts # Tab 停止位
├── bidi.ts # 双向文本支持
├── searchHighlight.ts # 搜索结果高亮
├── supports-hyperlinks.ts # 终端超链接能力检测
├── clearTerminal.ts # 清屏
├── constants.ts # 常量
├── instances.ts # Ink 实例管理
├── node-cache.ts # 节点缓存
├── line-width-cache.ts # 行宽缓存
├── widest-line.ts # 最宽行计算
├── get-max-width.ts # 最大宽度计算
├── warn.ts # 警告输出
├── squash-text-nodes.ts # 文本节点合并
├── Ansi.tsx # ANSI 组件
└── useTerminalNotification.ts # 终端通知 (bell, iTerm2 等)事件系统详解
Ink 的事件系统模仿了浏览器 DOM 事件模型,支持 捕获 和 冒泡 两个阶段:
终端的原始字节流首先被 termio/ 解析为结构化的 ANSI 序列,然后 parse-keypress.ts 将其转换为人类可理解的按键对象(如 {name: 'a', ctrl: false, shift: true})。最后 dispatcher.ts 将事件从根节点向下传递(捕获),到达目标节点后向上传递(冒泡)。
鼠标事件(如果终端支持)通过类似的路径处理,hit-test.ts 负责根据 (x, y) 坐标找到对应的组件节点。
与业务层的接口
Ink 引擎通过 ink.tsx 导出公共 API,业务代码通过这些导出与 Ink 交互:
typescript
// 组件
export { Box, Text, useInput, useStdin, useTheme, ... } from './ink.js'
// 创建渲染根
export function createRoot(stdout): Root业务组件(components/)和 Hooks(hooks/)构建在 Ink 基础组件之上,不直接操作终端 IO。这种分层确保了业务逻辑与渲染引擎的解耦。