Files
ttui/TODO.md
T
chuan c435df9fcd
ci / restore build format test pack (push) Failing after 22s
test: converge smoke checks into xunit suite
- migrate remaining text and component smoke assertions into TinyTUI.Tests

- remove legacy TextChecks and ComponentChecks projects from solution

- update README test guidance to use the unified xUnit suite
2026-06-04 10:15:57 +08:00

798 lines
77 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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# 侧接口直接继承 `ITerminalOutput`Renderer 和 Runtime 可以共享同一个 terminal session,避免真实终端和测试终端各自维护两套输出入口
- 保留旧构造函数降低迁移成本,现有使用 `ITerminalInput` 的代码不会立刻断裂
后续仍需补齐:
- Kitty keyboard protocol 已有启动侧查询、响应解析和 fallback,但还需要结合真实 Kitty、WezTerm、iTerm2、Windows Terminal、tmux 和 SSH 延迟场景验证协商时序
- 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,实现虚拟终端回归测试前需要继续抽象可观测输出和模拟输入
本次 Kitty keyboard protocol 协商推进:
- `ConsoleTerminalSession.Start()` 现在会在 bracketed paste 和隐藏光标后写出 `ESC[>7u ESC[?u ESC[c`,优先请求 Kitty keyboard protocol 的 disambiguate、event type 和 alternate key flags
- 会话输入事件会在启动协商窗口内先拦截 `ESC[?Nu` Kitty flags 响应和 `ESC[?...c` DA 响应;非零 Kitty flags 会设置 `KittyProtocolActive`DA 响应或 `ESC[?0u` 会 fallback 到 xterm `modifyOtherKeys`
- 未收到确认时会在短超时后写出 `ESC[>4;2m` 启用 `modifyOtherKeys`,并用内部 `ModifyOtherKeysActive` 状态避免重复启用
- `Stop()` 会先取消协商 timer 并按实际状态恢复 Kitty / modifyOtherKeys,再排空输入、关闭进度、关闭 bracketed paste、恢复 Windows VT input、标题和光标
- 新增 `ConsoleTerminalSessionTests`,用 fake input 和 fake output 覆盖启动写序列、Kitty flags 成功、DA fallback、超时 fallback、停止恢复和分片响应缓存
为什么这样做:
- 对齐 `tmp/tui/src/terminal.ts` 的渐进增强策略:先 push 所需 Kitty flags,再查询 flags,同时用 DA 和短 timeout 兜底不支持 Kitty 的终端
- 协商逻辑放在 `ConsoleTerminalSession` 而不是 input parser,是为了让协议响应不会被应用组件当作普通输入,同时保持 `DefaultInputParser` 只负责真实按键序列解析
- Stop 先恢复键盘协议再 drain,是为了减少 Kitty key release 事件在慢 SSH 或终端延迟下继续生成新协议序列并泄漏到父 shell 的风险
当前更好的点:
- C# 侧通过构造函数注入 fallback timeout,测试可以稳定覆盖超时路径,不需要真实等待默认 150ms
- `ModifyOtherKeysActive` 仅 internal 暴露给测试,公开 `ITerminalSession` API 没有因为实现状态膨胀而增加额外属性
- 分片 Kitty flags 响应会被短暂缓存,非协商输入会恢复成原始合并序列继续转发,降低协商窗口误吞输入的概率
本次后续仍需补齐:
- C# 侧还没有像参考实现那样对 late response buffer 单独设置 fragment flush timer;当前只在协商窗口内缓存完整前缀,真实终端若在 fallback 后慢速拆分 DA 响应仍需要继续验证
- `DrainInput` 仍基于 `Console.KeyAvailable`,没有暂时替换 input handler 统计 late data,因此对异步 `ConsoleTerminalInput` 缓冲里的 release 事件处理还不如参考实现精细
- 还没有把 Kitty protocol active 状态同步给 parser 或 key 模块的全局状态;当前 parser 已能解析协议序列,但没有会话级开关参与解析策略
- 测试目前断言写出的 ANSI 序列和状态,没有完整虚拟终端 emulator 断言父 shell 恢复后的最终屏幕和键盘模式
### 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.ts``tmp/tui/src/keybindings.ts`
本次推进:
- 新增 `KeyId`,把组件使用的按键名称收敛为可比较、可匹配的标准按键标识,并兼容 `pageUp/pageDown` 与当前 `page-up/page-down` 命名差异
- 新增 `KeybindingDefinition``KeybindingConflict``KeybindingRegistry``TuiKeybindings`,建立动作名到默认快捷键的注册表、用户覆盖入口和冲突检测
- `Input``SelectList``Editor` 改为通过 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 keyboard protocol 已有基础 press、repeat、release 和 alternate key 解析,但还没有为组件提供是否接收 release 事件的声明能力
- `KeybindingRegistry` 目前只支持组件级注入,尚未接入 Runtime 统一持有,也没有全局 input listener、拦截、consume 和 debug 快捷键入口
- 冲突检测当前对用户覆盖生效,后续可以增加“默认绑定冲突是否允许按上下文共存”的策略,比如 editor/select 都使用 Enter 但不会同时处理
- `DefaultInputParser` 还需要继续补 `parseKey` 级能力:空格、Ctrl+符号、Alt+方向键、Windows Ctrl+Backspace 等特殊兼容路径
本次 Runtime 级推进:
- `TuiRuntime` 改为统一持有 `KeybindingRegistry`,构造函数支持传入外部 registry,并通过 `ITuiRuntime.Keybindings` 暴露给运行时级输入监听器
- 新增 `TuiInputListener``TuiInputListenerContext`,全局 listener 可以读取当前输入、Runtime 和共享 keybinding registry
- `ITuiRuntime.AddInputListener` 支持按注册顺序添加全局输入监听器,返回 `IDisposable` 句柄用于注销
- `TuiRuntime.Dispatch` 改为先执行全局 listener,任一 listener 返回 consumed 后立即停止后续 listener,并阻止输入继续派发到焦点组件
- consumed 和未 consumed 的输入都会在 Dispatch 末尾走统一 `RequestRender`,保证全局快捷键修改状态后可以触发刷新
- 新增 xUnit 回归测试,覆盖 listener 顺序、consume 阻止焦点组件收到输入、不 consume 继续派发、注销句柄、listener 使用 Runtime registry 匹配动作
为什么这样做:
- 对齐 `tmp/tui/src/tui.ts``addInputListener` 思路,把 debug、退出、快捷命令等框架级输入入口放在 Runtime,而不是继续分散到组件
- C# 侧保留 `IInputComponent.HandleInput(TuiInputEvent)` 不变,只在 Runtime 派发前增加拦截层,避免组件体系和已有 Example 大面积重构
- listener 派发前复制快照,允许监听器在回调中注册或注销其他监听器,本轮输入顺序仍保持稳定
当前更好的点:
- Runtime 级 registry 是构造注入的实例属性,测试和应用可以显式传入自定义快捷键配置,不依赖全局静态状态
- 注册 API 返回 `IDisposable`,适合 C# 的 `using` 生命周期,也比单独暴露 remove 方法更不容易忘记注销
- `TuiInputListenerContext.Keybindings` 是便捷属性,listener 写全局快捷键时不需要知道 Runtime 内部字段
后续仍需补齐:
- 还没有补 `removeInputListener(listener)` 形式的 API;当前以 `IDisposable` 句柄为主,若需要和参考实现更一致可再增加等价注销方法
- listener 当前是同步 bool 契约,尚未支持异步命令、取消令牌、异常隔离和按优先级/作用域管理监听器
- 输入协议已有 Kitty keyboard protocol 的基础 press、repeat、release、alternate key 和 release 过滤,还没有组件声明能力或完整 raw matching
- 仍缺 Runtime + virtual terminal 的端到端屏幕断言,本次只覆盖输入派发路径和渲染请求计数
本次 Runtime 共享注入推进:
- 新增 `IKeybindingComponent`,让内置输入组件可以在进入 Runtime 后接收共享 `KeybindingRegistry`
- `Input``SelectList``Editor``CancellableLoader` 改为实现该接口,保留构造函数显式传入 registry 的兼容性
- `TuiRuntime.Add``ShowOverlay` 会在组件进入根树或 overlay 栈前注入 `Runtime.Keybindings`
- Runtime 注入会递归处理内置 `Container.Children``Box.Child`,避免容器里的输入组件继续使用各自默认 registry
- `Editor.SetKeybindings` 会同步更新内部 `AutocompleteList`,确保补全候选列表导航也走 Runtime 共享配置
- 新增 xUnit 回归测试,覆盖根输入组件、overlay 列表、容器子组件和 Editor 内部补全列表使用 Runtime 自定义快捷键
为什么这样做:
- 对比 `tmp/tui/src/keybindings.ts``getKeybindings()` 全局 managerC# 侧已有 Runtime 实例级 `Keybindings`,显式注入更适合测试隔离和多 Runtime 场景
- 注入点放在 `Add``ShowOverlay`,是因为这是组件真正进入运行时输入派发范围的边界,能让组件离线构造时仍保持原有默认行为
- 递归只覆盖当前内置容器类型,不引入反射扫描,减少第三方组件属性命名或生命周期被 Runtime 误触碰的风险
当前更好的点:
- 自定义 keybinding 现在能真实影响焦点组件行为,而不只是 Runtime listener 可见
- C# 侧可以给每个 Runtime 持有独立 registry,比参考实现的模块级全局 keybindings 更容易做并发测试和多终端实例
- Editor 内部补全列表与编辑器共享同一 registry,避免外层动作覆盖生效但内层选择列表仍用默认键的割裂行为
后续仍需补齐:
- Runtime 只知道内置 `Container``Box` 的子组件结构,第三方复合组件若要递归注入仍需要自己实现 `IKeybindingComponent` 并转发给子组件
- `Container.Add` 在容器已经挂到 Runtime 之后再动态添加子组件时,目前不会自动触发 Runtime 注入;后续可以引入组件挂载上下文或 Runtime-aware container
- 已补 `tmp/tui/src/keys.ts` 里的 Kitty press、repeat、release、alternate key 基础解析,仍缺组件声明是否接收 release 事件和完整 raw matching
- keybinding 冲突检测仍主要针对用户覆盖集合,后续需要补作用域或上下文策略,避免全局配置复杂后误报或漏报
本次动态 Container 注入推进:
- `Container` 新增 `ChildAdded``ChildRemoved` 事件,让 Runtime 可以感知已挂载容器后续动态增删的子组件
- `TuiRuntime` 新增 Runtime 组件挂载上下文,组件进入根树或 overlay 时会递归挂载并注入共享 `KeybindingRegistry`
- 已挂载 `Container.Add(input)` 会立即为新输入组件注入 Runtime 级快捷键,并触发子组件失效和重渲染请求
- 已挂载 `Container.Add(new Box(input))` 会递归穿透内置 `Box.Child`,避免动态加入内置复合组件子树时漏掉输入组件
- 动态 `Remove``Clear` 会递归卸载子树并解除容器事件订阅,避免已移除容器继续响应 Runtime 注入
- overlay 中已经挂载的 `Container` 也复用同一套挂载上下文,动态加入子组件后可以使用 Runtime 共享快捷键
- 新增 xUnit 回归测试,覆盖动态根容器输入、动态 Box 子树和动态 overlay 容器子树的 keybinding 注入行为
为什么这样做:
- 对比 `tmp/tui/src/tui.ts`,参考实现的 `TUI` 自身继承 `Container`,动态添加后的渲染天然由根容器覆盖;C# 侧 Runtime 是组合式根树,因此需要显式挂载上下文把 Runtime 级能力传播到后续动态子树
- hook 放在 `Container` 层而不是每个输入组件层,是因为问题发生在复合组件子树边界;Runtime 只要监听容器增删,就能复用现有 `IKeybindingComponent` 注入契约
- 挂载计数用于处理同一组件子树同时出现在根树和 overlay 的情况,避免一次移除就提前解除仍然挂载路径上的动态监听
当前更好的点:
- C# 侧 Runtime keybinding 是实例级注入,多 Runtime 或测试场景可以各自持有独立 registry,不依赖参考实现那类模块级全局状态
- 动态子树注入同时支持根树和 overlay 栈,普通应用组件和浮层容器不需要走两套生命周期逻辑
- `ChildRemoved` 路径会解除订阅并失效子树,比只做 `ChildAdded` 注入更不容易在长生命周期应用里留下过期 Runtime 回调
本次后续仍需补齐:
- Runtime 递归仍只理解内置 `Container``Box`,第三方复合组件的内部子树还需要实现自己的 `IKeybindingComponent` 转发或后续新增通用子组件枚举接口
- `Box` 当前是固定单子组件结构,没有动态更换 `Child` 的 API;如果后续增加可变 Child,需要同样触发 Runtime 挂载上下文更新
- 动态加入新输入组件后 Runtime 不会自动切焦点,本次保持现有显式 `SetFocus` 语义,后续可以按组件声明或 overlay 策略决定是否自动聚焦
- 组件生命周期目前只覆盖 keybinding 注入和 invalidate,还没有像完整框架那样统一管理主题、终端能力、异步任务取消和组件 dispose
本次协议解析推进:
- 新增 `TuiKeyEventType`,让 `TuiInputEvent` 可以承载 Press、Repeat、Release 三种按键阶段,并保留现有两参数构造默认 Press 的兼容行为
- `TuiInputEvent` 增加 `AlternateKey``RawSequence` 元数据,用于承载 Kitty CSI-u alternate key 和原始协议序列,现有组件仍继续读取 `Value`
- `DefaultInputParser` 增加 Kitty CSI-u 基础解析,覆盖普通 codepoint、Ctrl/Alt/Shift/Super modifier、event type、shifted alternate key、base layout key 和 Kitty keypad functional codepoint 归一化
- `DefaultInputParser` 增加带 event type 的 Kitty final-byte 功能键解析,并保留无 event type 的 legacy modified CSI 行为,避免 `ESC[1;2A` 这类旧序列因为新增元数据影响现有相等断言和组件行为
- `DefaultInputParser` 增加 xterm modifyOtherKeys `CSI 27 ; modifier ; codepoint ~` 解析,覆盖 Ctrl+字母、Shift+Tab、Alt+Enter 等组合键
- 补齐 `ESC[Z` Shift+Tab 和 legacy Alt+字母解析,减少组件或测试直接识别 raw escape sequence 的需求
- release 事件默认在 parser 层不派发,同时 `KeybindingRegistry.Matches` 对手工构造的 release 事件也默认不匹配,避免释放阶段误触发组件动作
- 新增 xUnit 回归测试,覆盖 Shift+Tab、Alt+字母、Kitty 普通字符、Ctrl/Shift 组合、repeat 阶段、alternate key 元数据、modifyOtherKeys 组合键和 release 过滤
本次当前更好的点:
- C# 侧把按键阶段作为 `TuiInputEvent` 的强类型字段,不依赖类似 `isKeyRelease(data)` 的全局最近解析状态,Runtime listener 和组件拿到事件后可以直接判断来源阶段
- release 过滤同时放在 parser 和 keybinding registry 两层,真实终端输入不会派发 release,测试或外部代码手工构造 release 事件时也不会误匹配默认动作
- 协议解析后仍输出原有 `Value` key name,组件和 keybinding registry 不需要立刻迁移到 raw sequence 匹配模型
- Kitty final-byte 解析只接管带 `:event` 的新协议形态,旧 xterm modified CSI 仍走原逻辑,避免为了新增元数据破坏已有 record 相等和 legacy 行为
本次后续仍需补齐:
- 还没有实现组件声明是否接收 release 事件的 API;当前策略是框架默认过滤,后续若需要 key-up 交互要给 Runtime listener 或组件增加 opt-in 通道
- `AlternateKey` 目前只承载 shifted alternate keybase layout key 用于解析匹配但没有单独暴露字段;后续如果要完整支持非 Latin 布局调试和用户配置,应扩展为结构化元数据
- modifyOtherKeys 目前覆盖基础 `CSI 27 ; modifier ; codepoint ~`,还没有处理所有参考实现中的 printable duplicate drop、Windows Ctrl+Backspace heuristic、legacy Ctrl+符号兼容路径
- parser 仍不是完整 `tmp/tui/src/keys.ts``matchesKey` raw sequence 模型,`KeyId` 还不能直接匹配 raw Kitty/modifyOtherKeys 序列,只是依赖解析后的 `Value`
- 终端协议协商仍属于 TODO 1,没有在本轮启用 Kitty keyboard protocol 或 fallback 到 modifyOtherKeys,也没有处理退出时恢复协议状态
本次 release opt-in 推进:
- `DefaultInputParser` 不再在解析层丢弃 Kitty release 事件,`TuiInputEvent.EventType` 会保留 `Release` 让 Runtime 和全局 listener 有机会观察
- 新增 `IKeyReleaseComponent`,聚焦输入组件只有显式实现该接口时才接收 key release,普通 `IInputComponent` 默认继续过滤 release
- `TuiRuntime.Dispatch` 保持先派发全局 input listener,再判断是否进入焦点组件;因此 listener 可以记录或消费 release,组件侧不会默认触发 key-up 行为
- `KeybindingRegistry.Matches` 继续拒绝 `TuiKeyEventType.Release`,避免 release 触发快捷键动作
- 新增 xUnit 回归测试,覆盖 parser 保留 release、listener 可见 release、普通组件默认过滤、opt-in 组件接收和 keybinding 不匹配 release
为什么这样做:
- 对齐 `tmp/tui/src/tui.ts` 的边界:input listener 先看到输入,焦点组件派发前再按 `wantsKeyRelease` 决定是否过滤 release
- C# 侧用 marker interface 表达 opt-in,比参考实现的可选布尔属性更类型安全,组件是否接收 release 能从类型声明直接看出
- 过滤放在 Runtime 而不是 parser,可以让 debug listener、全局快捷层或测试基础设施看到完整协议事件,同时不改变普通组件的默认行为
当前 C# 版本更好的点:
- `TuiInputEvent.EventType` 是强类型字段,Runtime 不需要像 `tmp/tui/src/keys.ts` 一样重新解析原始字符串或依赖最近一次解析状态判断 release
- `IKeyReleaseComponent` 复用现有 `IInputComponent.HandleInput(TuiInputEvent)`,不需要引入新的回调形状,也不会影响已有组件构造和 keybinding 注入
- release 过滤和 keybinding 过滤是两层防线:组件默认不收到 release,手工构造的 release 事件也不会匹配动作
仍存在的问题和后续建议:
- `IKeyReleaseComponent` 目前是 marker interface,没有提供更细粒度的按键级 release opt-in;如果后续有拖拽、长按或组合键释放逻辑,可以扩展为属性或策略接口
- Runtime 仍是同步 listener 和同步组件派发,若 release 事件用于异步状态机,后续需要补异常隔离、取消令牌和 listener 作用域
- raw matching 仍未实现,`KeyId` 还不能直接匹配 Kitty 原始序列;本轮只保证 release 事件不被过早丢弃并按 opt-in 分发
### 3. 渲染管线和视口模型
目标:把当前按行差分刷新升级成能长期运行、低闪烁、能处理滚动区域和复杂内容的渲染管线
- 使用 synchronized output 包裹每次渲染,降低闪烁和中间态显示
- 维护 viewport top、工作区高度、硬件光标位置和上一帧尺寸,避免内容增长或缩短时滚动错位
- 区分首次渲染、宽度变化、视口外变化、普通差分更新、内容缩短清理等策略
- 每行输出前统一追加 SGR reset 和 OSC 8 reset,避免 ANSI 样式或超链接泄漏到后续行
- 在渲染层校验每行可见宽度不超过终端宽度,错误时提供可定位的调试信息
- 增加可选写出日志和重绘调试日志,方便定位真实终端中的闪烁、残影和错位问题
参考:`tmp/tui/src/tui.ts`
本次推进:
- 新增 `RenderFrameBuilder``RenderedFrame``RenderViewportState``RenderPipelineOptions`,把组件输出统一转换成逻辑帧、可见视口帧和可调试的渲染状态
- `DifferentialRenderer` 改为维护上一帧尺寸、viewport top、working height 和内容高水位,能区分首次渲染、宽度变化、高度变化、viewport 变化、内容缩短和普通差分更新
- 每次实际写终端时使用 synchronized output 包裹,并且每行统一追加 SGR reset 和 OSC 8 reset,降低闪烁和样式/超链接泄漏风险
- 渲染层先校验可见宽度,默认继续按当前文本模型截断作为兼容防线,也支持通过 `ThrowOnWidthOverflow` 让组件宽度溢出直接暴露
- 增加 `TINYTUI_DEBUG_REDRAW``TINYTUI_DEBUG_RENDER` 对应的重绘/差分日志,默认写到临时目录下的 `tinytui`
- `FullScreenRenderer` 也复用同一套帧构建逻辑,避免全量渲染和差分渲染在截断、cursor marker、行尾 reset 上行为不一致
为什么先做:
- `tmp/tui/src/tui.ts` 的渲染稳定性核心不是单个 escape sequence,而是“渲染前归一化 + viewport 状态 + full redraw 策略 + 差分写入”组合;先把 C# 侧帧模型抽出来,后续 overlay、文本工具和图像能力都能接入同一条管线
- 当前组件仍可能输出超过终端宽度的内容,本次保留截断兼容,同时提供溢出日志和可选异常,避免一次性把所有组件改成严格模式造成使用方回归
- 先采用 bottom viewport 模型,只渲染终端可见的最后 `Rows` 行,能避免内容高度超过终端时继续用绝对行号写到不可见区域
当前更好的点:
- C# 侧把渲染帧构建抽成内部类型,`DifferentialRenderer``FullScreenRenderer` 共享同一套规则,后续做测试 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.ts``tmp/tui/test/tui-overlay-style-leak.test.ts``tmp/tui/test/overlay-short-content.test.ts`
本次推进:
- 新增 `TextSlice``ITextMeasurer.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.ts``compositeLineAt` 首先解决的是 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 感知的 `VisibleWidth``TruncateToWidth``SliceByColumn``WrapTextWithAnsi`
- 支持 tab 宽度配置
- 截断和换行时保持 ANSI 样式闭合,并在换行后恢复必要样式
- 抽象 terminal output normalization,统一清理和补齐行尾 reset
- 所有组件渲染统一依赖这套文本工具,减少宽度计算不一致导致的渲染 bug
参考:`tmp/tui/src/utils.ts`
本次推进:
- `TerminalTextMeasurer` 从 Rune 扫描升级为 ANSI / OSC 感知的 grapheme cluster token 扫描,统一用于可见宽度、截断和按列切片
- 支持可配置 tab 宽度,默认按 `tmp/tui` 的 3 列处理,避免 tab 在切片、overlay 和渲染宽度中计算不一致
- 对 ZWJ emoji、variation selector、regional indicator 和常见 emoji 区间按终端双宽处理,降低流式输出中 emoji 中间态导致的差分渲染漂移
- 新增带 ellipsis 和 pad 参数的 ANSI 感知截断,截断时保留样式前缀,并在省略符前后插入 SGR reset,避免样式污染省略符和后续内容
- 新增 `ITextMeasurer.Wrap``TerminalTextMeasurer.Wrap`,提供 ANSI / OSC 8 感知换行,支持长词按 grapheme 切分、续行恢复活动 SGR 样式、行末临时关闭 underline 和 OSC 8 超链接
- 新增 `test/TinyTUI.TextChecks` 最小文本检查项目,覆盖 tab、regional indicator、ZWJ emoji、variation selector、ANSI 截断、宽省略符、ANSI 换行和 OSC 8 BEL 超链接续行
为什么先做:
- 第 3 项渲染管线和第 4 项 overlay 合成都已经依赖 `ITextMeasurer` 的宽度、截断和切片;先把文本底座升级后,可以减少组件、overlay、renderer 各自实现宽度规则导致的错位
- `tmp/tui/src/utils.ts` 的核心能力是同一套 grapheme/ANSI 工具支撑 `visibleWidth``truncateToWidth``sliceWithWidth``wrapTextWithAnsi`,本次选择在 C# 侧先收敛到 `TerminalTextMeasurer`,保持现有公开入口简单
- 换行先处理 ANSI SGR 与 OSC 8,是因为 Markdown、Editor、SelectList 后续都需要长链接、带颜色文本和 underline 内容稳定换行
当前更好的点:
- C# 侧把 `Wrap` 挂到 `ITextMeasurer`,调用方可以先依赖抽象逐步迁移组件,而不是直接绑定某个静态工具函数
- `TerminalTextMeasurer` 的 escape 识别覆盖 CSI、OSC、APC 和 DCS,切片与宽度计算共用同一次 token 化逻辑,比组件内手写字符串长度更稳定
- 换行时 OSC 8 会保留原始 BEL 或 ST 终止符,避免 OAuth 等 BEL 终止超链接在续行中被改写成另一种终止形式
- 新增检查项目只验证文本工具,不引入完整虚拟终端测试基础设施,避免提前扩大第 9 项范围
后续仍需补齐:
- C# 当前使用 `StringInfo.GetNextTextElementLength` 和 emoji 区间启发式,不等价于 `tmp/tui``Intl.Segmenter` + `RGI_Emoji` 精确判断,未来可引入更完整的 Unicode/RGI 数据或专门测试集
- `Wrap` 目前是 word wrap + 长词切分,还没有实现 `tmp/tui` 中完整的 punctuation/word segmenter 行为,CJK 断行、标点避头尾和复杂 Markdown token 换行仍需细化
- SGR tracker 已覆盖常见样式、标准色、256 色和 RGB 色,但还没有处理所有终端私有样式参数和嵌套 hyperlink 的边界测试
- 按列切片仍没有像 `extractSegments` 那样恢复切片前的活动样式,overlay 右侧继承样式丢失问题需要在后续文本 segment 模型中解决
- 还缺正式测试框架和虚拟终端断言;当前 `TinyTUI.TextChecks` 是最小可执行检查,后续第 9 项应把这些用例迁移到统一测试项目
### 6. 组件基础设施
目标:补齐组件体系的横向能力,而不是继续只补单个组件的小功能
- 给组件接口增加 `Invalidate` 语义,用于主题变化、缓存失效和强制重绘
- 建立 Focusable 模型,由运行时统一设置焦点状态,组件只负责在渲染中输出 cursor marker
- 支持硬件光标定位配置,兼容 IME 候选窗定位
- 引入主题接口和默认主题,让组件样式可配置且跨组件一致
- 增加通用组件:`Spacer``TruncatedText``SettingsList``CancellableLoader`
- 组件渲染缓存统一由宽度、内容和主题状态驱动,避免每帧重复解析 Markdown 或重算复杂布局
参考:`tmp/tui/src/components/*`
本次推进:
- 新增 `IFocusableComponent`,把焦点状态从组件内部自管改成由 `TuiRuntime.SetFocus` 统一写入
- `Input``SelectList``Editor` 改为只在 `Focused` 为 true 时输出 `CursorMarker`,避免多个可输入组件同时影响硬件光标定位
- `TuiRuntime` 在添加、移除、焦点切换、resize 和显式 `Invalidate()` 时递归清理组件缓存,为主题变化和复杂组件缓存失效提供框架入口
- `RenderPipelineOptions` 新增 `ShowHardwareCursor`,renderer 始终能把硬件光标定位到 marker,是否显示光标由配置或 `TINYTUI_HARDWARE_CURSOR` 控制
- 新增 `ITuiTheme` 和默认 `TuiTheme`,先提供 hint、dim、输入光标、设置项标签和值、选择 cursor 等跨组件样式入口
- 新增通用组件 `Spacer``TruncatedText``CancellableLoader`,覆盖空行间隔、ANSI 感知单行截断和 Escape 取消加载流程
- 新增 `TinyTUI.ComponentChecks` 最小检查项目,验证 focusable marker、通用组件渲染和 cancellable loader 取消行为
为什么先做:
- `tmp/tui/src/tui.ts``Focusable.focused` 是组件基础设施的关键分界:运行时决定谁有焦点,组件只根据状态决定是否发出硬件光标 marker;先补这一层能减少后续 overlay、autocomplete、settings 子菜单抢焦点时的耦合
- 主题和通用组件先提供薄接口和默认实现,不直接重写所有现有组件样式,避免一次提交同时改变大量视觉输出
- `Invalidate()` 先接入 runtime 生命周期和 resize,不提前实现完整缓存系统,但给 Markdown、SettingsList、Autocomplete 这类后续复杂组件留下统一失效入口
当前更好的点:
- C# 侧通过 `IFocusableComponent` 明确焦点状态契约,运行时切换焦点时会同时 invalidate 前后组件,后续带缓存组件不需要自己猜测焦点变化
- 硬件光标显示配置放在渲染管线选项里,组件只负责 marker,避免组件直接依赖终端输出细节
- `CancellableLoader` 暴露标准 `CancellationToken`,比只暴露回调更容易接入 C# async 工作流
- `TruncatedText` 直接复用 `ITextMeasurer`,能继承第 5 项已经补过的 ANSI、OSC 和 grapheme 宽度处理
本次 Runtime 级主题上下文推进:
- 新增 `IThemeComponent`,让组件可以显式接收 Runtime 级共享 `ITuiTheme`
- `TuiRuntime` 现在持有 `Theme`,构造函数支持传入初始主题,默认仍使用 `TuiTheme.Default`
- `ITuiRuntime` 增加 `ThemeChanged``SetTheme(ITuiTheme theme)`,主题切换时会更新已挂载根组件和 overlay 组件并请求重渲染
- `MountRuntimeComponentTree` 复用现有 Runtime 挂载上下文,在根组件、overlay、动态 `Container.Add` 和内置 `Box.Child` 路径同时注入共享主题和共享 keybinding
- `Input``SelectList``Image``CancellableLoader``Editor` 接入主题注入,其中 `Editor` 会把主题转发给内部 `AutocompleteList`
- 新增 xUnit 回归测试,覆盖根组件主题注入、overlay 主题注入、动态容器子组件主题注入、`SetTheme` 刷新已挂载组件且不破坏 keybinding 注入
本次为什么这样做:
- 对比 `tmp/tui/src/tui.ts`,参考实现的关键生命周期是 `TUI.invalidate()` 会递归根组件和 overlayC# 侧已有 Runtime 挂载计数,因此把主题注入接到同一条生命周期能避免维护两套递归逻辑
- 主题切换只负责更新共享主题、触发组件失效和重渲染,不扩展控件样式细节,避免把本轮任务扩大成完整主题系统
- `IThemeComponent``IKeybindingComponent` 对称,第三方组件可以选择接入并自行向内部子组件转发,Runtime 不需要用反射猜测组件结构
本次当前更好的点:
- C# 侧主题是 Runtime 实例级状态,不是模块级全局对象,多 Runtime 和测试场景可以隔离不同主题
- 已挂载 overlay 和动态添加的容器子树会收到同一个主题对象,避免浮层和后续加入组件继续使用默认主题
- 主题切换路径不会重建 keybinding registry,测试覆盖了切换主题后共享快捷键仍然生效
本次通用复合组件子树枚举推进:
- 新增 `ICompositeComponent`,让复合组件可以通过 `GetChildren()` 暴露直接子组件,Runtime 不再需要硬编码认识每一种内置复合组件
- `Container``Box` 实现统一子组件枚举契约,保留现有 `Container.ChildAdded` / `ChildRemoved` 动态事件作为可变容器专属能力
- `TuiRuntime.MountRuntimeComponentTree``UnmountRuntimeComponentTree` 改为通过 `ICompositeComponent` 递归,keybinding、theme、invalidate 和动态挂载上下文共用同一条子树遍历路径
- 新增第三方复合组件回归测试,覆盖 Runtime 根组件添加、overlay 添加、已挂载 `Container` 动态添加后,内部 `Input` 和主题组件都能收到 Runtime 共享上下文
本次为什么这样做:
- 对比 `tmp/tui/src/tui.ts``tmp/tui/src/components/box.ts`,参考实现的 `Container` / `Box` 都以 `children` 作为复合组件结构来源;C# 侧无法依赖 TS 的公开字段约定,因此用显式接口把这个能力稳定成组件契约
- 动态增删事件仍只保留在 `Container`,是因为本轮目标是统一“如何枚举子树”,不是扩展所有复合组件的可变生命周期协议,避免把 `Box` 或第三方组件强行要求成可变容器
- Runtime 挂载上下文本来已经负责 keybinding 和 theme 注入,本轮只替换递归来源,保持现有根树、overlay 和动态容器行为不退化
本次当前更好的点:
- 第三方复合组件只要实现 `ICompositeComponent`,内部子树就能自动拿到 Runtime 级 keybinding 和 theme,不再需要自己实现 `IKeybindingComponent``IThemeComponent` 转发
- Runtime 的子树遍历入口收敛为一个 helper,后续 invalidate、生命周期、dispose 或更多上下文注入可以复用同一个契约,减少继续追加 `Container` / `Box` 分支的风险
- C# 侧把“可枚举子树”和“动态子组件事件”拆开,静态复合组件可以轻量接入,动态容器仍能保留已有挂载计数和事件订阅语义
本次 Runtime 组件生命周期上下文推进:
- 新增 `TuiRuntimeContext`,把 Runtime、共享 `KeybindingRegistry`、共享 `ITuiTheme`、当前 `ITerminalSession``TerminalSize`、Kitty 协议状态和 Runtime 退出 `CancellationToken` 收敛成组件可接收的上下文快照
- 新增 `IRuntimeContextComponent`,组件可以通过 `OnMounted``OnUnmounted``OnRuntimeContextChanged` 感知首次挂载、最后卸载和 Runtime 上下文变化
- `TuiRuntime.MountRuntimeComponentTree``UnmountRuntimeComponentTree` 继续沿用挂载计数,同一组件经根树和 overlay 多路径挂载时只在首次挂载触发 `OnMounted`,最后一次卸载触发 `OnUnmounted`
- keybinding 和 theme 的旧接口注入改为由统一 Runtime 上下文路径驱动,`IKeybindingComponent``IThemeComponent` 仍保持兼容,不要求现有组件立即迁移到新生命周期接口
- `SetTheme`、终端 resize、`Stop``Dispose` 会通知仍挂载组件 Runtime 上下文变化,Runtime 停止时取消上下文令牌但不自动卸载组件
- `CancellableLoader` 接入 Runtime 上下文取消令牌,Runtime 停止时会复用现有 `Cancel()` 路径让等待中的异步工作退出,组件卸载时解除令牌注册
- 新增 xUnit 回归测试,覆盖根组件添加移除、overlay show/remove、动态 `Container` 子组件、同一组件多路径挂载计数、`SetTheme` 上下文变化、Runtime `Stop` / `Dispose` 不重复卸载仍挂载组件
本次为什么这样做:
- 对比 `tmp/tui/src/tui.ts`,参考实现没有显式 mount/unmount 接口,主要依赖 `TUI extends Container`、递归 `invalidate()` 和组件构造注入 theme;C# 侧 Runtime 是组合式根树并已有多 Runtime 实例状态,因此用显式上下文契约更适合 keybinding、theme、终端能力和取消信号统一传播
- 生命周期通知放在 Runtime 的组件树挂载边界,而不是单个控件内部,是为了覆盖根树、overlay 和动态容器三条路径,避免各组件自行猜测是否已经进入运行时
- Stop/Dispose 只取消 Runtime 上下文,不自动 `Dispose` 用户组件,是为了避免 Runtime 移除 overlay 或根组件时释放调用方仍持有、可能复用的组件对象
本次当前更好的点:
- C# 侧把终端会话和能力状态放进上下文,组件不需要通过全局变量读取当前终端尺寸或 Kitty 协议状态
- 上下文是 Runtime 实例级快照,多 Runtime 测试和多终端实例可以各自隔离 keybinding、theme、终端会话和取消令牌
- 重复挂载计数让同一组件可以同时出现在根树和 overlay,避免一次隐藏 overlay 就提前触发卸载或取消仍在根树中的组件
本次可变复合组件动态契约推进:
- 新增 `IMutableCompositeComponent``CompositeComponentChildChangedEventArgs`,把动态子组件 `ChildAdded` / `ChildRemoved``Container` 专属能力抽成通用复合组件契约
- `Container` 实现通用可变复合组件契约,同时保留原有 `ContainerChildChangedEventArgs` 和公开事件类型,避免旧调用方因为事件参数类型变化断裂
- `TuiRuntime` 的挂载上下文不再特判 `Container`,改为订阅所有 `IMutableCompositeComponent`,动态添加的第三方子树会自动获得 keybinding、theme 和 runtime context 注入
- 动态移除第三方可变复合组件子树时会复用同一条 `UnmountRuntimeComponentTree` 路径,触发 `IRuntimeContextComponent.OnUnmounted` 并解除子树内动态事件订阅
- 新增 xUnit 回归测试,用自定义第三方可变复合组件覆盖动态添加后快捷键提交、主题注入、Runtime 上下文挂载,以及动态移除后的 unmount 通知
本次为什么这样做:
- 对比 `tmp/tui/src/tui.ts`,参考实现的 `TUI extends Container` 让根容器动态增删天然影响渲染,但它没有给第三方可变复合组件提供独立生命周期事件;C# 侧 Runtime 是组合式根树,需要显式事件契约才能把 Runtime 级上下文传播到任意动态子树
- `ICompositeComponent` 继续只负责静态子树枚举,`IMutableCompositeComponent` 只给会动态增删子组件的类型实现,避免让固定结构组件如 `Box` 承担不需要的事件协议
- `Container` 使用显式接口事件接入通用契约,是为了让 Runtime 看到统一事件类型,同时保留原公开 API 的兼容性
本次当前更好的点:
- 第三方可变复合组件只要实现 `IMutableCompositeComponent`,无需继承 `Container` 或让 Runtime 认识具体类型,也能获得完整挂载上下文
- 动态添加和动态移除都走同一套 Runtime 挂载计数,根树和 overlay 多路径挂载时仍能避免重复 mount 或提前 unmount
- C# 侧把“可枚举子树”和“可变子树事件”拆成两个接口,比参考实现依赖 `instanceof Container` 的递归判断更适合第三方组件扩展
本次焦点生命周期修复推进:
- `TuiRuntime.Remove` 和动态 `IMutableCompositeComponent.ChildRemoved` 路径会在卸载子树前递归检查当前焦点是否位于被移除子树内
- 如果焦点命中被移除子树,Runtime 会优先切到当前最上层可聚焦 overlay;普通根树或动态容器移除则保守清空焦点
- 子树检测复用 `ICompositeComponent.GetChildren()`,因此 `Container``Box` 和第三方复合组件都走同一套焦点清理逻辑
- 新增 xUnit 回归测试,覆盖动态 `Container.Remove(focusedInput)``Container.Remove(boxContainingFocusedInput)` 和第三方 `IMutableCompositeComponent.Remove(subtree)` 后旧输入组件不再收到后续输入
本次为什么这样做:
- 对比 `tmp/tui/src/tui.ts`,参考实现通过 `containsComponent` / `isComponentMounted` 辅助判断焦点目标是否仍处于组件树内;C# 侧已有通用复合组件枚举契约,因此把判断落在 Runtime 子树移除边界即可避免 stale focus
- 清理发生在 `UnmountRuntimeComponentTree` 前,是因为此时被移除子树仍可通过 `GetChildren()` 完整遍历,能可靠判断深层已聚焦子组件
- 本轮不扩展完整 overlay blocked restore 机制,只在 overlay 场景使用现有 `_overlayManager.TopFocusableComponent` 作为安全 fallback,避免把焦点生命周期修复扩大成多层 overlay 重构
本次当前更好的点:
- C# 侧焦点清理支持第三方 `ICompositeComponent` / `IMutableCompositeComponent`,不依赖参考实现中的 `instanceof Container`
- Runtime 统一在根移除和动态复合移除两条路径处理焦点,后续输入派发入口不需要每次再判断焦点组件是否仍挂载
- 保守清空焦点能确保已卸载组件不会再接收输入,比继续保留显式 `SetFocus` 语义更安全
本次 owned 组件释放生命周期推进:
- `ITuiRuntime` 新增 `AddOwned(IComponent component)``ShowOwnedOverlay(IComponent component, OverlayOptions? options = null)`,显式表达 Runtime 接管组件释放责任
- `Add``ShowOverlay` 保持不释放用户组件的默认语义,避免调用方仍持有或准备复用的组件在移除后被 Runtime 意外释放
- `TuiRuntime` 新增 owned 组件集合和已释放集合,owned 子树通过 `ICompositeComponent.GetChildren()` 递归标记,动态 `IMutableCompositeComponent.ChildAdded` 会继承父组件所有权
- root `Remove`、overlay handle `Hide`、动态子组件移除和 Runtime `Dispose` 都会尝试释放 owned disposable,但只有在组件挂载计数归零或 Runtime 自身释放时才真正释放
- 同一 owned 组件通过根树和 overlay 多路径挂载时,关闭 overlay 不会提前释放,最后一个挂载路径移除后才释放一次
- 新增 xUnit 回归测试,覆盖 owned root、owned overlay、owned composite children、owned dynamic children、多路径挂载、非 owned 不释放、重复 remove/hide/dispose 不二次释放
本次为什么这样做:
- 对比 `tmp/tui/src/tui.ts`,参考实现的 remove/hide 只管理组件树和焦点恢复,并没有自动 dispose 用户组件;C# 侧补 owned API 而不是改变默认 `Add` / `ShowOverlay`,可以保留同样保守的用户组件语义
- owned 状态接到 Runtime 现有挂载计数,是为了和 `OnMounted` / `OnUnmounted` 的最后卸载语义一致,避免多路径挂载时某一路移除导致资源提前释放
- 释放递归复用 `ICompositeComponent``IMutableCompositeComponent`,让第三方复合组件只要实现现有契约就能被 owned 生命周期覆盖,不需要 Runtime 继续硬编码具体组件类型
本次当前更好的点:
- C# 侧显式区分“进入 Runtime 生命周期”和“Runtime 拥有释放责任”,比隐式 dispose 所有 `IDisposable` 用户组件更安全
- owned 释放覆盖动态子组件和第三方复合组件子树,能力范围比参考实现只递归 `Container``containsComponent` 判断更通用
- Runtime Dispose 会释放仍挂载的 owned 组件,同时用已释放集合保证 root remove、overlay hide 和 Runtime dispose 任意顺序下同一实例只释放一次
后续仍需补齐:
- owned 释放目前只覆盖同步 `IDisposable`,如果后续组件持有异步资源,需要评估是否引入 `IAsyncDisposable` 和异步 Runtime dispose 流程
- `IMutableCompositeComponent` 目前只表达直接子组件增删,没有批量变更、移动、替换或变更事务;如果后续出现高频动态列表,需要补批量事件或挂载 diff 策略减少重复注入
- owned 动态子组件继承父组件所有权,但还没有单独的“释放子树但保留父组件”策略;如果后续需要组件池或临时 detach/reuse,需要增加更细粒度的所有权转移 API
- `SettingsList` 已完成基础移植,覆盖标签和值渲染、搜索 fuzzy 过滤、上下移动、Enter/Space 值循环、Escape 取消、选中项描述换行和 submenu 输入委托
- `ITuiTheme` 当前仍是轻量样式接口,还没有 theme version、分区主题、Markdown 完整主题或控件级样式覆盖策略
- `Input` 目前仍是尾部输入模型,还没有参考实现里的水平滚动、按 grapheme 移动、kill ring 和 undo;这些应在输入/编辑能力后续增量中处理
- 组件缓存规范还只具备 invalidate 入口,没有统一的 width/content/theme cache key,也没有 Markdown AST 或复杂布局缓存基类
- 焦点恢复策略仍比 `tmp/tui/src/tui.ts` 简化,overlay 被临时隐藏、非 overlay 组件短暂抢焦点和多层 submenu 的 blocked restore 还需要继续补强
- 硬件光标定位已可配置显示隐藏,但还没有维护 renderer 级 `hardwareCursorRow` 相对移动模型,IME 在滚动和 resize 场景仍需要真实终端验证
本次 SettingsList 移植总结:
- 新增 `SettingItem``SettingsList`,设置项保留 `Id``Label``Description``CurrentValue`、可循环 `Values` 和同步 submenu 工厂
- `SettingsList` 接入 `IInputComponent``IFocusableComponent``IKeybindingComponent``IThemeComponent``IMutableCompositeComponent`,主列表处理标准化输入,激活 submenu 后把输入完全委托给子组件
- 新增 `FuzzyMatcher` / `FuzzyMatch`,按 `tmp/tui/src/fuzzy.ts` 的顺序匹配、连续奖励、边界奖励、间隔惩罚和字母数字换序兼容实现搜索过滤
- `ITuiTheme` 补充 `SettingDescription`,描述行使用现有 ANSI 感知 `TerminalTextMeasurer.Wrap` 换行,不额外引入完整主题分区系统
- 新增 `SettingsListTests` 覆盖渲染与描述、搜索过滤、值循环、取消、submenu 输入转发和 done 后恢复选择
本次为什么这样做:
- 对比 `tmp/tui/src/components/settings-list.ts`C# 侧没有把搜索实现为嵌套 `Input`,而是直接消费 `TuiInputEvent` 并维护 `SearchQuery`,这样可以避免为单行过滤框额外引入焦点切换和光标模型
- submenu 使用 `IMutableCompositeComponent.ChildAdded` / `ChildRemoved` 事件暴露动态子组件,让已挂载 Runtime 复用现有主题、快捷键和生命周期注入路径,不需要扩展 overlay 或焦点策略
- `SettingItem.CurrentValue` 设计为可变属性,是为了和参考实现一致,让组件内部值循环、外部 `UpdateValue` 和 submenu done 回调都能更新同一设置项模型
本次当前更好的点:
- C# 版本复用标准化输入事件和 `KeybindingRegistry`,不会像参考实现一样依赖原始字符串按键分支
- 描述换行复用 ANSI 感知测量器,后续如果描述文本带样式或超链接,换行边界比参考实现里的普通工具函数更贴合现有渲染管线
- 动态 submenu 通过通用可变复合组件契约进入 Runtime,上下文注入能力覆盖第三方子菜单组件,不需要 Runtime 识别 `SettingsList` 具体类型
- `FuzzyMatcher` 做成通用文本工具,后续 Autocomplete、文件搜索或命令面板可以复用同一套排序规则
SettingsList 后续仍需补齐:
- submenu 仍是同步工厂和 done 回调,未实现异步加载、取消中的状态恢复或 overlay 自动挂载
- 搜索框目前是轻量尾部输入,只支持追加和 Backspace,不支持光标移动、水平滚动、组合文本编辑或输入法边界验证
- 主题只补了 settings 描述样式,还没有控件级主题对象、主题版本或完整 settings 分区
- fuzzy 搜索当前只按 `Label` 过滤,后续如果要匹配 `Id``CurrentValue``Description`,需要明确排序权重
- `SettingsList` 自身没有引入布局缓存,复杂列表大量刷新时仍依赖后续统一 cache key 设计
### 7. 自动补全和命令交互基础
目标:把自动补全从 Editor 局部功能升级为可复用的交互能力
- 定义 autocomplete provider 接口,支持同步和异步候选
- 支持 slash command 候选、文件路径候选、特殊前缀候选
- 使用 overlay 呈现补全列表,并复用 SelectList 的选择、过滤和滚动能力
- 将补全确认、取消、预览、应用文本变更抽象成独立流程
- 让 keybinding 系统负责 Tab、Enter、Escape 等动作触发,避免补全逻辑和具体按键强绑定
参考:`tmp/tui/src/autocomplete.ts``tmp/tui/src/components/editor.ts`
本次增量已完成:
- 新增 `TinyTUI.Autocomplete` 模块,定义 `IAutocompleteProvider``AutocompleteRequest``AutocompleteSuggestions``AutocompleteApplyContext``AutocompleteApplyResult`,先把“查询候选”和“应用候选”从 Editor 中拆成可复用契约
- 新增 `AutocompleteProviderBase`,提供默认的前缀替换实现,后续文件路径、变量、命令参数等 provider 可以只覆盖查询或特殊应用逻辑
- 新增 `SlashCommandAutocompleteProvider``SlashCommand`,支持行首 `/` 命令名候选,确认后自动写回 `/{command} ` 并把光标放到参数位置
- 新增 `FilePathAutocompleteProvider`,支持普通路径和 `@` 附件路径补全,覆盖 `./``../``/``~/`、含路径分隔符的 token、quoted path 和路径含空格自动加引号
- 新增 `CompositeAutocompleteProvider`,按顺序组合 slash command、文件路径或后续自定义 provider,让 Editor 仍只依赖一个 `IAutocompleteProvider`
- `SlashCommand` 新增参数补全入口,支持 `/command argPrefix` 场景调用命令自己的补全回调,返回候选时只把 `argPrefix` 作为待替换前缀
- `Editor` 增加 `AutocompleteProvider``AutocompleteList``IsAutocompleteActive``StartAutocomplete``ConfirmAutocomplete``CancelAutocomplete``RenderAutocomplete`,补全列表复用现有 `SelectList` 的选择、滚动和渲染能力
- 新增 `tui.autocomplete.trigger``tui.autocomplete.confirm``tui.autocomplete.cancel` 默认动作,Tab 触发或确认补全,Enter 在补全激活时确认,Escape 取消补全
- 扩展 `TinyTUI.ComponentChecks` 覆盖 slash command 候选渲染、方向键切换候选、Tab 确认应用和 Escape 取消
- 新增 `TinyTUI.Tests.Autocomplete` 回归测试,覆盖路径前缀提取、目录优先排序、嵌套路径、`@` 附件前缀、quoted path、路径含空格加引号、确认应用和组合 provider 分派
这样设计的原因:
- provider 契约返回 `items + prefix`,与 `tmp/tui/src/autocomplete.ts` 的核心模型一致,Editor 不需要知道候选来自命令、路径还是特殊前缀
- 应用候选独立成 `ApplyCompletion`,是为了保留 slash command 自动补空格、路径目录不补空格、引号去重等后续差异,不把这些规则硬塞进 Editor
- slash command 参数补全复用默认 prefix 替换,而命令名补全仍由专门分支自动补 `/{command} `,这样参数候选确认时不会重写命令名,也不会把参数误当成新的 slash command
- 路径 provider 只做当前目录同步枚举和稳定排序,不引入 fd/fuzzy 搜索,是为了先把可预测的补全协议、引用规则和确认行为稳定下来,避免把异步搜索和请求取消提前塞进 Editor
- 组合 provider 记录最近一次返回候选的 provider,确认时把 `ApplyCompletion` 分派回原 provider,这样 slash command 和路径补全可以共用一个 Editor 入口
- Editor 内部持有 `SelectList`,先把补全选择和确认流程跑通,后续 overlay 只需要挂载 `RenderAutocomplete` 或直接挂载同一个列表组件
- 补全确认通过现有 `Edit` 包装写回文本,所以撤销栈、redo 清理和 `OnChanged` 行为与普通编辑保持一致
怎么看懂当前代码:
- 先看 `Autocomplete/IAutocompleteProvider.cs`,它定义自动补全框架的两个关键动作:查询候选和应用候选
- 再看 `Autocomplete/SlashCommandAutocompleteProvider.cs`,它展示了一个最小 provider 如何根据当前行和光标返回命令名或参数候选,以及如何把候选写回文本
- 然后看 `Autocomplete/FilePathAutocompleteProvider.cs`,重点是 `ExtractAtPrefix` / `ExtractPathPrefix` 负责判断当前 token`ResolveSearchScope` 负责把显示路径映射到真实目录,`ApplyCompletion` 负责目录不补空格、文件补空格和 closing quote 去重
- 如果要同时启用命令和路径补全,看 `Autocomplete/CompositeAutocompleteProvider.cs`,provider 顺序就是优先级,先返回候选的 provider 会负责后续确认应用
- 最后看 `Components/Editor/Editor.Autocomplete.cs`,这里是 Editor 的补全状态机:启动查询、用 `SelectList` 展示候选、确认应用、取消清理
- `Components/Editor/Editor.cs``HandleInput` 入口可以看到补全输入优先级:活动补全先消费导航和确认,普通 Tab 强制触发补全,普通文本输入后尝试自然补全
对比 `tmp/tui/src/autocomplete.ts``tmp/tui/test/autocomplete.test.ts` 的结论:
- 已对齐的核心行为:prefix 提取仍以“光标前 token”为边界,支持 `@``@"..."`、普通 `"..."`、路径分隔符和强制 Tab 补全;候选值保留用户输入的显示前缀,目录追加 `/`,文件确认后追加空格
- 已对齐的 slash command 参数行为:`/command argPrefix` 会先定位命令名,再调用该命令的参数补全回调;回调无结果或不存在时返回 null;确认参数候选时只替换 `argPrefix`,不会重写 `/command `
- 已对齐的引用行为:路径含空格或原输入已经处于 quoted path 时自动生成 quoted value;确认 quoted completion 时如果光标后已有 closing quote,会移除旧 quote,避免出现重复 closing quote
- 已对齐的排序行为:目录优先,再按名称进行大小写不敏感排序,并增加原始名称排序作为稳定兜底
- 当前 C# 版本更好的点:路径 provider 被拆成独立类型,不和 slash command 逻辑混在一个 `CombinedAutocompleteProvider` 里;组合能力由 `CompositeAutocompleteProvider` 提供,后续变量、命令参数或远程资源补全可以按优先级插入
- 当前 C# 版本更好的点:测试不依赖外部 `fd` 是否安装,普通路径和 `@` 附件路径都用临时目录同步验证,结果更稳定
- 当前 C# 版本更好的点:`AutocompleteRequest` 使用 `CancellationToken``ValueTask` 预留异步 provider 形态,后续迁移 fd/fuzzy 时不需要改 provider 主接口
- 当前 C# 版本更好的点:命令名应用和参数应用分成两个清晰路径,参数补全直接复用 `AutocompleteProviderBase` 的前缀替换逻辑,比参考实现里在一个 `applyCompletion` 中靠字符串上下文猜测更容易测试和维护
对比 `tmp/tui` 后续仍需补齐:
- C# 侧 provider 契约已用 `ValueTask` 支持异步形态,但 Editor 当前 `HandleInput` 仍是同步接口,会阻塞等待 provider;后续需要 Runtime 级 invalidate 和取消令牌来实现真正非阻塞异步补全
- 文件路径 provider 已支持本地同步目录枚举、`@` 附件前缀、引号路径、`~/` 展开和目录优先排序,但还没有移植 fd fuzzy 搜索、递归匹配、`.git` 排除、隐藏文件策略配置或 symlink 递归跟随
- slash command 参数补全当前只把第一个空格后的完整文本作为 `argumentPrefix` 传给命令回调,与参考实现一致;后续如果要支持多参数、引号内参数或 option/value 级别补全,需要为参数范围引入更明确的解析协议
- 还没有把补全列表通过 `OverlayManager` 自动挂到光标附近,目前只提供 `RenderAutocomplete` 和内部 `SelectList` 供后续 overlay 接线
- 还没有做补全预览文本、候选变化 debounce、过期请求丢弃和 AbortController 等请求竞争处理
- `CompositeAutocompleteProvider` 当前用最近一次查询记录确认 provider,适合现有同步 Editor 流程;如果后续多个 Editor 共享同一个组合 provider 或实现并发异步查询,应把 provider 归属放进 suggestions 上下文或引入补全 session
- 当前默认前缀替换基类按字符串长度处理 prefix,后续支持宽字符或跨行补全时需要把 provider 光标协议统一成 Rune 列或专门的文本范围类型
### 8. 图像和特殊终端内容
目标:为 Kitty / iTerm2 图像和特殊终端序列预留完整框架入口
- 检测终端图像能力,选择 Kitty graphics protocol、iTerm2 inline image 或文本 fallback
- 解析图片尺寸并按终端 cell 尺寸计算显示区域
- 渲染差分时追踪已显示的 Kitty image id,在内容变化或清屏时删除旧图像
- 合成和截断逻辑识别 image line,避免把图像序列当普通文本切坏
- 在终端 resize 或 cell size 变化后刷新图像布局
参考:`tmp/tui/src/terminal-image.ts``tmp/tui/src/components/image.ts`
本次推进:
- 新增 `TinyTUI.Terminal.Images` 图像基础设施,提供终端能力检测、Kitty/iTerm2 协议选择、cell 像素尺寸、图片尺寸解析、终端 cell 尺寸换算、Kitty/iTerm2 编码和 fallback 所需的数据结构
- 新增 `Image` 组件,支持 PNG/JPEG/GIF/WebP 尺寸解析结果或外部尺寸注入,终端支持图像时输出 Kitty/iTerm2 内联图像序列,不支持时输出带文件名、MIME 和尺寸的文本 fallback
- `RenderFrameBuilder` 识别 Kitty 和 iTerm2 image line,遇到图像序列时跳过普通宽度测量和截断,避免把大段 base64 当作可见文本导致溢出或切坏协议序列
- `DifferentialRenderer``FullScreenRenderer` 增加 Kitty image cleanup,清屏、reset 或变化行覆盖旧 Kitty 图像时写出删除序列,降低图像残影
- 扩展 `TinyTUI.ComponentChecks` 覆盖 cell 尺寸换算、PNG 尺寸解析、Kitty Image 组件输出、fallback、image line 任意位置识别、长 iTerm2 image line 不触发宽度溢出和差分删除旧 Kitty image id
为什么先做:
- `tmp/tui/src/terminal-image.ts` 的图像能力是渲染、组件和终端环境共同依赖的底座;先把 C# 侧服务抽出来,后续真实 cell size 查询、resize 后重新布局和更多组件都能复用同一个入口
- 参考实现里 `isImageLine` 的回归说明很明确:图像序列可能出现在行中间,且终端不支持图像时仍需要识别,否则宽度校验会处理几百 KB base64 并崩溃;因此本次优先把 image line 保护接到渲染帧构建
- Kitty 图像不会只靠清行自动消失,差分渲染覆盖旧内容时必须发删除序列;本次只做可见帧中的 id 追踪和变化行清理,避免一次性引入完整滚动区域和硬件光标模型重构
当前更好的点:
- C# 侧 `ITerminalImageService` 是显式可替换服务,组件测试可以直接注入固定能力和 cell 尺寸,不需要改全局环境变量
- `Image` 组件构造时直接接受 `byte[]` 并缓存 base64,尺寸解析和协议渲染分离,调用方可以用真实图片数据,也可以为测试或远端元数据直接注入尺寸
- renderer 层的 image line 判断不依赖当前终端是否支持图像,能覆盖“fallback 终端仍收到工具输出图像 escape”的崩溃场景
后续仍需补齐:
- 终端 cell size 目前仍是默认估计或外部手动设置,还没有像 `tmp/tui/src/tui.ts` 那样查询 `CSI 16 t` 并在响应后 invalidate 所有图片组件
- 能力检测对 tmux hyperlink forwarding 仍保守禁用,没有调用 tmux client_termfeatures,也没有区分更多终端的图像代理能力
- Kitty cleanup 目前只按可见行变化删除旧 id,没有实现参考实现中的 `expandLastChangedForKittyImages`、滚动区域跨行扩展和 previousKittyImageIds 全量精细同步
- `Image` 组件还没有真实示例入口、动画/更新复用 id 的公开构造选项,也没有 iTerm2 name、inline、preserveAspectRatio 等完整参数
- WebP/JPEG/GIF 尺寸解析已有基础路径,但还缺专门测试样本和异常格式覆盖;后续第 9 项测试基础设施应把这些协议和 renderer cleanup 用例迁移到正式测试项目
- overlay 合成对 image line 仍只是依赖文本切片层不主动切 escape,后续需要在 overlay manager 中像参考实现一样遇到 image line 直接保留或跳过合成,避免浮层覆盖图片区域时产生未定义行为
### 9. 测试基础设施
目标:引入可以验证真实终端行为的测试层,而不是只靠 Example 手动看效果
- 建立虚拟终端或 fake terminal output,捕获 ANSI 输出并模拟窗口尺寸
- 覆盖输入缓冲、按键协议、keybinding、文本宽度、ANSI 换行和截断
- 覆盖差分渲染、内容缩短清理、resize full redraw、viewport 覆盖和硬件光标定位
- 覆盖 overlay 样式泄漏、短内容覆盖长内容、宽字符边界和多层焦点恢复
- 覆盖 Markdown、SelectList、Editor、Autocomplete 等组件的框架行为
- 示例仍用于人工验收,但不能替代回归测试
参考:`tmp/tui/test/virtual-terminal.ts``tmp/tui/test/*.test.ts`
本次推进:
- 新增 `test/TinyTUI.Tests` 正式 xUnit 测试项目并加入 `ttui.slnx`,让后续回归可以通过 `dotnet test ttui.slnx` 统一执行,不再只依赖手写 console check
- 新增 `VirtualTerminalOutput` 测试辅助,捕获渲染器写出的 ANSI 序列、记录每次 write、模拟终端窗口尺寸,并复用 `ITerminalOutput` 接口验证真实渲染输出路径
- 新增 `AnsiAssert`,提供 ANSI 感知的可见文本比较,便于 overlay、renderer 和文本工具测试在保留 escape sequence 的同时断言最终可见内容
- 迁移并规范化 TextChecks 的关键文本回归,覆盖 ANSI/tab/emoji 宽度、按列切片、ANSI 截断、省略符和 OSC 8 wrap
- 新增输入基础测试,覆盖 `StdinBuffer` 分块 escape、批量 Kitty CSI-u 片段、bracketed paste、flush 未完成序列、`DefaultInputParser` 常见按键和 keybinding 冲突归一化
- 新增 overlay 回归测试,覆盖短内容时 overlay 仍可见、短 overlay 覆盖长底层行时保留右侧内容、overlay 段 reset 隔离
- 新增差分渲染测试,覆盖 bottom viewport、行尾 reset、只重绘变化行、resize/shrink full redraw、硬件光标 marker 定位和 Kitty image 删除顺序
- 新增组件回归测试,覆盖 focusable 光标 marker、slash command autocomplete 确认/取消、Image 组件 Kitty/fallback 和 image line 任意位置识别
-`TextChecks` 残余断言完全迁移到 `TerminalTextMeasurerTests`,补齐 plain wrap、underline URL wrap、ANSI continuation 和 OSC 8 BEL hyperlink wrap 覆盖
-`ComponentChecks` 残余断言完全迁移到 `ComponentRegressionTests``DifferentialRendererTests`,补齐 SelectList focused marker、Spacer 负数 clamp、TruncatedText padding/首行截断、CancellableLoader 取消、PNG dimensions、长 iTerm2 image line overflow bypass
-`ttui.slnx` 移除 `test/TinyTUI.TextChecks``test/TinyTUI.ComponentChecks`,删除两个早期 console smoke check 项目,README 测试说明改为统一 xUnit 入口
为什么先做:
- `tmp/tui/test/virtual-terminal.ts` 的核心价值是让终端行为可自动断言;C# 侧当前没有 xterm.js 等完整终端模拟依赖,本次先用 `ITerminalOutput` 建立轻量 fake output,优先覆盖 renderer 实际写出的 ANSI 序列和尺寸变化
- 前面 TODO 5/6/7/8 已经产生 TextChecks 和 ComponentChecks,继续追加 console check 会让失败定位和 CI 接入变差;正式测试项目能把这些回归按模块拆分,并为第 10 项 CI 打基础
- 本次只固化已实现能力和已修过的关键回归,不把 Kitty 完整协议、真实终端屏幕缓冲或更多组件行为混进同一个 commit,降低测试基础设施增量的风险
- 旧 smoke check 已经和 xUnit 用例重叠,继续保留会导致同一行为维护两套入口;本次先按旧 `Program.cs` 的有效断言逐项对齐,再删除旧项目,保证边界清晰
当前更好的点:
- C# 侧测试辅助直接实现 `ITerminalOutput`,不需要绕过 renderer 或 runtime 私有状态,能观察 `ClearScreen``MoveCursorTo``HideCursor`、Kitty cleanup 等真实输出
- `VirtualTerminalOutput` 同时保留完整 buffer 和 write 片段,既能断言最终序列,也能检查删除图像必须早于新内容写入这类顺序问题
- 正式测试项目已覆盖 text/input/overlay/rendering/components 五个基础面,后续新增回归时有明确目录,不需要继续把所有断言塞进单个 `Program.cs`
- 相比 console check 手写 `AssertTrue`,xUnit 测试按模块命名,失败时能直接定位到文本、组件或 renderer 的具体行为
- 现有 C# 写法用 collection expression、target-typed `new``using var` 保持测试输入短小,断言结构更接近被测行为本身
后续仍需补齐:
- 还没有像 `tmp/tui/test/virtual-terminal.ts` 那样基于真实终端 emulator 解析 ANSI 后得到 viewport、scrollback、cell style 和 cursor position;当前只能断言写出的序列,不能验证终端最终屏幕状态
- `TextChecks``ComponentChecks` 已删除,后续新增回归应直接进入 `test/TinyTUI.Tests`,不要再恢复独立 console smoke check
- `DefaultInputParser` 当前对 `ESC[Z` Shift+Tab 仍会原样透传,本次测试只覆盖当前支持的修饰 CSI;后续输入协议增量应补正式回归
- `StdinBuffer` 还没有参考实现中的超时 flush、ESC+ESC+CSI 分裂、Kitty printable duplicate drop、鼠标协议和旧式 mouse sequence 测试
- overlay 测试目前通过 `OverlayManager.Compose` 断言合成结果,还没有跑完整 Runtime + Renderer + virtual terminal 的端到端路径,多层 overlay 焦点恢复和 viewport 对齐仍需继续补
- 渲染测试尚未覆盖 synchronized output、debug 日志、Termux resize 策略、硬件光标相对移动和完整 Kitty reserved row redraw 行为
- 图像尺寸目前只把旧 smoke check 的 PNG 样本迁入正式测试;参考 `tmp/tui/test/terminal-image.test.ts` 的大量协议负例、环境能力检测和 WebP/JPEG/GIF 样本仍建议后续拆成专门测试类
### 10. 工程化和发布完整性
目标:让 TinyTUI 从本地骨架变成可维护、可发布、可集成的类库
- 完善 README,覆盖快速开始、核心 API、组件接口、overlay、keybinding、文本工具和自定义组件约束
- 补 XML documentation 和包元数据,为 NuGet 发布做准备
- 增加 CIrestore、build、format、test、pack
- 增加 benchmark 或压力示例,用于观察大文本、频繁刷新、overlay 合成和 Markdown 缓存性能
- 拆分 Example 为多个真实场景:基础输入、overlay 菜单、聊天界面、Markdown 浏览、设置面板、图像 fallback
- 明确公开 API 和内部实现边界,避免后续重构时破坏使用者代码
本次推进:
- README 从空文件补成可执行入口文档,覆盖快速开始、最小 C# 示例、核心 API、内置组件、overlay、keybinding、文本工具、自动补全、图像 fallback、自定义组件约束、工程命令和调试日志
- `Directory.Build.props` 增加统一工程属性:`LangVersion=latest`、nullable、implicit usings、确定性构建、CI build 标记和统一 `build/artifacts` 输出目录
- `TinyTUI.csproj` 增加 NuGet 包元数据、tags、README 打包、XML documentation、symbol package 和包输出目录,为 `dotnet pack` 产出可发布包做准备
- 新增 GitHub Actions CI,按 restore、Release build、format verify、test、pack 的顺序覆盖工程化闭环
- 新增 `docs/public-api.md`,把应用侧稳定入口、推荐组合、组件约束和仍在演进的公开边界写清楚,避免后续重构误伤使用者依赖
- 新增 `benchmark/TinyTUI.Benchmarks` console 压力示例项目,并加入 `ttui.slnx`,覆盖大文本测量/截断、Markdown render、overlay compose、差分渲染频繁刷新和图像行检测保护
- README 增加压力示例运行命令和场景说明,明确它是本地观察入口,不替代 xUnit,也不作为默认 CI test gate
为什么先做:
- 对比 `tmp/tui/README.md``package.json`,参考实现已经能让使用者快速判断“怎么安装、怎么跑、有哪些 API、怎么测试和发布”;C# 侧 README 和包元数据为空或缺失,会阻塞集成和 NuGet 发布
- 第 9 项已经有正式 xUnit 项目,先接 CI 和 pack 可以把测试基础设施转成持续验证能力,后续每个框架增量都有自动守门
- 公开 API 边界先用文档声明,不急着重命名或收缩源码命名空间,避免在第 10 项里引入运行时行为变更
- benchmark 先选择无真实终端交互的内存输出,可以稳定覆盖参考实现 changelog 和源码里反复出现的性能风险:宽度测量缓存、Markdown 渲染缓存、overlay 按列合成、频繁 diff patch、image line 不进入普通文本测宽
当前更好的点:
- C# 侧把 XML documentation 和 artifacts 输出放在根级 `Directory.Build.props`,类库、测试和示例的构建产物路径一致,CI 和本地命令更容易对齐
- NuGet README 直接复用仓库根 README,包页面和源码文档不会维护两套快速开始
- CI 同时跑 `dotnet format --verify-no-changes``dotnet pack`,比只跑测试更早暴露工程文件、包元数据和格式问题
- 压力示例直接复用 `ITerminalOutput` 抽象和 `DifferentialRenderer`,不会为了 benchmark 绕过真实渲染路径;C# 的接口边界让同一份场景能在本地稳定统计写入次数、输出字节和 redraw 计数
- 图像行保护场景启用 `ThrowOnWidthOverflow`,能观察大段 Kitty payload 是否被 `RenderFrameBuilder` 按 image line 跳过普通测宽和截断,比只调用 `IsImageLine` 更贴近真实渲染风险
- benchmark 项目加入 solution 参与 restore/build,但 README 明确不作为默认 CI test gate,避免机器性能波动影响回归测试稳定性
后续仍需补齐:
- 当前还没有正式 `LICENSE` 文件,包使用 `PackageLicenseExpression=MIT` 只是发布元数据,后续应补仓库级 license 文本
- `PackageProjectUrl``RepositoryUrl` 先使用占位仓库地址,真实发布前需要替换成实际远端
- XML documentation 已启用,但大量 public API 还缺中文 summary,后续需要分模块补齐并决定是否把 CS1591 提升为 CI 约束
- CI 已提供 `dotnet format`,但当前仓库还没有 `.editorconfig`,格式规则仍主要依赖 SDK 默认值,后续应补编码、缩进、nullable 和 analyzer 策略
- benchmark 当前是可读压力示例,不是统计严格的微基准;后续如果要比较版本间性能,需要固定 warmup、迭代次数、结果导出格式和基线阈值,或再引入 BenchmarkDotNet
- Markdown 压力场景当前只能观察 C# 轻量逐行 render 成本,尚未具备参考实现 `Markdown` 的 AST 解析和缓存命中对照;后续补 Markdown 缓存时应把命中/失效计数加入输出
- overlay 压力场景覆盖普通文本和 ANSI reset 隔离,但 overlay 遇到 image line 的行为仍是已知后续项,暂不在本轮 benchmark 中模拟真实图片区域覆盖
- Example 仍是单个综合示例,尚未像参考实现 `test/chat-simple.ts` 那样拆出聊天、设置、图像 fallback、Markdown 浏览等真实场景
## 建议执行顺序
1. 先补终端会话能力、输入协议和 keybinding,因为它们会影响所有交互组件
2. 再升级文本模型、渲染管线和 overlay 合成正确性,因为它们决定整体显示稳定性
3. 然后补组件基础设施、自动补全和主题,让上层组件可以复用统一能力
4. 接着引入虚拟终端测试和关键回归用例,把已修过的问题固化下来
5. 最后处理图像、benchmark、README、CI、NuGet 等发布和扩展能力
## 暂缓
- 不继续列举 Editor、SelectList、Markdown 的零散小功能
- 不优先追求所有组件与 `tmp/tui` 逐项一致
- 图像能力可以等终端、渲染、文本和测试基础稳定后再做
- NuGet 发布可以等公开 API 基本稳定后再做