Files
ttui/TODO.md
T
chuan 48fa673daf feat: improve overlay composition
- preserve base content around overlay regions

- add ANSI-aware column slicing for overlay segments

- align overlay rows with bottom viewport
2026-06-04 01:55:13 +08:00

18 KiB
Raw Blame History

TODO

当前定位

TinyTUI 现在已经具备最小可运行的 C# TUI 框架骨架:终端输入输出、输入缓冲、按键解析、运行时、组件树、差分渲染、overlay、基础组件和示例都已经成型

后续 TODO 不再按单个控件的小功能罗列,而是对齐 tmp/tui 的完整框架能力,优先补齐会影响整个 TUI 可用性、稳定性、可扩展性和可验证性的部分

框架级缺口

1. 终端会话能力

目标:把当前 console 封装升级成完整的终端会话抽象,负责启动、停止、协议协商和状态恢复

  • 管理 bracketed paste、光标显示隐藏、清屏、窗口标题、进度指示等终端模式
  • 启动时保存原始终端状态,停止时稳定恢复,避免 raw mode、paste mode、键盘协议泄漏到父 shell
  • 支持 Kitty keyboard protocol 探测和启用,失败时 fallback 到 modifyOtherKeys
  • Windows 下处理 Virtual Terminal Input,保证 Shift+Tab、组合方向键等输入不会丢失修饰键
  • 停止前 drain stdin,避免慢 SSH 或终端延迟导致的按键释放事件泄漏
  • 提供可替换的 Terminal 接口,为真实终端和测试终端使用同一套运行时

参考:tmp/tui/src/terminal.ts

本次推进:

  • 新增 ITerminalSession,把输入、输出、尺寸、启动停止和终端控制序列收敛为一个可替换的会话抽象
  • 新增 ConsoleTerminalSession,统一管理 bracketed paste、光标显示隐藏、窗口标题恢复、OSC 9;4 进度指示、Windows Virtual Terminal Input、退出前输入 drain 和停止时模式恢复
  • TuiRuntime 改为依赖终端会话,保留旧 ITerminalInput 构造函数用于兼容现有调用方
  • Example 改为用同一个 terminal session 同时提供 renderer 输出和 runtime 生命周期

为什么先做:

  • tmp/tui/src/terminal.ts 的能力不是单个输出方法,而是完整生命周期对象;先把 C# 侧入口统一起来,后续 Kitty protocol、modifyOtherKeys、虚拟终端测试和渲染管线才能接在同一个抽象上
  • 当前实现把 Runtime 从“直接控制 input 生命周期”推进到“只协调组件和会话”,终端状态恢复责任更清晰

当前更好的点:

  • C# 侧接口直接继承 ITerminalOutputRenderer 和 Runtime 可以共享同一个 terminal session,避免真实终端和测试终端各自维护两套输出入口
  • 保留旧构造函数降低迁移成本,现有使用 ITerminalInput 的代码不会立刻断裂

后续仍需补齐:

  • Kitty keyboard protocol 目前只预留 KittyProtocolActive 和停止恢复序列,还没有实现查询、响应解析、启用和 fallback 到 modifyOtherKeys
  • Windows VT input 已用 P/Invoke 处理 console mode,但还需要结合真实终端验证 Shift+Tab、Ctrl+方向键、Alt 组合键在不同宿主中的表现
  • DrainInput 目前是基于 Console.KeyAvailable 的同步排空,后续要和 stdin buffer / keyboard protocol release 事件联动,避免吞掉应由应用处理的 late input
  • 标题恢复当前只在 Windows 通过 Console.Title 尝试保存;Unix 终端通常无法可靠读取原始标题,后续可以增加可选 title stack 或由调用方提供恢复标题
  • ITerminalSession 还缺键盘协议协商事件和测试 fake terminal,实现虚拟终端回归测试前需要继续抽象可观测输出和模拟输入

2. 输入协议和 Keybinding 系统

目标:把按键解析从“组件里判断具体 key name”升级为“协议解析 + 动作映射”的框架能力

  • 完整处理 Kitty keyboard protocol 的 press、repeat、release 和 alternate key 信息
  • 支持 key release 过滤,并允许组件声明是否接收 release 事件
  • 建立统一 KeyId 表达和 MatchesKey 能力,避免组件分散处理 escape sequence
  • 建立全局 keybinding registry,按动作名绑定默认快捷键,并允许外部覆盖
  • 提供 keybinding 冲突检测,避免多个动作抢同一个快捷键
  • 输入 listener 支持全局拦截和 consume,用于 debug、退出、快捷命令等框架级输入

参考:tmp/tui/src/keys.tstmp/tui/src/keybindings.ts

本次推进:

  • 新增 KeyId,把组件使用的按键名称收敛为可比较、可匹配的标准按键标识,并兼容 pageUp/pageDown 与当前 page-up/page-down 命名差异
  • 新增 KeybindingDefinitionKeybindingConflictKeybindingRegistryTuiKeybindings,建立动作名到默认快捷键的注册表、用户覆盖入口和冲突检测
  • InputSelectListEditor 改为通过 keybinding 动作处理快捷键,不再在组件内直接散落判断具体 key name
  • 组件构造函数支持注入 KeybindingRegistry,外部可以先在组件级覆盖默认快捷键,后续再接到运行时级全局配置

为什么先做:

  • tmp/tui/src/keybindings.ts 的关键价值是把“按键是什么”和“动作是什么”拆开;先补 registry 后,自动补全、选择列表、编辑器和全局快捷命令可以复用同一个动作系统
  • C# 侧目前解析器已经能产出标准 key name,本次先不扩大到 Kitty 完整协议,避免同时重写输入解析和组件行为造成不可控回归
  • 保持 IInputComponent.HandleInput(TuiInputEvent) 不变,只在组件内部使用 action matching,兼容现有 Runtime 和 Example

当前更好的点:

  • C# 侧 KeybindingRegistry.SetUserBinding 会校验动作名,调用方写错动作会立即抛错,而不是静默忽略
  • 默认动作常量集中在 TuiKeybindings,组件调用处能直接看出“行为意图”,比继续比较 ctrl+... 字符串更容易维护
  • 单行输入、选择列表和多行编辑器已经区分 tui.input.*tui.select.*tui.editor.*,后续做用户配置时不会因为复用 Enter 语义互相污染

后续仍需补齐:

  • KeyId 目前仍基于已解析的 key name 匹配,还没有像 tmp/tui/src/keys.ts 那样直接识别 raw escape sequence、Kitty CSI-u、modifyOtherKeys 和 alternate key
  • 还没有解析 Kitty keyboard protocol 的 press、repeat、release 事件类型,也没有为组件提供是否接收 release 事件的声明能力
  • KeybindingRegistry 目前只支持组件级注入,尚未接入 Runtime 统一持有,也没有全局 input listener、拦截、consume 和 debug 快捷键入口
  • 冲突检测当前对用户覆盖生效,后续可以增加“默认绑定冲突是否允许按上下文共存”的策略,比如 editor/select 都使用 Enter 但不会同时处理
  • DefaultInputParser 还需要继续补 parseKey 级能力:空格、Alt+字母、Ctrl+符号、Shift+Tab、Alt+方向键、Windows Ctrl+Backspace 等特殊兼容路径

3. 渲染管线和视口模型

目标:把当前按行差分刷新升级成能长期运行、低闪烁、能处理滚动区域和复杂内容的渲染管线

  • 使用 synchronized output 包裹每次渲染,降低闪烁和中间态显示
  • 维护 viewport top、工作区高度、硬件光标位置和上一帧尺寸,避免内容增长或缩短时滚动错位
  • 区分首次渲染、宽度变化、视口外变化、普通差分更新、内容缩短清理等策略
  • 每行输出前统一追加 SGR reset 和 OSC 8 reset,避免 ANSI 样式或超链接泄漏到后续行
  • 在渲染层校验每行可见宽度不超过终端宽度,错误时提供可定位的调试信息
  • 增加可选写出日志和重绘调试日志,方便定位真实终端中的闪烁、残影和错位问题

参考:tmp/tui/src/tui.ts

本次推进:

  • 新增 RenderFrameBuilderRenderedFrameRenderViewportStateRenderPipelineOptions,把组件输出统一转换成逻辑帧、可见视口帧和可调试的渲染状态
  • DifferentialRenderer 改为维护上一帧尺寸、viewport top、working height 和内容高水位,能区分首次渲染、宽度变化、高度变化、viewport 变化、内容缩短和普通差分更新
  • 每次实际写终端时使用 synchronized output 包裹,并且每行统一追加 SGR reset 和 OSC 8 reset,降低闪烁和样式/超链接泄漏风险
  • 渲染层先校验可见宽度,默认继续按当前文本模型截断作为兼容防线,也支持通过 ThrowOnWidthOverflow 让组件宽度溢出直接暴露
  • 增加 TINYTUI_DEBUG_REDRAWTINYTUI_DEBUG_RENDER 对应的重绘/差分日志,默认写到临时目录下的 tinytui
  • FullScreenRenderer 也复用同一套帧构建逻辑,避免全量渲染和差分渲染在截断、cursor marker、行尾 reset 上行为不一致

为什么先做:

  • tmp/tui/src/tui.ts 的渲染稳定性核心不是单个 escape sequence,而是“渲染前归一化 + viewport 状态 + full redraw 策略 + 差分写入”组合;先把 C# 侧帧模型抽出来,后续 overlay、文本工具和图像能力都能接入同一条管线
  • 当前组件仍可能输出超过终端宽度的内容,本次保留截断兼容,同时提供溢出日志和可选异常,避免一次性把所有组件改成严格模式造成使用方回归
  • 先采用 bottom viewport 模型,只渲染终端可见的最后 Rows 行,能避免内容高度超过终端时继续用绝对行号写到不可见区域

当前更好的点:

  • C# 侧把渲染帧构建抽成内部类型,DifferentialRendererFullScreenRenderer 共享同一套规则,后续做测试 fake output 时更容易只验证帧输出和状态
  • RenderPipelineOptions 是显式对象,不完全依赖环境变量,测试和调用方可以按实例控制 synchronized output、行 reset、缩短清理和溢出策略
  • ViewportState 暴露最近一次渲染状态,Example 或后续测试可以直接检查 viewport top、working height 和 logical line count

后续仍需补齐:

  • 当前 viewport 变化直接触发全量重绘,还没有像 tmp/tui/src/tui.ts 那样在部分追加行场景里精确滚动并更新 hardware cursor row
  • 宽度校验仍依赖现有 TerminalTextMeasurer,还没有 grapheme cluster、Kitty image line、OSC/APC 完整识别和按列严格切片能力
  • 内容缩短清理现在对所有 shrink 默认 full redraw,后续可以增加 overlay-aware 策略,避免 overlay 需要 padding 时误清理工作区
  • 调试日志目前记录帧和变化行,但还没有写出最终 ANSI buffer,也没有像参考实现那样在崩溃前恢复终端会话状态
  • 硬件光标位置目前仍以绝对 MoveCursorTo 到可见行处理,尚未维护 hardwareCursorRow 与相对移动模型,IME 定位在复杂滚动场景下还需要真实终端验证

4. Overlay 合成正确性

目标:让 overlay 成为菜单、弹窗、自动补全、设置面板等上层交互的稳定基础

  • 合成时保留 overlay 右侧的底层内容,而不是简单截断整行
  • 处理 ANSI、OSC 8 超链接、APC、Kitty image sequence 等不可见序列
  • 使用按列切片和宽字符边界保护,避免中文、emoji、regional indicator 在 overlay 边界被切坏
  • overlay 前后插入样式 reset,避免底层样式污染 overlay 或 overlay 样式污染底层
  • 完善多层 overlay 的焦点恢复策略,处理 overlay 被临时隐藏、释放焦点、被其他组件短暂抢焦点后的恢复
  • 支持 overlay 显示区域与底部视口对齐,避免在内容超过终端高度时覆盖位置偏移

参考:tmp/tui/src/tui.tstmp/tui/test/tui-overlay-style-leak.test.tstmp/tui/test/overlay-short-content.test.ts

本次推进:

  • 新增 TextSliceITextMeasurer.Slice,让文本工具可以按终端可见列切出片段并返回实际宽度
  • TerminalTextMeasurer.Slice 使用单次扫描处理 ANSI、OSC、APC、DCS 等不可见序列,并在 strict 模式下跳过压到边界的双宽字符
  • OverlayManager.ComposeLine 改为拼接底层左侧、overlay 区域和底层右侧,不再因为 overlay 覆盖一段内容就丢弃整行右侧内容
  • overlay 段前后插入 SGR reset 和 OSC 8 reset,降低底层样式污染 overlay、overlay 样式污染右侧底层内容的风险
  • overlay 行坐标改为映射到底部 viewport 的逻辑行,内容超过终端高度时 overlay 仍按当前屏幕可见区域定位

为什么先做:

  • tmp/tui/src/tui.tscompositeLineAt 首先解决的是 overlay 短内容覆盖长底层行时的残留和截断问题;C# 侧原实现明确丢弃了右侧底层内容,这是菜单、补全列表和浮层提示最容易出现的视觉回归
  • 按列切片必须下沉到文本工具,否则 overlay、渲染管线和后续组件会继续各自用字符串长度或局部截断逻辑处理 ANSI 内容
  • 本次只引入轻量 TextSlice,不直接引入完整 grapheme / style tracker,避免把 TODO 第 5 项文本模型重构提前扩大

当前更好的点:

  • C# 侧把按列切片挂到 ITextMeasurer,后续组件可以先复用同一入口,不需要等完整文本模型落地
  • overlay 合成现在和第 3 项的 bottom viewport 模型对齐,短内容和超高内容都使用同一套屏幕坐标逻辑
  • reset 隔离在 overlay 合成层执行,即使 overlay 组件输出缺少结尾 reset,也能降低样式泄漏到右侧底层内容和后续行的概率

后续仍需补齐:

  • TerminalTextMeasurer.Slice 仍按 Rune 处理,不是 tmp/tui/src/utils.ts 那样的 grapheme cluster 切片;ZWJ emoji、肤色、regional indicator 等复杂组合还可能被切开
  • overlay 右侧底层内容现在会被 reset 隔离,但还没有像参考实现的 extractSegments 那样追踪并恢复切片前的活动 SGR 样式,因此右侧内容的继承样式可能丢失
  • 当前没有识别 Kitty image lineoverlay 合成还不会像参考实现一样跳过图片行或管理已显示图片 id
  • ITextMeasurer 的默认 Slice 是兼容 fallback,遇到带 ANSI 的非 TerminalTextMeasurer 实现时不如专用实现可靠,后续文本模型稳定后应收敛为显式能力
  • 还缺 C# 虚拟终端测试,无法自动覆盖 overlay 短内容保留右侧、样式泄漏、宽字符边界和底部 viewport 对齐这些回归

5. 文本模型和 ANSI 感知工具

目标:把文本宽度、截断、切片、换行作为框架底座,而不是散落在组件里的局部逻辑

  • 用 grapheme cluster 计算宽度,覆盖 ZWJ emoji、肤色、variation selector、regional indicator
  • 提供 ANSI / OSC 感知的 VisibleWidthTruncateToWidthSliceByColumnWrapTextWithAnsi
  • 支持 tab 宽度配置
  • 截断和换行时保持 ANSI 样式闭合,并在换行后恢复必要样式
  • 抽象 terminal output normalization,统一清理和补齐行尾 reset
  • 所有组件渲染统一依赖这套文本工具,减少宽度计算不一致导致的渲染 bug

参考:tmp/tui/src/utils.ts

6. 组件基础设施

目标:补齐组件体系的横向能力,而不是继续只补单个组件的小功能

  • 给组件接口增加 Invalidate 语义,用于主题变化、缓存失效和强制重绘
  • 建立 Focusable 模型,由运行时统一设置焦点状态,组件只负责在渲染中输出 cursor marker
  • 支持硬件光标定位配置,兼容 IME 候选窗定位
  • 引入主题接口和默认主题,让组件样式可配置且跨组件一致
  • 增加通用组件:SpacerTruncatedTextSettingsListCancellableLoader
  • 组件渲染缓存统一由宽度、内容和主题状态驱动,避免每帧重复解析 Markdown 或重算复杂布局

参考:tmp/tui/src/components/*

7. 自动补全和命令交互基础

目标:把自动补全从 Editor 局部功能升级为可复用的交互能力

  • 定义 autocomplete provider 接口,支持同步和异步候选
  • 支持 slash command 候选、文件路径候选、特殊前缀候选
  • 使用 overlay 呈现补全列表,并复用 SelectList 的选择、过滤和滚动能力
  • 将补全确认、取消、预览、应用文本变更抽象成独立流程
  • 让 keybinding 系统负责 Tab、Enter、Escape 等动作触发,避免补全逻辑和具体按键强绑定

参考:tmp/tui/src/autocomplete.tstmp/tui/src/components/editor.ts

8. 图像和特殊终端内容

目标:为 Kitty / iTerm2 图像和特殊终端序列预留完整框架入口

  • 检测终端图像能力,选择 Kitty graphics protocol、iTerm2 inline image 或文本 fallback
  • 解析图片尺寸并按终端 cell 尺寸计算显示区域
  • 渲染差分时追踪已显示的 Kitty image id,在内容变化或清屏时删除旧图像
  • 合成和截断逻辑识别 image line,避免把图像序列当普通文本切坏
  • 在终端 resize 或 cell size 变化后刷新图像布局

参考:tmp/tui/src/terminal-image.tstmp/tui/src/components/image.ts

9. 测试基础设施

目标:引入可以验证真实终端行为的测试层,而不是只靠 Example 手动看效果

  • 建立虚拟终端或 fake terminal output,捕获 ANSI 输出并模拟窗口尺寸
  • 覆盖输入缓冲、按键协议、keybinding、文本宽度、ANSI 换行和截断
  • 覆盖差分渲染、内容缩短清理、resize full redraw、viewport 覆盖和硬件光标定位
  • 覆盖 overlay 样式泄漏、短内容覆盖长内容、宽字符边界和多层焦点恢复
  • 覆盖 Markdown、SelectList、Editor、Autocomplete 等组件的框架行为
  • 示例仍用于人工验收,但不能替代回归测试

参考:tmp/tui/test/virtual-terminal.tstmp/tui/test/*.test.ts

10. 工程化和发布完整性

目标:让 TinyTUI 从本地骨架变成可维护、可发布、可集成的类库

  • 完善 README,覆盖快速开始、核心 API、组件接口、overlay、keybinding、文本工具和自定义组件约束
  • 补 XML documentation 和包元数据,为 NuGet 发布做准备
  • 增加 CIrestore、build、format、test、pack
  • 增加 benchmark 或压力示例,用于观察大文本、频繁刷新、overlay 合成和 Markdown 缓存性能
  • 拆分 Example 为多个真实场景:基础输入、overlay 菜单、聊天界面、Markdown 浏览、设置面板、图像 fallback
  • 明确公开 API 和内部实现边界,避免后续重构时破坏使用者代码

建议执行顺序

  1. 先补终端会话能力、输入协议和 keybinding,因为它们会影响所有交互组件
  2. 再升级文本模型、渲染管线和 overlay 合成正确性,因为它们决定整体显示稳定性
  3. 然后补组件基础设施、自动补全和主题,让上层组件可以复用统一能力
  4. 接着引入虚拟终端测试和关键回归用例,把已修过的问题固化下来
  5. 最后处理图像、benchmark、README、CI、NuGet 等发布和扩展能力

暂缓

  • 不继续列举 Editor、SelectList、Markdown 的零散小功能
  • 不优先追求所有组件与 tmp/tui 逐项一致
  • 图像能力可以等终端、渲染、文本和测试基础稳定后再做
  • NuGet 发布可以等公开 API 基本稳定后再做