41. packages/tui 组件:从 Editor 到可复用显示块¶
这个模块干什么¶
components/ 把 TUI 所需的 render(width): string[] 协议实现成输入、编辑器、列表、Markdown、图片和布局小件。packages/tui/src/tui.ts:61-88 packages/tui/src/index.ts:11-29
其中 Editor 是多行输入的重心:它保存逻辑行与光标,处理历史、粘贴、撤销、Emacs 风格 kill/yank、可视行移动和异步补全。packages/tui/src/components/editor.ts:208-343
EditorComponent 则是给扩展的兼容合同;扩展可替换编辑实现,但仍须提供文本读取、写入、输入处理和提交/变更回调。packages/tui/src/editor-component.ts:4-34
其余组件不是另一套框架,而是同一行数组协议上的小实现,通常复用 visibleWidth()、wrapTextWithAnsi()、truncateToWidth() 与全局 keybinding。packages/tui/src/components/text.ts:1-105 packages/tui/src/components/select-list.ts:1-229
核心文件速查¶
| 文件 | 一句话职责 | 必读优先级 |
|---|---|---|
packages/tui/src/editor-component.ts |
规定自定义编辑器必须和可选能力,供核心应用与扩展互换编辑实现。packages/tui/src/editor-component.ts:4-74 |
⭐⭐⭐ |
packages/tui/src/components/editor.ts |
实现多行编辑、可视折行、历史、粘贴占位、补全、撤销、kill/yank 与 IME 光标。packages/tui/src/components/editor.ts:270-353 |
⭐⭐⭐ |
packages/tui/src/autocomplete.ts |
定义 provider 合同,并实现 slash command、普通路径和 @ 附件路径补全。packages/tui/src/autocomplete.ts:219-270 packages/tui/src/autocomplete.ts:272-786 |
⭐⭐⭐ |
packages/tui/src/fuzzy.ts |
为命令和设置搜索提供按序字符匹配与排序分数。packages/tui/src/fuzzy.ts:1-137 |
⭐⭐ |
packages/tui/src/undo-stack.ts |
以 structuredClone() 保存泛型状态快照。packages/tui/src/undo-stack.ts:1-28 |
⭐⭐ |
packages/tui/src/kill-ring.ts |
保存连续删除文本,并支持 yank 与 yank-pop 轮换。packages/tui/src/kill-ring.ts:1-46 |
⭐⭐ |
packages/tui/src/word-navigation.ts |
用 Intl.Segmenter 和标点规则计算 word-left/word-right 的目标下标。packages/tui/src/word-navigation.ts:1-117 |
⭐⭐ |
packages/tui/src/components/select-list.ts |
渲染可滚动选择列表,是编辑器补全菜单的基础。packages/tui/src/components/select-list.ts:12-58 packages/tui/src/components/select-list.ts:74-137 |
⭐⭐⭐ |
packages/tui/src/components/markdown.ts |
用 marked token 渲染 ANSI Markdown、表格、列表、链接和代码块,并缓存结果。packages/tui/src/components/markdown.ts:1-53 packages/tui/src/components/markdown.ts:110-242 |
⭐⭐⭐ |
packages/tui/src/components/input.ts |
提供单行输入、横向滚动、粘贴、undo 与 kill/yank。packages/tui/src/components/input.ts:16-47 packages/tui/src/components/input.ts:48-446 |
⭐⭐ |
components/text.ts、box.ts、truncated-text.ts、spacer.ts |
提供文本折行、带背景容器、单行截断和垂直留白。packages/tui/src/components/text.ts:4-105 packages/tui/src/components/box.ts:11-137 packages/tui/src/components/truncated-text.ts:4-64 packages/tui/src/components/spacer.ts:3-27 |
⭐ |
components/loader.ts、cancellable-loader.ts、image.ts、settings-list.ts |
提供转圈/取消、图片协议适配和可搜索设置面板。packages/tui/src/components/loader.ts:14-92 packages/tui/src/components/cancellable-loader.ts:4-40 packages/tui/src/components/image.ts:12-125 packages/tui/src/components/settings-list.ts:7-250 |
⭐⭐ |
精读¶
1. editor-component.ts:替换编辑器时必须守住的合同¶
EditorComponent 继承通用 Component,因此替代实现仍要能 render(width)、invalidate(),并遵守 TUI 的输入接口。packages/tui/src/editor-component.ts:1-2 packages/tui/src/editor-component.ts:11-24 packages/tui/src/tui.ts:61-88
必需的简化签名是 getText(): string、setText(text): void、handleInput(data): void、onSubmit?: (text) => void 和 onChange?: (text) => void。packages/tui/src/editor-component.ts:13-34
这意味着调用方可以只依赖“读取、写入、提交、变化”四种能力,而不关心底层是默认编辑器还是 Vim/Emacs 风格扩展。packages/tui/src/editor-component.ts:4-10 packages/tui/src/editor-component.ts:13-34
addToHistory?(text) 是可选的历史接口,默认 Editor 会实现它。packages/tui/src/editor-component.ts:36-40 packages/tui/src/components/editor.ts:395-409
insertTextAtCursor?(text) 和 getExpandedText?() 是给程序化插入及 paste marker 展开留下的可选能力。packages/tui/src/editor-component.ts:43-53
setAutocompleteProvider?(provider) 让替代编辑器可接入统一的 provider,而不用暴露默认 Editor 的内部状态。packages/tui/src/editor-component.ts:56-60
外观侧只有可选 borderColor、setPaddingX() 与 setAutocompleteMaxVisible(),所以扩展不必假装拥有默认编辑器全部的布局配置。packages/tui/src/editor-component.ts:63-73
容易漏掉的是接口将 handleInput 列为必需成员;自定义 editor 不能只实现渲染,否则获得焦点后无法参与输入管线。packages/tui/src/editor-component.ts:22-24
2. components/editor.ts:多行编辑器的状态、布局和输入¶
Editor 实现 Component, Focusable,因此既参与每帧渲染,也能在获得焦点时放置 CURSOR_MARKER 给 TUI 定位 IME 光标。packages/tui/src/components/editor.ts:270-279 packages/tui/src/tui.ts:98-120
它的核心状态是 lines: string[]、cursorLine、cursorCol,初始值是单个空逻辑行和 (0, 0) 光标。packages/tui/src/components/editor.ts:208-220 packages/tui/src/components/editor.ts:270-275
这里的 cursorCol 是逻辑字符串的下标,而渲染和换行时要另用 visibleWidth() 处理宽字符。packages/tui/src/components/editor.ts:893-979 packages/tui/src/utils.ts:213-270
构造函数接收 TUI、EditorTheme 与可选 padding/补全最大行数,并把 padding 夹成非负整数、补全可见数夹到 3 到 20。packages/tui/src/components/editor.ts:228-236 packages/tui/src/components/editor.ts:345-353
EditorTheme 只要求边框染色函数和 SelectListTheme,因为补全下拉菜单复用 SelectList。packages/tui/src/components/editor.ts:228-231 packages/tui/src/components/editor.ts:2132-2138
setPaddingX() 与 setAutocompleteMaxVisible() 改值后请求 TUI 重绘,不保存自己的渲染缓存。packages/tui/src/components/editor.ts:365-387 packages/tui/src/components/editor.ts:478-480
setAutocompleteProvider(provider) 会先取消旧请求,再保存 provider,并按 provider 的 trigger characters 更新本地 trigger pattern。packages/tui/src/components/editor.ts:389-393 packages/tui/src/components/editor.ts:2219-2230
文本布局:逻辑行先折成 visual lines¶
wordWrapLine(line, maxWidth, preSegmented?) 返回带 text、startIndex、endIndex 的 TextChunk[],而不是只返回字符串。packages/tui/src/components/editor.ts:93-114
它优先在“空白后接非空白”处换行,并允许任一侧为 CJK grapheme 的边界成为换行点。packages/tui/src/components/editor.ts:130-199
没有合适断点时,超长词按 grapheme 强制切开;一个过宽的 atomic paste marker 仅在视觉层递归拆开,逻辑编辑仍把它看作单元。packages/tui/src/components/editor.ts:142-178
segmentWithMarkers() 会把有效 [paste #N ...] marker 内部的多个 grapheme 合并为单个 segment,失效 ID 的看起来相同的文本不会合并。packages/tui/src/components/editor.ts:21-91
validPasteIds() 从 paste map 的 key 构造集合,segment(text, mode) 再选择 word 或 grapheme segmenter。packages/tui/src/components/editor.ts:355-363
layoutText(contentWidth) 遍历每个逻辑行;能放下的行直接生成一个 LayoutLine,放不下的行按 wordWrapLine() 展开并定位哪一块拥有光标。packages/tui/src/components/editor.ts:893-979
render(width) 会从外部终端行数取 30% 作为编辑区最大可视行数,但最少保留 5 行。packages/tui/src/components/editor.ts:482-519
它根据 cursor 所在 layout line 调整 scrollOffset,再把超出上方或下方的行数画成带 ↑/↓ 的边框提示。packages/tui/src/components/editor.ts:507-531 packages/tui/src/components/editor.ts:581-588
每个可见文本行的 cursor 都使用 inverse video;获得焦点时,反色字符前还会插入零宽 CURSOR_MARKER。packages/tui/src/components/editor.ts:533-579
开启补全时,render() 将 autocompleteList.render(contentWidth) 的行追加在编辑器底边之后,并加回左右 padding。packages/tui/src/components/editor.ts:590-600
普通输入:先是模式机,再是编辑操作¶
handleInput(data) 一开始先检查 jumpMode;再次按 jump 热键取消,否则将下一个可打印字符交给 jumpToChar()。packages/tui/src/components/editor.ts:603-625 packages/tui/src/components/editor.ts:2030-2062
之后它缓冲 bracketed paste,直到检测到结束 marker 后才调用 handlePaste();marker 之后同一 chunk 的剩余输入会递归继续处理。packages/tui/src/components/editor.ts:627-651
tui.input.copy 对应的 Ctrl+C 在 editor 内直接 return,把退出或清屏的决定留给更上层。packages/tui/src/components/editor.ts:653-656
undo、补全模式、Tab、删除、kill/yank、光标移动、换行、提交、历史和 page navigation 都经 getKeybindings().matches() 判断。packages/tui/src/components/editor.ts:658-890 packages/tui/src/keybindings.ts:194-200
当补全菜单存在时,取消键关闭菜单,上下键只交给 SelectList,Tab 或确认键把选中项目交给 provider 的 applyCompletion()。packages/tui/src/components/editor.ts:664-723
Tab 在普通模式中若位于未带参数的 slash command 上,就请求 slash 补全;否则强制请求文件补全。packages/tui/src/components/editor.ts:725-729 packages/tui/src/components/editor.ts:2144-2163
换行接受 keybinding 的 tui.input.newLine 以及多种终端序列;输入前一个字符是反斜杠且按键确属 Enter 时,会把反斜杠删掉并提交,作为不支持 Shift+Enter 的终端回退。packages/tui/src/components/editor.ts:785-818 packages/tui/src/components/editor.ts:1249-1258
普通可打印文本通过 decodePrintableKey(data) 优先解析 CSI-u/modifyOtherKeys,解析不到才接收首字符码点至少为 32 的原始数据。packages/tui/src/components/editor.ts:875-890 packages/tui/src/keys.ts:1385-1401
insertCharacter() 对连续 word 字符合并 undo;遇到空白或动作不是 type-word 时先保存 snapshot。packages/tui/src/components/editor.ts:1095-1121
插入后,/ 只在消息开头自动触发命令补全;@、# 和 provider 自定义符号只在 token 边界触发;命令或符号上下文中继续输入字母数字、点、连字符、下划线也会更新补全。packages/tui/src/components/editor.ts:1123-1154
大粘贴:界面保持短,提交仍保留全文¶
handlePaste() 先取消补全并压入 undo snapshot,再把部分终端把 Ctrl 字节重编码成 CSI-u 的内容还原为文字控制字节。packages/tui/src/components/editor.ts:1156-1173
它规范化换行与 tab,过滤除换行外的不可打印字符;如果粘贴路径紧贴单词字符,会先补一个空格。packages/tui/src/components/editor.ts:1175-1192
超过 10 行或 1000 字符的粘贴不直接放进 state.lines,而是保存到 pastes map,并插入 [paste #N +X lines] 或 [paste #N Y chars] marker。packages/tui/src/components/editor.ts:1194-1222
getText() 返回当前 marker 文本;getExpandedText() 会按 map 把 marker 展回原内容。packages/tui/src/components/editor.ts:981-1000
提交时 submitValue() 会先展开 marker、trim() 结果、清空编辑器、pastes、历史浏览状态与 undo stack,再触发 onChange("") 和 onSubmit(result)。packages/tui/src/components/editor.ts:1260-1274
删除一个 marker 时,handleBackspace() 还会删除相应 paste,并按 ID 重新编号 map 与所有剩余 marker,避免 marker 与存储内容错位。packages/tui/src/components/editor.ts:1276-1359
光标:grapheme、visual line 与 sticky column¶
左右移动不是 cursorCol ± 1;moveCursor() 对当前行做 grapheme segmentation,以整个 emoji 或组合字符为单位跨越。packages/tui/src/components/editor.ts:1790-1851
行间移动先由 buildVisualLineMap(width) 映射 logical line 和折行 chunk,再由 findVisualLineAt() 找到当前位置。packages/tui/src/components/editor.ts:1725-1788
moveToVisualLine() 维护 preferred visual column;目标较短时会保留偏好列,之后遇到够长的行再回到该列。packages/tui/src/components/editor.ts:1371-1508
光标若落在多 grapheme 的 atomic marker 中,会 snap 到 marker 开头并记录原位置;向下跨越 marker continuation 时可以跳到 marker 后的 visual line。packages/tui/src/components/editor.ts:1414-1455
pageScroll() 用同一 visual line 映射,以 max(5, floor(rows * 0.3)) 为页大小移动光标。packages/tui/src/components/editor.ts:1853-1867
moveWordBackwards() 和 moveWordForwards() 跨逻辑行时跳到相邻行端点,同一行时把 marker-aware segmenter 传给 findWordBackward/Forward()。packages/tui/src/components/editor.ts:1869-1889 packages/tui/src/components/editor.ts:2064-2083
历史、删除、kill/yank 与 undo¶
addToHistory(text) 会 trim 空内容、避免紧邻重复,并将最新记录放在 index 0,最多存 100 条。packages/tui/src/components/editor.ts:395-409
第一次进入 history browsing 时,navigateHistory() 保存当前草稿与 undo snapshot;向下离开历史时恢复草稿。packages/tui/src/components/editor.ts:427-462
删除到行首/行尾和删词都会将被删文本放进 KillRing;跨行时把换行符本身也作为 kill 文本。packages/tui/src/components/editor.ts:1521-1586 packages/tui/src/components/editor.ts:1588-1673
yank() 插入 ring 最新项,yankPop() 只允许紧跟 yank/yank-pop,并先删旧插入内容、轮换 ring、再插入新项。packages/tui/src/components/editor.ts:1891-1926 packages/tui/src/components/editor.ts:1970-2010
pushUndoSnapshot() 的 snapshot 不只含 text state,还含 paste map 与 counter;undo() 因此能回滚大粘贴 marker 与它的隐藏正文。packages/tui/src/components/editor.ts:2012-2028
3. autocomplete.ts 与 fuzzy.ts:provider 是异步的,筛选是稳定的¶
AutocompleteProvider 的简化签名是异步 getSuggestions(lines, line, col, { signal, force? })、同步 applyCompletion(...),以及可选 triggerCharacters、shouldTriggerFileCompletion()。packages/tui/src/autocomplete.ts:241-270
返回值中的 items 给菜单,prefix 指明当前被替换的部分;applyCompletion() 必须返回新的 lines、cursorLine 与 cursorCol。packages/tui/src/autocomplete.ts:236-266
默认 CombinedAutocompleteProvider 接受 slash command/普通 item 列表、basePath 和可选 fdPath。packages/tui/src/autocomplete.ts:272-282
getSuggestions() 优先识别 @ 前缀,使用异步 fuzzy 文件查找;随后在非 force 状态处理首字符为 / 的命令名或命令参数;最后尝试普通路径。packages/tui/src/autocomplete.ts:284-373
applyCompletion() 对 slash command 会补 / 与尾随空格,对 @ 附件文件补空格但目录不补,且会避免 quoted item 与光标后已有引号重复。packages/tui/src/autocomplete.ts:375-460
普通路径补全使用 readdirSync(),处理 ~、相对/绝对路径、目录 symlink、目录优先排序、保留 ./ 以及带空格路径的引号。packages/tui/src/autocomplete.ts:509-693
带 @ 的 fuzzy 文件补全只有在提供 fdPath 且 signal 未 abort 时才工作;它调用 fd,跟随文件树、包含隐藏文件、排除 .git,最多取 100 条候选再筛到 20 条。packages/tui/src/autocomplete.ts:123-217 packages/tui/src/autocomplete.ts:719-772
walkDirectoryWithFd() 为 signal 注册 abort handler,abort 时杀掉仍在运行的子进程,并将失败、空 stdout 或非零退出统一为 []。packages/tui/src/autocomplete.ts:158-216
fuzzyMatch(query, text) 要求 query 的字符按顺序出现;连续匹配和词边界降低 score,缺口和靠后位置增加 score,完整相等再减 100。packages/tui/src/fuzzy.ts:1-93
它还对“字母+数字”与“数字+字母”两种 query 顺序尝试互换,成功的替代匹配加 5 分。packages/tui/src/fuzzy.ts:75-92
fuzzyFilter() 以空白和 / 拆成多个 token,要求每个 token 都匹配,并按总分由小到大排序。packages/tui/src/fuzzy.ts:95-137
Editor.requestAutocomplete() 每次请求前会取消旧 debounce timer 和 AbortController,再生成新的 start token。packages/tui/src/components/editor.ts:2165-2194 packages/tui/src/components/editor.ts:2322-2341
普通 @/附件上下文会 debounce 20ms,显式 Tab 或 force 请求不延迟。packages/tui/src/components/editor.ts:243-245 packages/tui/src/components/editor.ts:2232-2240
请求按前一个 promise 串行化;开始时记录 request ID、文本、行和列快照。packages/tui/src/components/editor.ts:2196-2217
请求完成后,只有 signal 未 abort、request ID 仍相等、文本与光标仍等于快照才接受结果,避免旧异步结果覆盖新输入。packages/tui/src/components/editor.ts:2242-2308
force + 显式 Tab 且唯一候选时,编辑器直接应用补全;其他候选创建 SelectList 并选中精确匹配或第一个前缀匹配项。packages/tui/src/components/editor.ts:2271-2291 packages/tui/src/components/editor.ts:2102-2138
4. undo-stack.ts、kill-ring.ts 与 word-navigation.ts:三个小模块约束编辑语义¶
UndoStack<S>.push(state) 对状态做 structuredClone() 后再入栈,pop() 返回已经脱离当前状态的 snapshot,clear() 清空数组。packages/tui/src/undo-stack.ts:1-28
Editor 把 { state, pastes, pasteCounter } 放进这个通用栈,所以回滚涵盖文本和大粘贴的外部存储。packages/tui/src/components/editor.ts:215-220 packages/tui/src/components/editor.ts:2012-2028
KillRing.push(text, { prepend, accumulate? }) 忽略空串;连续 kill 时与最后一项拼接,向后删除 append、向前删除 prepend。packages/tui/src/kill-ring.ts:11-28
peek() 不改变 ring,rotate() 在至少两项时把最后一项移到开头。packages/tui/src/kill-ring.ts:30-45
由于 peek() 取数组末尾,rotate() 后“下一项”会成为新的末尾,正好支持 editor 的 yankPop()。packages/tui/src/kill-ring.ts:30-45 packages/tui/src/components/editor.ts:1905-1926
findWordBackward(text, cursor, options?) 先跳过尾部空白,再按 atomic segment、word-like segment 或标点 run 决定停点。packages/tui/src/word-navigation.ts:16-70
word-like segment 内部若含 ASCII 标点,会停在最后一个标点后的边界,保留代码式词语的标点导航。packages/tui/src/word-navigation.ts:47-56
findWordForward() 是镜像操作:先跳过前导空白,再处理 atomic、word-like 或标点 run。packages/tui/src/word-navigation.ts:72-117
它接受可注入 segment 与 isAtomicSegment,使默认 Editor 能把 paste marker 当成一整个 word-navigation 单元。packages/tui/src/word-navigation.ts:5-14 packages/tui/src/components/editor.ts:1869-1889
5. components/ 清单:每个可复用块负责什么¶
Text¶
Text 显示多行文本,先把 tab 变成三空格、使用 wrapTextWithAnsi() 折行,再添加 padding、可选背景和缓存。packages/tui/src/components/text.ts:4-105
setText()、setCustomBgFn() 与 invalidate() 都会清空缓存,因此它适合内容频繁更新但宽度不变时避免重复布局。packages/tui/src/components/text.ts:13-43 packages/tui/src/components/text.ts:45-105
TruncatedText¶
TruncatedText 只显示输入的第一逻辑行,减去 padding 后用 truncateToWidth() 截断,并把最终行补到完整 viewport 宽度。packages/tui/src/components/truncated-text.ts:4-64
它适合不能换行的状态栏、标题或行内摘要,而不是正文段落。packages/tui/src/components/truncated-text.ts:33-56
Spacer¶
Spacer(lines = 1) 的 render() 返回指定数量的空字符串,setLines() 可以在运行时改变垂直间距。packages/tui/src/components/spacer.ts:3-27
它不关心 width,也没有渲染缓存。packages/tui/src/components/spacer.ts:17-27
Box¶
Box 自己维护 children,向 child 传入扣除左右 padding 的宽度,再把 child 行加上左 padding、上下空白和可选背景。packages/tui/src/components/box.ts:11-27 packages/tui/src/components/box.ts:74-135
它以 width、child 行数组和对 bgFn("test") 的采样共同判断缓存是否命中,因此动态背景函数的输出改变也能失效缓存。packages/tui/src/components/box.ts:4-9 packages/tui/src/components/box.ts:56-65 packages/tui/src/components/box.ts:95-124
Input¶
Input 是单行版编辑器,保存 value 和单个 cursor,也实现 Focusable。packages/tui/src/components/input.ts:11-37
它支持 bracketed paste、全局键绑定、grapheme 删除、word navigation、kill/yank、undo 以及 Kitty 可打印 CSI-u 解码。packages/tui/src/components/input.ts:48-372
渲染时它以 visibleWidth() 决定横向 scroll window,以 sliceByColumn() 截取,并使用 inverse video 与 CURSOR_MARKER 画光标。packages/tui/src/components/input.ts:378-446
Editor¶
Editor 是上述多行版本;它额外拥有 logical/visual line 映射、30% 高度 viewport、历史、异步补全和大粘贴 marker registry。packages/tui/src/components/editor.ts:270-343 packages/tui/src/components/editor.ts:482-600
它是交互式 prompt 输入最重要的组件,因为它把终端按键转换成可提交的多行文本和补全 UI。packages/tui/src/components/editor.ts:603-890 packages/tui/src/components/editor.ts:1260-1274
SelectList¶
SelectList 保存原 items、前缀过滤后的 items、选中下标和最大可见数,并提供 select、cancel、selection-change 回调。packages/tui/src/components/select-list.ts:12-58
它用 startsWith 过滤、以选项中心计算窗口、支持选中项上下循环,并在 Enter/Escape 时调用回调。packages/tui/src/components/select-list.ts:60-137
宽终端会渲染主列与描述两列,窄终端只保留主列;所有主文本和描述都经 truncateToWidth() 限宽。packages/tui/src/components/select-list.ts:139-216
SettingsList¶
SettingsList 显示 { id, label, currentValue },可选 values 用于循环切换,可选 submenu 工厂用于进入子组件。packages/tui/src/components/settings-list.ts:7-32 packages/tui/src/components/settings-list.ts:199-230
启用搜索时它创建内部 Input,将输入交给该 Input,再以 fuzzyFilter() 过滤 setting label。packages/tui/src/components/settings-list.ts:49-67 packages/tui/src/components/settings-list.ts:168-197 packages/tui/src/components/settings-list.ts:232-235
active submenu 期间 render 和输入都委派给 submenu;done callback 可选地更新值、通知 onChange 并恢复原选中项。packages/tui/src/components/settings-list.ts:81-88 packages/tui/src/components/settings-list.ts:168-174 packages/tui/src/components/settings-list.ts:199-230
Markdown¶
Markdown 用一个配置了 StrictStrikethroughTokenizer 的 Marked 实例 lexer 文本,并修剪流式输出中不完整的闭合 code fence,避免代码块在最后几个 fence 字符到来时收缩闪烁。packages/tui/src/components/markdown.ts:6-53
它的 render 缓存键是 text 和 width;空文本返回空数组,非空文本依次 token 渲染、ANSI 保留折行、加 padding/背景并缓存。packages/tui/src/components/markdown.ts:110-242
renderToken() 覆盖标题、段落、代码块、列表、表格、引用、分隔线、HTML 和空白 token。packages/tui/src/components/markdown.ts:327-490
代码块可走主题提供的 highlightCode(),否则逐行用 codeBlock();链接在终端能力支持时生成 OSC 8,否则把 URL 以括号文本回退显示。packages/tui/src/components/markdown.ts:378-397 packages/tui/src/components/markdown.ts:537-557
表格先计算自然宽度和最小词宽,再在可用宽度内按比例分配并对单元格折行;过窄时退回原 Markdown 文本。packages/tui/src/components/markdown.ts:671-857
Loader¶
Loader 继承 Text,默认帧是十个 braille 字符,默认每 80ms 更新一次。packages/tui/src/components/loader.ts:1-26
start() 更新显示并重启 interval,interval 每帧变更后调用 TUI.requestRender();stop() 清理定时器。packages/tui/src/components/loader.ts:43-91
自定义 indicator 若显式传入,其帧会原样渲染;否则 spinner frame 经过颜色函数,消息始终经过消息颜色函数。packages/tui/src/components/loader.ts:59-70 packages/tui/src/components/loader.ts:83-91
CancellableLoader¶
CancellableLoader 在 Loader 上增加一个 AbortController、只读 signal/aborted 和可选 onAbort。packages/tui/src/components/cancellable-loader.ts:4-27
它收到 tui.select.cancel 时 abort signal 并调用回调,dispose() 只停止 spinner interval。packages/tui/src/components/cancellable-loader.ts:29-40
Image¶
Image 接收 base64、MIME、fallback theme 和尺寸限制;没有显式 dimensions 时先解析图片头,失败就使用 800×600 默认值。packages/tui/src/components/image.ts:12-48
渲染时它按当前 cell dimensions 和 width 计算最大尺寸,并缓存生成行。packages/tui/src/components/image.ts:55-68 packages/tui/src/components/image.ts:121-125
Kitty 模式为未分配的图片生成 ID,并返回第一行图形序列加剩余空行;iTerm2 则在最后一行移回去画图以维持 TUI 光标核算。packages/tui/src/components/image.ts:70-111
终端没有图片能力或 renderImage() 返回 null 时,组件用 imageFallback() 文本并经 fallbackColor 渲染。packages/tui/src/components/image.ts:112-119
6. 深入组件一:SelectList 为什么适合补全菜单¶
SelectList 的条目使用稳定的 value、显示用 label 和可选 description,这正好对应 AutocompleteItem 的三个字段。packages/tui/src/components/select-list.ts:12-16 packages/tui/src/autocomplete.ts:219-223
Editor.createAutocompleteList() 把 provider 返回的 item、autocompleteMaxVisible、编辑器主题内的 selectList theme 交给 SelectList。packages/tui/src/components/editor.ts:2132-2138
组件自己管理 selected index,所以上下移动无需修改编辑器文本或重发 provider 请求。packages/tui/src/components/select-list.ts:40-58 packages/tui/src/components/select-list.ts:112-137
鼠标不在此协议中;键盘的 up/down/confirm/cancel 都经共享 KeybindingsManager,因此用户改键后补全菜单与其他选择器同步。packages/tui/src/components/select-list.ts:112-137 packages/tui/src/keybindings.ts:7-42 packages/tui/src/keybindings.ts:194-200
菜单输出会让选中项尽量居中,超出 maxVisible 时补一个 (current/total) 滚动信息行。packages/tui/src/components/select-list.ts:74-109
补全候选可很长,renderItem() 会先测 prefix 和主列,宽度大于 40 时才给 description 留足至少 10 列的空间。packages/tui/src/components/select-list.ts:139-176
Editor 只在补全激活时将该列表渲染在文本编辑区之后,因此选择 UI 不改变文本的 logical lines。packages/tui/src/components/editor.ts:590-600 packages/tui/src/components/editor.ts:664-723
7. 深入组件二:Markdown 如何在终端保留样式与宽度¶
Markdown.render(width) 先把 source tabs 统一为三空格,再对 source 做 lexer,并逐 token 调用 renderToken()。packages/tui/src/components/markdown.ts:151-187
生成的非图片行会经过 wrapTextWithAnsi(),所以 ANSI 样式与 OSC 8 链接能跨物理行恢复,而图片行不会被文字折行器破坏。packages/tui/src/components/markdown.ts:189-199 packages/tui/src/utils.ts:704-819
最终 padding 时如有背景会调用 applyBackgroundToLine();否则依据 visibleWidth() 补空格到确切 width。packages/tui/src/components/markdown.ts:201-241
inline 渲染在 bold、italic、code、link、strikethrough 后附回当前 style prefix,因此内层 theme reset 不会意外抹掉外层标题或默认样式。packages/tui/src/components/markdown.ts:492-588
引用块不是简单在原文本前加符号;它先以减去 │ 的宽度递归渲染 block token,再折行并补 quote border。packages/tui/src/components/markdown.ts:414-460
这使 Markdown 是一个真正的文本布局组件,而不是把 Markdown 转为字符串后直接 console.log。packages/tui/src/components/markdown.ts:151-242 packages/tui/src/components/markdown.ts:327-490
数据流¶
下面追踪用户在第一个逻辑行键入 /he,Tab 选择命令后继续输入正文的完整路径。packages/tui/src/components/editor.ts:1123-1154 packages/tui/src/components/editor.ts:2144-2163
stdin 的完整 key sequence
-> TUI 将输入交给 focused Editor.handleInput(data)
-> decodePrintableKey() / 原始可打印字符
-> Editor.insertCharacter()
-> isAtStartOfMessage() 触发 requestAutocomplete()
-> provider.getSuggestions(lines, cursorLine, cursorCol, { signal })
-> CombinedAutocompleteProvider 用 fuzzyFilter() 产生 items + prefix
-> Editor.createAutocompleteList() -> SelectList
-> TUI.requestRender() -> Editor.render() 把菜单附到编辑器下方
-> Tab/Enter -> provider.applyCompletion()
-> 新 lines/cursor 写回 Editor state,触发 onChange
TUI 只把输入给焦点组件并在 handleInput() 返回后请求重绘,因此 Editor 是这条链中解释按键的边界。packages/tui/src/tui.ts:829-838
Editor.handleInput() 对可打印键调用 decodePrintableKey(),并在无法解码时接收普通可打印数据。packages/tui/src/components/editor.ts:875-890
insertCharacter() 改写当前逻辑行和 cursor,再视 slash 起点或 trigger context 启动/更新补全。packages/tui/src/components/editor.ts:1095-1154
请求传入 AbortSignal,编辑器在每次新请求前撤销旧请求,并用文本/光标快照拒绝过时返回。packages/tui/src/components/editor.ts:2165-2194 packages/tui/src/components/editor.ts:2242-2308
默认 provider 在 / 前缀时把命令转为候选,再让 fuzzyFilter() 过滤 name。packages/tui/src/autocomplete.ts:308-359 packages/tui/src/fuzzy.ts:95-137
编辑器收到候选后创建 SelectList,并在本帧的 render() 将其行追加到编辑器底部。packages/tui/src/components/editor.ts:2310-2320 packages/tui/src/components/editor.ts:590-600
Tab 或确认后,applyCompletion() 返回替换后的 lines 与光标;编辑器保存它们、取消菜单、调用 onChange() 或对 slash completion 继续走提交逻辑。packages/tui/src/components/editor.ts:676-723 packages/tui/src/autocomplete.ts:375-460
自测¶
-
一个扩展编辑器若想取代默认
Editor,哪些成员必须实现,哪些能力可以缺省?packages/tui/src/editor-component.ts:11-74 -
为什么大粘贴显示为
[paste #N ...]仍能在提交时得到原始全文?packages/tui/src/components/editor.ts:1156-1222packages/tui/src/components/editor.ts:981-1000packages/tui/src/components/editor.ts:1260-1274 -
如何保证一个慢的异步补全请求不会把新文本上的菜单覆盖掉?
packages/tui/src/components/editor.ts:2165-2217packages/tui/src/components/editor.ts:2242-2308 -
SelectList的上/下键、确认和取消为何会自动跟随用户的 keybinding 配置?packages/tui/src/components/select-list.ts:112-137packages/tui/src/keybindings.ts:194-200 -
Markdown在折行、链接和背景填充时分别怎样避免 ANSI 样式、OSC 8 与宽字符造成布局错误?packages/tui/src/components/markdown.ts:189-241packages/tui/src/components/markdown.ts:537-557packages/tui/src/utils.ts:213-270packages/tui/src/utils.ts:704-819