40. packages/tui:终端框架而不是 agent 大脑¶
这个模块干什么¶
@earendil-works/pi-tui 是一个面向文本终端的 UI 库,包描述把重点放在 differential rendering,而不是模型调用、工具调用或 agent 状态机。packages/tui/package.json:2-5
它直接管理 raw mode、终端转义序列、输入分帧、组件焦点和 ANSI 输出,所以不是把 React 树交给 Ink 一类渲染器的薄封装。packages/tui/src/terminal.ts:134-167 packages/tui/src/tui.ts:1258-1329
从可核对的依赖看,这个包只列出 get-east-asian-width 与 marked,没有 ink;源码自己实现了 ProcessTerminal、TUI 和组件协议。packages/tui/package.json:39-45 packages/tui/src/terminal.ts:52-99 packages/tui/src/tui.ts:61-88
若你只关心 agent 的模型循环、工具执行和会话状态,可以先略读本章;这个包的职责是 interactive mode 的输入、布局与重绘表面。packages/tui/package.json:2-5 packages/tui/src/index.ts:1-114
需要排查键盘兼容、中文宽度、粘贴、闪烁、叠层或图片时,再从这里按调用链向下读。packages/tui/src/stdin-buffer.ts:1-17 packages/tui/src/utils.ts:213-270 packages/tui/src/tui.ts:1288-1313
核心文件速查¶
| 文件 | 一句话职责 | 必读优先级 |
|---|---|---|
packages/tui/src/terminal.ts |
把 Node 的 stdin/stdout 包装成 Terminal,并管理 raw mode、粘贴、键盘协议与退出清理。packages/tui/src/terminal.ts:52-94 |
⭐⭐⭐ |
packages/tui/src/tui.ts |
定义组件协议、焦点与 overlay,并把组件行数组差量写成同步 ANSI 输出。packages/tui/src/tui.ts:61-120 packages/tui/src/tui.ts:1258-1329 |
⭐⭐⭐ |
packages/tui/src/keys.ts |
同时识别传统终端序列、Kitty CSI-u 和 modifyOtherKeys,提供 matchesKey。packages/tui/src/keys.ts:497-713 packages/tui/src/keys.ts:803-1204 |
⭐⭐⭐ |
packages/tui/src/keybindings.ts |
用可覆盖的动作名把具体按键从编辑器和选择器逻辑中抽离。packages/tui/src/keybindings.ts:7-52 packages/tui/src/keybindings.ts:155-231 |
⭐⭐ |
packages/tui/src/stdin-buffer.ts |
将可能拆包或合包的 stdin 数据拆成完整转义序列,并单独聚合 bracketed paste。packages/tui/src/stdin-buffer.ts:192-255 packages/tui/src/stdin-buffer.ts:287-387 |
⭐⭐⭐ |
packages/tui/src/native-modifiers.ts |
在 macOS 上可选加载原生模块,补足 Apple Terminal 的 Shift 状态。packages/tui/src/native-modifiers.ts:21-59 |
⭐ |
packages/tui/src/terminal-colors.ts |
解析 OSC 11 背景色回复和终端深浅色偏好回复。packages/tui/src/terminal-colors.ts:28-73 |
⭐ |
packages/tui/src/terminal-image.ts |
探测终端能力,编码 Kitty/iTerm2 图片、OSC 8 链接,并计算图片占用的字符格。packages/tui/src/terminal-image.ts:65-141 packages/tui/src/terminal-image.ts:432-488 |
⭐⭐ |
packages/tui/src/utils.ts |
以 grapheme、ANSI 状态和东亚宽度为单位做测宽、折行、截断与列切片。packages/tui/src/utils.ts:3-49 packages/tui/src/utils.ts:213-270 |
⭐⭐⭐ |
packages/tui/src/index.ts |
把框架、输入、图片、工具函数和内置组件作为公开入口重新导出。packages/tui/src/index.ts:1-114 |
⭐ |
精读¶
1. terminal.ts:先把真实终端变成可控的 Terminal¶
这个文件存在的原因是让 TUI 依赖一个小接口,而不是直接散落地访问 process.stdin 与 process.stdout。packages/tui/src/terminal.ts:49-94
Terminal 的简化签名是 start(onInput, onResize), stop(), write(data), columns, rows,外加光标、清屏、标题和进度操作。packages/tui/src/terminal.ts:52-94
生产实现 ProcessTerminal 记录旧的 raw 状态,并持有输入/resize handler、协议状态、StdinBuffer 和进度定时器。packages/tui/src/terminal.ts:99-110
start(onInput, onResize) 先保存 process.stdin.isRaw,再调用 setRawMode(true)、设为 UTF-8 并 resume()。packages/tui/src/terminal.ts:134-145
这正是自写终端层的一个直接理由:raw mode 下应用收到的是字节和转义序列,不能只把 stdin 当成普通 readline 文本。packages/tui/src/terminal.ts:138-145
启动时还会写入 CSI ? 2004 h 开启 bracketed paste,并立即注册 stdout 的 resize handler。packages/tui/src/terminal.ts:146-150
非 Windows 平台会主动发送 SIGWINCH,用来刷新暂停/恢复后可能过期的终端尺寸。packages/tui/src/terminal.ts:152-156
Windows 路径在 raw mode 之后尝试启用 ENABLE_VIRTUAL_TERMINAL_INPUT,以保留例如 Shift+Tab 的修饰键转义序列。packages/tui/src/terminal.ts:158-166 packages/tui/src/terminal.ts:332-365
queryAndEnableKittyProtocol() 创建输入缓冲器、订阅 stdin、标记协议已推入,再把 Kitty 协商查询写到 stdout。packages/tui/src/terminal.ts:207-226
请求的 Kitty flags 是 1、2、4,分别用于消歧 escape、事件类型、以及 shifted/base-layout 键。packages/tui/src/terminal.ts:208-219
终端返回非零 Kitty flags 时,handleKeyboardProtocolNegotiationSequence() 关闭 modifyOtherKeys,设置本地标志,并调用 setKittyProtocolActive(true)。packages/tui/src/terminal.ts:228-249
若先收到 device attributes,或 Kitty 回复 flags 为零,则走 enableModifyOtherKeys() 写出 CSI > 4 ; 2 m 作为回退。packages/tui/src/terminal.ts:228-249 packages/tui/src/terminal.ts:320-330
容易漏掉的是协商回复也可能被拆包;readKeyboardProtocolNegotiationSequence() 会暂存像 ESC [ 这样的前缀,并在 150ms 后把不完整内容作为普通输入冲刷出去。packages/tui/src/terminal.ts:252-307
setupStdinBuffer() 不直接把 Node 的 data event 交给组件,而是用 StdinBuffer({ timeout: 10 }) 得到单个序列。packages/tui/src/terminal.ts:169-205
data 事件先吞掉键盘协商回复,再经 forwardInputSequence() 转发;paste 事件会重新包回 ESC [ 200 ~ 和 ESC [ 201 ~,保持编辑器的既有粘贴接口。packages/tui/src/terminal.ts:177-205
forwardInputSequence(sequence) 针对 Apple Terminal 的裸回车检查原生 Shift 状态,必要时规范化为 ESC [ 13 ; 2 u。packages/tui/src/terminal.ts:309-318 packages/tui/src/terminal.ts:40-46
drainInput(maxMs = 1000, idleMs = 50) 会先关闭 Kitty/modifyOtherKeys,暂时撤掉 input handler,再等待输入安静或超时。packages/tui/src/terminal.ts:368-404
这一步避免慢速 SSH 上晚到的 Kitty key-release 事件泄漏给恢复后的父 shell。packages/tui/src/terminal.ts:59-65 packages/tui/src/terminal.ts:368-379
stop() 的顺序同样重要:关闭进度与 bracketed paste,撤销键盘协议,销毁缓冲器和监听器,pause() stdin,最后恢复原 raw 状态。packages/tui/src/terminal.ts:406-452
暂停 stdin 在恢复 raw mode 前执行,专门避免缓冲的 Ctrl+D 被父 shell 重新解释。packages/tui/src/terminal.ts:443-451
write() 直接写 stdout,且在设置 PI_TUI_WRITE_LOG 时同步追加原始 ANSI 流到日志文件。packages/tui/src/terminal.ts:111-124 packages/tui/src/terminal.ts:454-463
columns 与 rows 优先读取 stdout 属性,缺失时退回 COLUMNS/LINES,再退回 80×24。packages/tui/src/terminal.ts:465-471
光标、清除、标题和进度方法没有隐式状态机,只是发出相应的 CSI/OSC 序列。packages/tui/src/terminal.ts:473-523
2. tui.ts:组件行数组到差量屏幕输出¶
Component 的简化协议是 render(width): string[]、可选 handleInput(data)、可选 wantsKeyRelease 与 invalidate()。packages/tui/src/tui.ts:61-88
每个 render() 返回一行一个字符串,而不是虚拟 DOM;这使比较前后两帧的每一行成为直接操作。packages/tui/src/tui.ts:64-70 packages/tui/src/tui.ts:1372-1386
Focusable 只要求 focused: boolean,CURSOR_MARKER 是组件在假光标位置放入的零宽 APC 标记。packages/tui/src/tui.ts:98-120
Container 依次调用每个 child 的 render(width) 并拼接结果,TUI 继承它,所以基础界面就是一个组件树。packages/tui/src/tui.ts:253-290 packages/tui/src/tui.ts:292-336
TUI 保存上一帧行、上一帧尺寸、焦点组件、输入监听器、渲染定时器、光标位置和 overlay 栈。packages/tui/src/tui.ts:295-327
start() 把终端输入回调接到 handleInput(),resize 回调接到 requestRender(),隐藏硬件光标、按需开颜色通知、查询图片格尺寸并请求首帧。packages/tui/src/tui.ts:637-649
requestRender() 会合并同一轮的多个请求;普通路径通过 process.nextTick() 后按最小 16ms 间隔调度。packages/tui/src/tui.ts:716-763
所以组件连续改变状态时不会为每次赋值立刻写终端,而会被合并成更接近一帧的输出。packages/tui/src/tui.ts:716-763
handleInput(data) 首先消费 OSC 11 与色彩方案报告,再让全局 listener 按顺序消费或改写数据。packages/tui/src/tui.ts:765-788 packages/tui/src/tui.ts:841-875
它随后消费图片 cell-size 回复、处理全局 shift+ctrl+d,校验焦点 overlay 的可见性,最后才把按键交给焦点组件。packages/tui/src/tui.ts:790-839
Kitty 的 key release 默认被过滤,只有把 wantsKeyRelease 设为真时焦点组件才会收到它。packages/tui/src/tui.ts:76-80 packages/tui/src/tui.ts:829-838
doRender() 读取当前宽高,先渲染组件树,再将 overlays 合成进基础行数组。packages/tui/src/tui.ts:1258-1280
它会在行末追加 SGR reset 和 OSC 8 reset,防止颜色或链接跨物理行泄漏。packages/tui/src/tui.ts:1097-1108
随后 extractCursorPosition() 从可视 viewport 底部寻找 CURSOR_MARKER,计算显示列,删除标记。packages/tui/src/tui.ts:1230-1256
首帧调用 fullRender(false),不清屏;宽度改变必然 fullRender(true),普通高度改变也全量重绘,但 Termux 是特例。packages/tui/src/tui.ts:1340-1361
全量路径用 CSI ? 2026 h/l 把清屏、图片删除和所有新行包在 synchronized output 中,并一次 terminal.write(buffer)。packages/tui/src/tui.ts:1287-1329
普通路径比较 newLines 与 previousLines,找出 first/last changed line。packages/tui/src/tui.ts:1372-1398
若变化在上一次可视 viewport 之上,就不能安全地局部改写,会退回全量重绘。packages/tui/src/tui.ts:1458-1464
否则它移动到第一处变化,清除并仅写 changed range,同样置于 synchronized output 中。packages/tui/src/tui.ts:1466-1504 packages/tui/src/tui.ts:1524-1607
局部写入前还会检查非图片行的 visibleWidth(line);超宽时记录所有行、停止 TUI 并抛错,强迫组件遵守宽度合同。packages/tui/src/tui.ts:1524-1552
这套按行比较、显式 cursor movement 和 synchronized output 的机制,就是源码能证明的“自己写渲染器”价值,而不是对 Ink 的历史动机猜测。packages/tui/src/tui.ts:1258-1329 packages/tui/src/tui.ts:1372-1607
positionHardwareCursor() 把标记坐标转成垂直 CSI movement 与绝对列 CSI n G,并按配置显示或继续隐藏硬件光标。packages/tui/src/tui.ts:1627-1663
这让假光标可负责视觉样式,同时真实光标仍能定位 IME 候选窗。packages/tui/src/tui.ts:98-120 packages/tui/src/tui.ts:1627-1663
3. keys.ts:把许多终端方言归一到按键判断¶
这个文件存在是因为同一逻辑按键在传统终端、Kitty 和 modifyOtherKeys 下会有不同字节表示。packages/tui/src/keys.ts:1-19 packages/tui/src/keys.ts:497-713
KeyId 是基础键加 ctrl、shift、alt、super 组合的类型;Key 提供 Key.enter、Key.ctrl("c") 这样的构造器。packages/tui/src/keys.ts:141-152 packages/tui/src/keys.ts:154-252
setKittyProtocolActive() 和 isKittyProtocolActive() 保存全局协议状态,状态由 ProcessTerminal 的协商路径设置。packages/tui/src/keys.ts:25-40 packages/tui/src/terminal.ts:233-242
parseKittySequence(data) 识别 CSI-u、带修饰箭头、功能键、Home/End,并提取 codepoint、modifier 与 press/repeat/release。packages/tui/src/keys.ts:501-650
matchesKittySequence() 会忽略 CapsLock/NumLock 位、规范化 keypad 功能键,并只在原 codepoint 不是已识别拉丁字母或符号时使用 base layout key。packages/tui/src/keys.ts:653-694
这个限制避免 Dvorak、Colemak 或 xremap 的物理键位回退把本来可识别的键误判成别的绑定。packages/tui/src/keys.ts:674-690
isKeyRelease(data) 与 isKeyRepeat(data) 通过 Kitty event-type 片段判断事件,但先排除包含 bracketed paste marker 的文本。packages/tui/src/keys.ts:520-577
matchesKey(data, keyId) 先拆出修饰位,再按 escape、space、tab、enter、backspace、导航键和可打印键分别处理。packages/tui/src/keys.ts:788-829 packages/tui/src/keys.ts:831-1204
例如 Shift+Enter 优先接受 Kitty、modifyOtherKeys 和启用 Kitty 后的专用 legacy mapping;Alt+Enter 在非 Kitty 模式才把 ESC CR 当作匹配。packages/tui/src/keys.ts:878-932
parseKey(data) 与 matchesKey() 不同:前者把已识别输入反向格式化成字符串,如 ctrl+c,并处理协议模式造成的歧义。packages/tui/src/keys.ts:1206-1327
decodeKittyPrintable() 只接受无修饰或仅 Shift 的可打印 CSI-u,拒绝 Ctrl/Alt 等输入,避免把快捷键误插入编辑器文本。packages/tui/src/keys.ts:1333-1383
decodePrintableKey() 先尝试 Kitty,再尝试 modifyOtherKeys;它是普通文本插入所需的窄入口。packages/tui/src/keys.ts:1385-1401
4. keybindings.ts:把“动作”与“字节”隔开¶
Keybindings 接口列出编辑、通用输入和选择器动作名,且注释明确允许下游通过 declaration merging 增加绑定。packages/tui/src/keybindings.ts:3-42
TUI_KEYBINDINGS 为每个动作给出默认 KeyId 或候选数组,例如 word-left 有 Alt+Left、Ctrl+Left、Alt+B 三个默认键。packages/tui/src/keybindings.ts:54-134
KeybindingsManager 的简化签名是 new KeybindingsManager(definitions, userBindings?)、matches(data, action)、getKeys(action) 与 setUserBindings(config)。packages/tui/src/keybindings.ts:155-231
构造和更新都会执行 rebuild(),它先归一化数组、去重、记录多个用户动作声明同一个键的冲突,再决定每个动作使用默认还是用户覆盖。packages/tui/src/keybindings.ts:141-192 packages/tui/src/keybindings.ts:214-229
matches(data, action) 不自己解析序列,而是遍历解析后的绑定并调用 matchesKey()。packages/tui/src/keybindings.ts:194-200
模块维护一个懒创建的全局 manager;未显式 setKeybindings() 时,getKeybindings() 使用 TUI_KEYBINDINGS。packages/tui/src/keybindings.ts:233-244
容易漏掉的是冲突只针对用户配置中同一 key 被多个已知动作声明的情况,默认表本身没有在这里做互斥裁决。packages/tui/src/keybindings.ts:167-191
5. stdin-buffer.ts:不要把半个 escape 当成按键¶
Node 的 stdin data event 不保证一个事件就是一个终端事件;文件头用拆成三块的 SGR mouse 序列说明了这个问题。packages/tui/src/stdin-buffer.ts:1-17
StdinBuffer 继承 typed EventEmitter,对外只发 data 与 paste 两类事件。packages/tui/src/stdin-buffer.ts:257-285
isCompleteSequence() 分流 CSI、OSC、DCS、APC、SS3 和 Meta 序列;CSI 完整性由终止字节范围判断,并为鼠标 SGR 做额外格式检查。packages/tui/src/stdin-buffer.ts:29-78 packages/tui/src/stdin-buffer.ts:80-126
extractCompleteSequences(buffer) 从左到右抽取完整序列,普通文本则一个字符一个字符发出。packages/tui/src/stdin-buffer.ts:192-255
它有一个 WezTerm 特例:若原始 Escape press 紧接完整 Kitty release,先单独发第一个 Escape,避免剩余 CSI-u 被当作普通文本。packages/tui/src/stdin-buffer.ts:207-231
process(data) 会取消旧 timeout,把高字节单 Buffer 转成 ESC + (byte - 128) 的兼容表示,然后追加到内部 buffer。packages/tui/src/stdin-buffer.ts:287-313
检测到 ESC [ 200 ~ 后,它进入 paste mode,跨多个 chunk 累积内容,直到 ESC [ 201 ~ 才发一个 paste 事件。packages/tui/src/stdin-buffer.ts:315-369
非粘贴输入经 extractCompleteSequences() 发出;仍有 remainder 时,最多等待默认 10ms 后 flush()。packages/tui/src/stdin-buffer.ts:371-387 packages/tui/src/stdin-buffer.ts:400-414
emitDataSequence() 还会抑制“Kitty 未修饰可打印 CSI-u 后紧跟同一原字符”的重复输入。packages/tui/src/stdin-buffer.ts:184-190 packages/tui/src/stdin-buffer.ts:389-398
6. native-modifiers.ts:macOS 的小而关键的补偿¶
这个文件只定义了 shift、command、control、option 四种 macOS 原生修饰键名称。packages/tui/src/native-modifiers.ts:7-10
loadNativeModifiersHelper() 只在 darwin 的 x64/arm64 上尝试加载 .node 模块,并依次检查包目录和可执行文件旁的候选路径。packages/tui/src/native-modifiers.ts:21-48
加载结果被缓存为 helper、null 或 undefined,失败不会抛给 UI。packages/tui/src/native-modifiers.ts:13-24 packages/tui/src/native-modifiers.ts:36-48
isNativeModifierPressed(key) 找不到 helper 或原生调用报错时都返回 false。packages/tui/src/native-modifiers.ts:51-59
它的唯一可见接点是 terminal.ts 对 Apple Terminal 的 Shift+Enter 规范化。packages/tui/src/terminal.ts:5-6 packages/tui/src/terminal.ts:309-318
7. terminal-colors.ts:把终端回复变成小型值对象¶
RgbColor 是 { r, g, b },而 TerminalColorScheme 只允许 dark 或 light。packages/tui/src/terminal-colors.ts:1-7
parseOsc11BackgroundColor(data) 只接受 OSC 11 回复,并能解析 #RRGGBB、12 位 hex 和 rgb:/rgba: 通道格式。packages/tui/src/terminal-colors.ts:17-65
不合法的通道、长度或格式一律返回 undefined,所以调用者能把“不支持”和“不可解析”当成可恢复结果。packages/tui/src/terminal-colors.ts:17-26 packages/tui/src/terminal-colors.ts:35-65
parseTerminalColorSchemeReport(data) 识别 CSI ? 997 ; 1/2 n,其中 2 是 light,其余匹配值是 dark。packages/tui/src/terminal-colors.ts:28-30 packages/tui/src/terminal-colors.ts:67-73
TUI.handleInput() 在交给组件前调用这些解析路径,因此终端报告不会误变成编辑器字符。packages/tui/src/tui.ts:765-771 packages/tui/src/tui.ts:841-875
8. terminal-image.ts:能力探测与协议编码¶
TerminalCapabilities 分别表示图片协议、true color 和 hyperlinks 是否可用。packages/tui/src/terminal-image.ts:3-9
detectCapabilities() 优先对 tmux 和 screen 保守处理,再通过环境变量识别 Kitty、Ghostty、WezTerm、Warp、iTerm2、Windows Terminal、VSCode、Alacritty 等终端。packages/tui/src/terminal-image.ts:65-125
tmux 路径关闭图片,并通过短超时的 tmux display-message 探测客户端是否转发 hyperlinks。packages/tui/src/terminal-image.ts:44-63 packages/tui/src/terminal-image.ts:72-76
getCapabilities() 懒缓存探测结果,resetCapabilitiesCache() 和 setCapabilities() 为重测与测试留出入口。packages/tui/src/terminal-image.ts:127-141
encodeKitty(base64Data, options?) 将数据按 4096 字符分块,首块与中间块设置 m=1,末块设置 m=0。packages/tui/src/terminal-image.ts:165-209
encodeITerm2() 生成 OSC 1337;File= 形式,文件名先 base64,宽高与宽高比按 options 加入。packages/tui/src/terminal-image.ts:227-250
calculateImageCellSize() 用像素尺寸和 cell 的像素宽高缩放,再以 ceil 计算 columns/rows,并夹到最大格数。packages/tui/src/terminal-image.ts:257-281
PNG、JPEG、GIF、WebP 都由各自头部解析函数取得像素尺寸,getImageDimensions() 按 MIME type 分派。packages/tui/src/terminal-image.ts:291-430
renderImage() 先检查能力,再用当前 cell dimensions 计算大小,并返回 Kitty 或 iTerm2 序列与预计 rows;无图片协议时返回 null。packages/tui/src/terminal-image.ts:432-466
TUI.start() 仅在图片能力存在时查询 cell 像素尺寸,收到 CSI 6 ; height ; width t 后更新尺寸、invalidate 所有组件并重绘。packages/tui/src/tui.ts:679-687 packages/tui/src/tui.ts:877-895
hyperlink(text, url) 输出 OSC 8 开闭序列,imageFallback() 在无图形能力时生成含 MIME、尺寸、文件名的文本占位。packages/tui/src/terminal-image.ts:468-488
9. utils.ts:显示宽度不是 JavaScript 字符串长度¶
本文件共享 Intl.Segmenter 的 grapheme 与 word 实例,并引入 get-east-asian-width。packages/tui/src/utils.ts:1-19
graphemeWidth() 将 tab 视为 3 列、控制/mark 类簇视为 0、RGI emoji 视为 2,并对 regional indicator 采用保守的 2 列。packages/tui/src/utils.ts:162-211
visibleWidth(str) 的 ASCII 快路径直接取长度;其他文本先把 tab 展为 3 空格、剥离 ANSI/OSC/APC,再逐 grapheme 累加并缓存最多 512 个非 ASCII 结果。packages/tui/src/utils.ts:213-270
因此组件不能用 text.length 判断是否越界,TUI 最终检查的也是 visibleWidth()。packages/tui/src/utils.ts:213-270 packages/tui/src/tui.ts:1524-1552
normalizeTerminalOutput() 会把泰语/老挝语 AM 元音作兼容分解,并只展开可见文本的 tab,不碰转义序列内部的 tab。packages/tui/src/utils.ts:273-306
extractAnsiCode(str, pos) 能识别 CSI、以 BEL/ST 结束的 OSC 和 APC;测宽、折行、截断与切列都依赖它跳过非显示字节。packages/tui/src/utils.ts:308-349 packages/tui/src/utils.ts:232-253
wrapTextWithAnsi(text, width) 逐逻辑行折行,并用 AnsiCodeTracker 在物理行之间恢复 SGR/OSC 8 状态。packages/tui/src/utils.ts:387-610 packages/tui/src/utils.ts:704-819
词元切分对汉字、平假名、片假名、韩文、注音符号逐 grapheme 断开,所以 CJK 文本不是只能在空格处折行。packages/tui/src/utils.ts:48-49 packages/tui/src/utils.ts:625-702
truncateToWidth(text, maxWidth, ellipsis = "...", pad = false) 处理 ANSI、tab、宽字符和省略号,并在需要时补齐到精确列宽。packages/tui/src/utils.ts:925-1072
sliceByColumn()/sliceWithWidth() 以显示列而不是 UTF-16 下标切片,并可用 strict 排除跨越右边界的宽字符。packages/tui/src/utils.ts:1074-1128
overlay 合成使用 extractSegments() 一次扫描得到前后片段,并让后片段继承 overlay 前已有的样式状态。packages/tui/src/utils.ts:1130-1209 packages/tui/src/tui.ts:1179-1228
数据流¶
下面追踪一次普通按键;实际 stdin chunk 是否刚好等于一次按键,由 StdinBuffer 决定。packages/tui/src/stdin-buffer.ts:1-17
终端键盘字节
-> ProcessTerminal 的 stdin "data" handler
-> StdinBuffer.process()
-> "data" 事件中的完整 sequence
-> ProcessTerminal.forwardInputSequence()
-> TUI.handleInput()
-> input listeners / 协议回复消费 / focusedComponent.handleInput()
-> TUI.requestRender()
-> TUI.doRender()
-> Component.render(width) 返回 string[]
-> 前后帧逐行比较,构造 CSI ? 2026 同步输出
-> Terminal.write() -> process.stdout.write()
ProcessTerminal 把原始 stdin data 交给 StdinBuffer.process(),而不是直接调用 UI input handler。packages/tui/src/terminal.ts:201-205
StdinBuffer 先处理粘贴状态或抽取完整转义序列,再通过 data 事件逐个发出。packages/tui/src/stdin-buffer.ts:315-387
ProcessTerminal 对每个 sequence 先消化键盘协议协商,未被消化的内容经 Apple Terminal 规范化后才调用 onInput。packages/tui/src/terminal.ts:177-192 packages/tui/src/terminal.ts:309-318
TUI.start() 把该 onInput 指向 TUI.handleInput(),所以数据进入全局监听器、协议回复处理、overlay 焦点修复和焦点组件。packages/tui/src/tui.ts:637-649 packages/tui/src/tui.ts:765-839
组件的 handleInput() 改变自身状态后,TUI 在调用结束时请求重绘。packages/tui/src/tui.ts:829-838
调度器合并请求,doRender() 产出行、提取 IME 光标标记、按行比较并生成同步 ANSI buffer。packages/tui/src/tui.ts:716-763 packages/tui/src/tui.ts:1258-1329 packages/tui/src/tui.ts:1372-1607
最终 Terminal.write() 落到 process.stdout.write(),所以屏幕更新的最末端仍是明确可见的 ANSI 字节流。packages/tui/src/terminal.ts:454-463
自测¶
-
为什么
ProcessTerminal.start()要先进入 raw mode,再开启 bracketed paste 和键盘协议协商?packages/tui/src/terminal.ts:134-167 -
StdinBuffer怎样区分一个尚未结束的 CSI 序列、普通文本与跨 chunk 的 bracketed paste?packages/tui/src/stdin-buffer.ts:29-178packages/tui/src/stdin-buffer.ts:315-387 -
在哪几种条件下
TUI.doRender()放弃局部重绘并走fullRender(true)?packages/tui/src/tui.ts:1347-1369packages/tui/src/tui.ts:1458-1464 -
为什么
matchesKey()需要同时理解 legacy、Kitty 和modifyOtherKeys序列?packages/tui/src/keys.ts:497-713packages/tui/src/keys.ts:803-1204 -
为什么一个组件应使用
visibleWidth()、truncateToWidth()或sliceByColumn(),而不是string.length和普通slice()?packages/tui/src/utils.ts:213-270packages/tui/src/utils.ts:925-1128packages/tui/src/tui.ts:1524-1552