v1.1.0 Pi v1.1.0 源码分析 Analysis of Pi v1.1.0 · 2026-10-07
tui tui
差分渲染的终端 UI 库 Terminal User Interface library with differential rendering for efficient text-based applications
pi-tui
← → 翻页 · F 全屏 · O 目录 · T 明暗 · L 语言 ← → navigate · F fullscreen · O overview · T theme · L language
01 · 组件模型 01 · The component model pi-tui
组件模型 The component model The component model: render(width) returns lines 例:交互模式的组件树 e.g. the interactive-mode tree 每一帧的约定 The contract for every frame 内置组件 Built-in components 18 个,按用途 18, by purpose «interface» Component + render(width): string[] + handleInput?(data): void + handleMouse?(event) + wantsKeyRelease?: boolean + invalidate(): void «interface» Focusable + focused: boolean «class» Container + children: Component[] + addChild() · removeChild() · clear() + render(w): concat(child.render(w)) «interface» TUI extends Component + mode: "regular" | "fullscreen" + setFocus(c) · showOverlay(c, opts) + requestRender(force?) · renderNow() + … 25 members in total TuiAltScreen 根 = Container root = Container Text 标题与提示 title & hints ScrollView 消息列表(Markdown…) messages (Markdown…) Container 扩展 widgets extension widgets Editor focused = true Text 页脚:模型 · token footer: model · tokens box stack h-stack v-stack spacer scroll-view mouse-region text truncated-text markdown image input editor select-list settings-list loader cancellable-loader alt-screen-flash render(width) 自上而下,各子组件的行拼在一起 render(width) top-down; child lines are concatenated 每行可见宽度 ≤ width Every line's visible width ≤ width 否则抛错,并写 pi-tui-crash.log otherwise it throws and writes pi-tui-crash.log focused 组件在光标处输出 CURSOR_MARKER The focused component emits CURSOR_MARKER "\x1b_pi:c\x07":零宽 APC 序列 "\x1b_pi:c\x07": a zero-width APC sequence TUI 找到标记并删掉,再移动硬件光标 TUI finds and strips it, then moves the hardware cursor 输入法候选框因此跟着光标走 so IME candidate windows follow the cursor 每行末尾追加 SEGMENT_RESET Each line gets SEGMENT_RESET appended \x1b[0m\x1b]8;;\x07:样式和超链接不会串行 \x1b[0m\x1b]8;;\x07: no style or link bleeds to the next line invalidate():主题变化时清缓存 invalidate(): drop caches on theme change 下一次 render 从头计算 the next render starts from scratch 布局 layout 内容 content 输入 input 反馈 feedback 1 2 3 4 5 接口 / 组件 interfaces / components 获得焦点 focused 约定 contract
每一行的可见宽度不能超过 width;超过时 TUI 抛错并写 pi-tui-crash.log,而不是让终端自动换行弄乱画面。 No line may be wider than width; if one is, the TUI throws and writes pi-tui-crash.log instead of letting the terminal wrap and corrupt the screen.
02 · 差分渲染 02 · Differential rendering pi-tui
差分渲染 Differential rendering 宽度变化、高度变化(Termux 除外)、内容变短(开启 clearOnShrink 时)以及变化发生在视口上方时,会清屏重绘——终端滚动区里已经滚出去的行无法原地修改。 A width change, a height change (except on Termux), shrinking content (with clearOnShrink) or a change above the viewport triggers a full redraw — lines already scrolled into the terminal's history cannot be edited in place. 只删了行时不重写任何内容,只清掉尾部多出来的行。 If lines were only deleted, nothing is rewritten; the extra tail lines are just cleared.
Differential rendering: what follows requestRender() 调度 Scheduling 合并请求 · 每帧至少间隔 16ms coalesced · at least 16 ms between frames 准备一帧(两种屏幕共用) Preparing a frame (both screens) TuiMainScreen.doRender() regular 模式:在终端滚动区里原地更新 regular mode: update in place in the scrollback requestRender() 已请求就直接返回 no-op if already pending nextTick 同一 tick 内合并 coalesce this tick scheduleRender() setTimeout(16ms − 距上一帧) setTimeout(16 ms − since last) doRender() 子类实现: per subclass: main / alt screen handleInput(data) requestImmediateRender() nextTick 直接渲染,取消等待中的定时器 render on nextTick, cancel any pending timer render(width) 组件树 → 行 tree → lines compositeOverlays 覆盖层拼进行内 splice overlays in extractCursorPosition 找到并删掉标记 find & strip marker applyLineResets 行尾 SEGMENT_RESET SEGMENT_RESET per line 宽度检查 width check 超宽 → 抛错 too wide → throw 首帧? first frame? previousLines = [] 全量输出 full render fullRender(false) 宽度变了? width changed? previousWidth 清屏重绘 clear + redraw fullRender(true) 高度变了? height changed? Termux 除外 not on Termux 清屏重绘 clear + redraw fullRender(true) 内容变短? shrank? clearOnShrink 清屏重绘 clear + redraw fullRender(true) 没有变化? nothing changed? 逐行比较 compare lines 只移光标 move cursor only 只删了行? only deletions? first ≥ new.length 清掉尾部行 clear tail lines 在视口上方? above viewport? first < viewportTop 清屏重绘 clear + redraw fullRender(true) 否则:局部重写 else: partial write firstChanged … lastChanged \x1b[?2026h … ?2026l 定时器到期 timer fires 按键延迟敏感 keys are latency-sensitive 比较前把变化区间扩大到覆盖整张 Kitty 图片;所有输出都包在同步输出 ?2026h … ?2026l 里,避免闪烁。 The changed range is widened to cover whole Kitty images; every write is wrapped in synchronized output ?2026h … ?2026l to avoid flicker. fullRender(true) = 清屏 + 清滚动区(\x1b[2J\x1b[H\x1b[3J)后输出全部行;TUI.fullRedraws 计数。 fullRender(true) = clear screen and scrollback (\x1b[2J\x1b[H\x1b[3J), then write every line; counted in TUI.fullRedraws. 否 → 下一个判断 no → next check 是 → 全量重绘 yes → full redraw 是 → 轻量处理 yes → light path 03 · 两种屏幕:regular 与 fullscreen 03 · Two screens: regular and fullscreen pi-tui
两种屏幕:regular 与 fullscreen Two screens: regular and fullscreen Two screens: regular (main) and fullscreen (alternate) TuiMainScreen mode = "regular" TuiAltScreen mode = "fullscreen" · 进入 \x1b[?1049h mode = "fullscreen" · enters \x1b[?1049h renderLayoutFrame(root) 布局帧:盒子 · 滚动视图 layout frame: boxes · scroll views applySearchHighlights 搜索命中高亮 search hits compositeScrollToEndIndicator "回到底部"提示 "jump to end" pill compositeOverlays 覆盖层 overlays applySelection 鼠标选区 mouse selection compositeFlashes 短暂提示 transient flashes extractCursorPosition · applyLineResets 光标 · 行尾重置 · 裁到 width cursor · resets · clip to width 与 previousScreen 逐行比较 diff rows against previousScreen 尺寸变化 → 整屏重绘 size change → full redraw \x1b[row;1H\x1b[2K … 只写变化的行 · 包在 ?2026 中 changed rows only · inside ?2026 内容写进终端自己的滚动区 Content goes into the terminal's own scrollback 往上翻就是历史,退出后仍在 scroll up for history; it stays after exit 按行差分(见上一张图) Line diff (see the previous figure) 只重写 firstChanged…lastChanged rewrites only firstChanged…lastChanged 不抓鼠标 No mouse capture 终端原生选择与复制照常 native selection and copy still work 视口之上的变化只能清屏重绘 Changes above the viewport force a full redraw rows/cols 变化同理 so do rows / cols changes Kitty 图片按行预留 Kitty images reserve rows 变化区间会扩到整张图 diff ranges widen to whole images 选择 CHOOSING # pi-coding-agent: settings.json "tuiMode": "regular" | "fullscreen" # 默认 fullscreen(getTuiMode) # default: fullscreen (getTuiMode) 鼠标 mouse ?1000 ?1002 ?1003 ?1004 ?1006 滚轮 wheel 加速,最多 6 行/次 accelerated, ≤ 6 lines 选择复制 select & copy OSC 52 搜索 search alt-screen-search.ts 布局 layout HStack · VStack · ScrollView 退出 exit \x1b[?1049l 1 2 3 4 5 regular fullscreen 输出 output
备用屏一帧依次经过:布局帧 → 搜索高亮 → 回到底部提示 → 覆盖层 → 选区 → 短暂提示 → 光标与行尾重置,最后只写变化的行。 An alternate-screen frame goes through: layout frame → search highlights → jump-to-end pill → overlays → selection → flashes → cursor and line resets, then only changed rows are written.
04 · 输入与按键 04 · Input and keys pi-tui
输入与按键 Input and keys Input: from stdin bytes to focused.handleInput() TuiBase.handleTerminalInput(data) 依次尝试,可被消费 in order; may be consumed keys.ts 把序列认成按键名 turn sequences into key names KeybindingsManager 47 个 tui.* 按键 id 47 tui.* key ids process.stdin 原始字节 raw bytes ProcessTerminal setRawMode · ?2004h 括号粘贴 setRawMode · ?2004h bracketed paste Kitty 键盘协议 flags = 7 Kitty keyboard protocol, flags = 7 StdinBuffer 拆成完整转义序列 · 粘贴整段保留 split into whole sequences · paste kept whole ESC 10ms(SSH 100ms)· 序列 50ms ESC 10 ms (SSH 100 ms) · sequence 50 ms 终端颜色回复 terminal colour reply OSC 10 / 11 / 4 配色方案通知 colour-scheme report 深/浅色切换 dark / light switch addInputListener() consume 或改写 data consume or rewrite data 单元格尺寸回复 cell-size reply 图片用 for images shift+ctrl+d 调试回调 onDebug onDebug hook 覆盖层焦点修复 overlay focus repair visible() 变化后 after visible() changes 过滤按键释放 drop key releases 除非 wantsKeyRelease unless wantsKeyRelease focused.handleInput(data) 然后立即渲染 then render immediately 序列举例 SAMPLE SEQUENCES \x1b[A up \x1b[13;2u shift+enter \x1b[99;5:3u ctrl+c release \x1b[200~ … \x1b[201~ paste \x1b]11;rgb:… bg colour reply Kitty 查询 \x1b[>7u\x1b[?u 无回应时, If the Kitty query \x1b[>7u\x1b[?u gets no reply 在 DA 回复后改用 modifyOtherKeys \x1b[>4;2m before DA, fall back to modifyOtherKeys \x1b[>4;2m matchesKey(data, "ctrl+c") Kitty CSI-u · 旧式 · modifyOtherKeys Kitty CSI-u · legacy · modifyOtherKeys parseKey(data) → "shift+enter" 等 → "shift+enter" etc. isKeyRelease(data) Kitty 事件类型 3 Kitty event type 3 decodeKittyPrintable(data) CSI-u → 可打印字符 CSI-u → printable text tui.input.* 4 tui.select.* 6 tui.editor.* 23 tui.altScreen.* 14 input.submit enter input.newLine shift+enter / ctrl+j input.copy ctrl+c editor.undo ctrl+- editor.yank ctrl+y 用户覆盖:~/.pi/agent/keybindings.json Overrides: ~/.pi/agent/keybindings.json 1 2 3 4 5 6 7 8 终端层 terminal layer TUI 分发 TUI dispatch 组件 · 按键 component · keys
单独的 ESC 要等 10ms 才能确定是 Escape 键而不是序列开头;SSH 下默认 100ms,可用 PI_TUI_ESC_TIMEOUT 调整。 A lone ESC needs 10 ms to be sure it is the Escape key rather than the start of a sequence; 100 ms over SSH, adjustable with PI_TUI_ESC_TIMEOUT.
05 · 编辑器与自动补全 05 · The editor and autocomplete pi-tui
编辑器与自动补全 The editor and autocomplete The Editor and autocomplete 细节 Details Editor.handleInput(data) 自上而下尝试,命中即返回 tried top to bottom; first match returns 自动补全 Autocomplete CombinedAutocompleteProvider «class» implements Component, Focusable Editor − state: { lines[], cursorLine, cursorCol } − undoStack: UndoStack<EditorState> − killRing: KillRing − history: string[] (≤ 100) − pastes: Map<id, text> + onSubmit?(text) · onChange?(text) + getText() · setText() · insertTextAtCursor() + setAutocompleteProvider(p) + addToHistory(text) 跳转模式 jump mode jumpForward 括号粘贴 bracketed paste \x1b[200~ 交给上层 to parent ctrl+c 撤销 undo ctrl+- 补全菜单 completion menu up · down · tab · enter Tab 触发 Tab trigger force: true 删除 delete 字符 · 单词 · 到行首尾 char · word · line kill ring ctrl+y · alt+y 历史 history up · down 光标移动 cursor 行首尾 · 按词 line · word 换行 new line shift+enter 提交 submit enter → onSubmit 其余:插入字符 else: insert text decodeKittyPrintable / 行首的斜杠命令 slash command at line start fuzzyFilter(commands) @ 文件附件 file attachment fd 搜索 · 防抖 20ms fd search · 20 ms debounce Tab 路径补全 path completion force: true · 读目录 force: true · readdir SelectList 弹出在编辑器上方 · ↑↓ 选择 pops above the editor · ↑↓ to pick applyCompletion(…) → { lines, cursorLine, cursorCol } 大段粘贴折叠成标记 Large pastes collapse to a marker [paste #1 +42 lines] 阈值 threshold > 10 行或 > 1000 字符 > 10 lines or > 1000 chars 提交时 on submit 标记换回原文 markers expand back 行尾 \ + Enter trailing \ + Enter 插入换行而不是提交 inserts a newline instead 撤销 undo UndoStack 保存状态快照 UndoStack keeps state snapshots 历史 history 最多 100 条 up to 100 entries kill ring 按行/按词删除的文本,连续删除合并 line / word kills; consecutive ones merge 确认 confirm 提供者接口:triggerCharacters? · getSuggestions(lines, line, col, { signal, force }) · applyCompletion(…) Provider interface: triggerCharacters? · getSuggestions(lines, line, col, { signal, force }) · applyCompletion(…) 1 2 3 4 5 6 7 8 9 10 11 12 13 编辑器状态 / 提交 editor state / submit 按键分支 key branches 补全 completion
超过 10 行或 1000 字符的粘贴显示为 [paste #1 +42 lines] 这样的标记,提交时再换回原文——编辑器本身保持轻量。 Pastes over 10 lines or 1000 characters show as markers like [paste #1 +42 lines] and expand back on submit — the editor itself stays light.
06 · 覆盖层 06 · Overlays pi-tui
覆盖层 Overlays Overlays: showOverlay(component, options) 9 个锚点 9 anchors 终端可用区域(去掉 margin) terminal area minus margin 每一帧 Every frame compositeOverlays() 焦点 Focus 覆盖层是一个栈 overlays form a stack «interface» OverlayOptions + width?: number | "50%" + minWidth?: number + maxHeight?: number | "%" + anchor?: 9 values + offsetX? · offsetY? + row? · col?: number | "%" + margin?: number | { top … } + visible?(w, h): boolean + nonCapturing?: boolean «returned» OverlayHandle + hide() + setHidden() + isHidden() + focus() + unfocus() + isFocused() + getBounds() 筛选可见的 filter visible visible(w, h) · 未隐藏 visible(w, h) · not hidden 解析宽度 resolve width % / minWidth / margin component.render(width) 截到 maxHeight cut to maxHeight 解析 row / col resolve row / col 用实际高度算锚点 anchor with real height compositeLineAt(…) 按列拼进底层行 splice into base lines showOverlay() 记住 preFocus,然后抢焦点 remember preFocus, take focus nonCapturing: true 显示但不抢焦点 shown, focus untouched hide() 焦点交给下一个可见覆盖层或 preFocus focus to next visible overlay or preFocus top-left top-center top-right left-center center right-center bottom-left bottom-center bottom-right 默认:center · 宽 min(80, 可用宽度) default: center · width min(80, available) 每次输入前都会复查:focused 覆盖层若已不可见(resize 或 visible() 变 false),焦点改到最上层可见覆盖层或 preFocus。 Before every input: if the focused overlay is no longer visible (resize, or visible() turned false), focus moves to the top visible overlay or preFocus. 覆盖层按 focusOrder 排序叠放;focus() 把一个覆盖层提到最上层。 Overlays stack by focusOrder; focus() brings one to the top. 1 2 3 4 5 API 定位 placement 合成 compositing 焦点 focus
默认居中,宽度为 min(80, 可用宽度);width / maxHeight / row / col 都接受数字或 "50%" 这样的百分比。 Default: centred, width min(80, available); width / maxHeight / row / col accept a number or a percentage such as "50%".
07 · 终端能力 07 · Terminal capabilities pi-tui
终端能力 Terminal capabilities Terminal capabilities: negotiation, output sequences, images ProcessTerminal.start() Terminal 接口 13 个成员 Terminal interface: 13 members 输出与查询 Output & queries detectCapabilities() 按环境变量识别;PI_IMAGE_PROTOCOL · PI_TRUE_COLOR · PI_HYPERLINKS 可覆盖 from env vars; PI_IMAGE_PROTOCOL · PI_TRUE_COLOR · PI_HYPERLINKS override setRawMode(true) 逐字节读取,不回显 byte-wise input, no echo \x1b[?2004h 开启括号粘贴 bracketed paste on \x1b[>7u \x1b[?u 请求 Kitty 键盘协议并查询 push Kitty flags 7 and query \x1b[c DA:用作"查询结束"哨兵 DA as the end-of-queries sentinel \x1b[>4;2m 无 Kitty 回复时的 modifyOtherKeys modifyOtherKeys when Kitty is silent OSC 0 OSC 9;4 OSC 7501 OSC 10/11 OSC 8 ?2026 kitty iterm2 Windows:额外打开 VT 输入;stop() 恢复原 raw 状态并关掉以上模式。 Windows: also enables VT input; stop() restores raw state and undoes the modes. \x1b]0;title\x07 窗口标题 window title \x1b]9;4;3 / 0 任务栏进度(不确定 / 清除) taskbar progress (busy / clear) idle · working · blocked … 程序状态(终端声明支持时) program status (if supported) 前景 / 背景色查询 fg / bg colour query 自动判断深浅主题 picks dark or light 超链接 hyperlinks 只在确认支持时启用 only where known to work 同步输出 synchronized output 每帧包起来,防闪烁 wraps every frame 终端 TERMINAL 图片协议 IMAGES 真彩色 TRUECOLOR 超链接 HYPERLINKS kitty · ghostty · wezterm · warp ✓ ✓ iTerm2 ✓ ✓ Windows Terminal · alacritty · vscode · zed — ✓ ✓ JetBrains · Windows console — ✓ — tmux — COLORTERM 探测 probed screen · 未知终端 screen · unknown — COLORTERM — 1 2 3 4 5 启动协商 start-up negotiation 输出序列 output sequences 能力识别 capability detection
图片:kitty、ghostty、wezterm、warp 用 Kitty 图形协议,iTerm2 用自己的协议;tmux、screen 与其他终端不显示图片。 Images: kitty, ghostty, wezterm and warp use the Kitty graphics protocol, iTerm2 its own; tmux, screen and other terminals show no images.
08 · 模块结构 08 · Module structure pi-tui
模块结构 Module structure pi-tui modules (19,409 lines of TypeScript) 终端与输入 Terminal & input 渲染核心 Render core 编辑 Editing 文本与图形 Text & graphics components/:开箱即用的组件 components/: ready-made components 18 个文件 18 files 609 terminal.ts ProcessTerminal · 协议协商 ProcessTerminal · negotiation 444 stdin-buffer.ts 字节流 → 完整序列 bytes → whole sequences 1,401 keys.ts parseKey · matchesKey 320 keybindings.ts 可配置的按键 id configurable key ids 65 native-platform.ts 剪贴板 · 修饰键(可选) clipboard · modifiers (opt.) 53 program-status.ts OSC 7501 程序状态 OSC 7501 program status 1,493 tui.ts Component · TuiBase · 覆盖层 Component · TuiBase · overlays 655 tui-main-screen.ts regular:行级差分 regular: line diff 1,784 tui-alt-screen.ts fullscreen:备用屏 fullscreen: alt screen 449 layout.ts 布局帧 · 滚动条 layout frame · scrollbars 327 alt-screen-search.ts 全屏内搜索 in-screen search 82 wheel-scroll.ts 滚轮加速 wheel acceleration 2,472 editor.ts 多行编辑器 multi-line editor 861 autocomplete.ts / 命令 · @ 文件 · 路径 / commands · @ files · paths 28 undo-stack.ts 快照式撤销 snapshot undo 46 kill-ring.ts Emacs 式 kill/yank Emacs kill / yank 138 fuzzy.ts 模糊匹配打分 fuzzy scoring 117 word-navigation.ts 按词移动 word motion 1,397 utils.ts visibleWidth · 截断 · 换行 visibleWidth · truncate · wrap 1,025 markdown.ts Markdown → ANSI 1,506 latex.ts LaTeX → Unicode 763 terminal-image.ts Kitty / iTerm2 图片 Kitty / iTerm2 images 367 colors.ts 颜色转换 colour conversion 91 terminal-colors.ts 前景/背景色探测 fg / bg detection alt-screen-flash 51 box 172 cancellable-loader 40 editor 2,472 h-stack 44 image 167 input 494 loader 101 markdown 1,025 mouse-region 33 scroll-view 224 select-list 273 settings-list 328 spacer 28 stack 154 text 113 truncated-text 65 v-stack 33 标签 = 行数 tag = lines of code 终端 terminal 渲染 render 编辑 editing 文本 · 图形 text · graphics
最大的文件是 components/editor.ts:多行编辑、撤销、kill ring、历史、粘贴折叠与补全菜单都在这里。 The largest file is components/editor.ts: multi-line editing, undo, the kill ring, history, paste folding and the completion menu all live there.
扩展点 Extension points pi-coding-agent
tui 的扩展点 Where to plug into tui 1
Component Component
实现 render(width) 即可成为组件;可选 handleInput / handleMouse。 Implement render(width) to be a component; handleInput / handleMouse are optional.
2
AutocompleteProvider AutocompleteProvider
替换或组合补全来源。 Replace or combine completion sources.
3
KeybindingsManager KeybindingsManager
覆盖默认键位,或增加自己的按键 id。 Override default bindings or add your own key ids.
4
showOverlay(component, options) showOverlay(component, options)
对话框、选择器、提示浮层。 Dialogs, pickers, pop-ups.
5
addInputListener() addInputListener()
在组件之前拦截或改写输入。 Intercept or rewrite input before components see it.