Skip to content

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。这种分层确保了业务逻辑与渲染引擎的解耦。