Files
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

77 KiB
Raw Permalink 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 已有启动侧查询、响应解析和 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 会设置 KittyProtocolActiveDA 响应或 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.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 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 暴露给运行时级输入监听器
  • 新增 TuiInputListenerTuiInputListenerContext,全局 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.tsaddInputListener 思路,把 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
  • InputSelectListEditorCancellableLoader 改为实现该接口,保留构造函数显式传入 registry 的兼容性
  • TuiRuntime.AddShowOverlay 会在组件进入根树或 overlay 栈前注入 Runtime.Keybindings
  • Runtime 注入会递归处理内置 Container.ChildrenBox.Child,避免容器里的输入组件继续使用各自默认 registry
  • Editor.SetKeybindings 会同步更新内部 AutocompleteList,确保补全候选列表导航也走 Runtime 共享配置
  • 新增 xUnit 回归测试,覆盖根输入组件、overlay 列表、容器子组件和 Editor 内部补全列表使用 Runtime 自定义快捷键

为什么这样做:

  • 对比 tmp/tui/src/keybindings.tsgetKeybindings() 全局 managerC# 侧已有 Runtime 实例级 Keybindings,显式注入更适合测试隔离和多 Runtime 场景
  • 注入点放在 AddShowOverlay,是因为这是组件真正进入运行时输入派发范围的边界,能让组件离线构造时仍保持原有默认行为
  • 递归只覆盖当前内置容器类型,不引入反射扫描,减少第三方组件属性命名或生命周期被 Runtime 误触碰的风险

当前更好的点:

  • 自定义 keybinding 现在能真实影响焦点组件行为,而不只是 Runtime listener 可见
  • C# 侧可以给每个 Runtime 持有独立 registry,比参考实现的模块级全局 keybindings 更容易做并发测试和多终端实例
  • Editor 内部补全列表与编辑器共享同一 registry,避免外层动作覆盖生效但内层选择列表仍用默认键的割裂行为

后续仍需补齐:

  • Runtime 只知道内置 ContainerBox 的子组件结构,第三方复合组件若要递归注入仍需要自己实现 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 新增 ChildAddedChildRemoved 事件,让 Runtime 可以感知已挂载容器后续动态增删的子组件
  • TuiRuntime 新增 Runtime 组件挂载上下文,组件进入根树或 overlay 时会递归挂载并注入共享 KeybindingRegistry
  • 已挂载 Container.Add(input) 会立即为新输入组件注入 Runtime 级快捷键,并触发子组件失效和重渲染请求
  • 已挂载 Container.Add(new Box(input)) 会递归穿透内置 Box.Child,避免动态加入内置复合组件子树时漏掉输入组件
  • 动态 RemoveClear 会递归卸载子树并解除容器事件订阅,避免已移除容器继续响应 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 递归仍只理解内置 ContainerBox,第三方复合组件的内部子树还需要实现自己的 IKeybindingComponent 转发或后续新增通用子组件枚举接口
  • Box 当前是固定单子组件结构,没有动态更换 Child 的 API;如果后续增加可变 Child,需要同样触发 Runtime 挂载上下文更新
  • 动态加入新输入组件后 Runtime 不会自动切焦点,本次保持现有显式 SetFocus 语义,后续可以按组件声明或 overlay 策略决定是否自动聚焦
  • 组件生命周期目前只覆盖 keybinding 注入和 invalidate,还没有像完整框架那样统一管理主题、终端能力、异步任务取消和组件 dispose

本次协议解析推进:

  • 新增 TuiKeyEventType,让 TuiInputEvent 可以承载 Press、Repeat、Release 三种按键阶段,并保留现有两参数构造默认 Press 的兼容行为
  • TuiInputEvent 增加 AlternateKeyRawSequence 元数据,用于承载 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.tsmatchesKey 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

本次推进:

  • 新增 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

本次推进:

  • 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.WrapTerminalTextMeasurer.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 工具支撑 visibleWidthtruncateToWidthsliceWithWidthwrapTextWithAnsi,本次选择在 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/tuiIntl.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 候选窗定位
  • 引入主题接口和默认主题,让组件样式可配置且跨组件一致
  • 增加通用组件:SpacerTruncatedTextSettingsListCancellableLoader
  • 组件渲染缓存统一由宽度、内容和主题状态驱动,避免每帧重复解析 Markdown 或重算复杂布局

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

本次推进:

  • 新增 IFocusableComponent,把焦点状态从组件内部自管改成由 TuiRuntime.SetFocus 统一写入
  • InputSelectListEditor 改为只在 Focused 为 true 时输出 CursorMarker,避免多个可输入组件同时影响硬件光标定位
  • TuiRuntime 在添加、移除、焦点切换、resize 和显式 Invalidate() 时递归清理组件缓存,为主题变化和复杂组件缓存失效提供框架入口
  • RenderPipelineOptions 新增 ShowHardwareCursor,renderer 始终能把硬件光标定位到 marker,是否显示光标由配置或 TINYTUI_HARDWARE_CURSOR 控制
  • 新增 ITuiTheme 和默认 TuiTheme,先提供 hint、dim、输入光标、设置项标签和值、选择 cursor 等跨组件样式入口
  • 新增通用组件 SpacerTruncatedTextCancellableLoader,覆盖空行间隔、ANSI 感知单行截断和 Escape 取消加载流程
  • 新增 TinyTUI.ComponentChecks 最小检查项目,验证 focusable marker、通用组件渲染和 cancellable loader 取消行为

为什么先做:

  • tmp/tui/src/tui.tsFocusable.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 增加 ThemeChangedSetTheme(ITuiTheme theme),主题切换时会更新已挂载根组件和 overlay 组件并请求重渲染
  • MountRuntimeComponentTree 复用现有 Runtime 挂载上下文,在根组件、overlay、动态 Container.Add 和内置 Box.Child 路径同时注入共享主题和共享 keybinding
  • InputSelectListImageCancellableLoaderEditor 接入主题注入,其中 Editor 会把主题转发给内部 AutocompleteList
  • 新增 xUnit 回归测试,覆盖根组件主题注入、overlay 主题注入、动态容器子组件主题注入、SetTheme 刷新已挂载组件且不破坏 keybinding 注入

本次为什么这样做:

  • 对比 tmp/tui/src/tui.ts,参考实现的关键生命周期是 TUI.invalidate() 会递归根组件和 overlayC# 侧已有 Runtime 挂载计数,因此把主题注入接到同一条生命周期能避免维护两套递归逻辑
  • 主题切换只负责更新共享主题、触发组件失效和重渲染,不扩展控件样式细节,避免把本轮任务扩大成完整主题系统
  • IThemeComponentIKeybindingComponent 对称,第三方组件可以选择接入并自行向内部子组件转发,Runtime 不需要用反射猜测组件结构

本次当前更好的点:

  • C# 侧主题是 Runtime 实例级状态,不是模块级全局对象,多 Runtime 和测试场景可以隔离不同主题
  • 已挂载 overlay 和动态添加的容器子树会收到同一个主题对象,避免浮层和后续加入组件继续使用默认主题
  • 主题切换路径不会重建 keybinding registry,测试覆盖了切换主题后共享快捷键仍然生效

本次通用复合组件子树枚举推进:

  • 新增 ICompositeComponent,让复合组件可以通过 GetChildren() 暴露直接子组件,Runtime 不再需要硬编码认识每一种内置复合组件
  • ContainerBox 实现统一子组件枚举契约,保留现有 Container.ChildAdded / ChildRemoved 动态事件作为可变容器专属能力
  • TuiRuntime.MountRuntimeComponentTreeUnmountRuntimeComponentTree 改为通过 ICompositeComponent 递归,keybinding、theme、invalidate 和动态挂载上下文共用同一条子树遍历路径
  • 新增第三方复合组件回归测试,覆盖 Runtime 根组件添加、overlay 添加、已挂载 Container 动态添加后,内部 Input 和主题组件都能收到 Runtime 共享上下文

本次为什么这样做:

  • 对比 tmp/tui/src/tui.tstmp/tui/src/components/box.ts,参考实现的 Container / Box 都以 children 作为复合组件结构来源;C# 侧无法依赖 TS 的公开字段约定,因此用显式接口把这个能力稳定成组件契约
  • 动态增删事件仍只保留在 Container,是因为本轮目标是统一“如何枚举子树”,不是扩展所有复合组件的可变生命周期协议,避免把 Box 或第三方组件强行要求成可变容器
  • Runtime 挂载上下文本来已经负责 keybinding 和 theme 注入,本轮只替换递归来源,保持现有根树、overlay 和动态容器行为不退化

本次当前更好的点:

  • 第三方复合组件只要实现 ICompositeComponent,内部子树就能自动拿到 Runtime 级 keybinding 和 theme,不再需要自己实现 IKeybindingComponentIThemeComponent 转发
  • Runtime 的子树遍历入口收敛为一个 helper,后续 invalidate、生命周期、dispose 或更多上下文注入可以复用同一个契约,减少继续追加 Container / Box 分支的风险
  • C# 侧把“可枚举子树”和“动态子组件事件”拆开,静态复合组件可以轻量接入,动态容器仍能保留已有挂载计数和事件订阅语义

本次 Runtime 组件生命周期上下文推进:

  • 新增 TuiRuntimeContext,把 Runtime、共享 KeybindingRegistry、共享 ITuiTheme、当前 ITerminalSessionTerminalSize、Kitty 协议状态和 Runtime 退出 CancellationToken 收敛成组件可接收的上下文快照
  • 新增 IRuntimeContextComponent,组件可以通过 OnMountedOnUnmountedOnRuntimeContextChanged 感知首次挂载、最后卸载和 Runtime 上下文变化
  • TuiRuntime.MountRuntimeComponentTreeUnmountRuntimeComponentTree 继续沿用挂载计数,同一组件经根树和 overlay 多路径挂载时只在首次挂载触发 OnMounted,最后一次卸载触发 OnUnmounted
  • keybinding 和 theme 的旧接口注入改为由统一 Runtime 上下文路径驱动,IKeybindingComponentIThemeComponent 仍保持兼容,不要求现有组件立即迁移到新生命周期接口
  • SetTheme、终端 resize、StopDispose 会通知仍挂载组件 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 就提前触发卸载或取消仍在根树中的组件

本次可变复合组件动态契约推进:

  • 新增 IMutableCompositeComponentCompositeComponentChildChangedEventArgs,把动态子组件 ChildAdded / ChildRemovedContainer 专属能力抽成通用复合组件契约
  • 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(),因此 ContainerBox 和第三方复合组件都走同一套焦点清理逻辑
  • 新增 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 接管组件释放责任
  • AddShowOverlay 保持不释放用户组件的默认语义,避免调用方仍持有或准备复用的组件在移除后被 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 的最后卸载语义一致,避免多路径挂载时某一路移除导致资源提前释放
  • 释放递归复用 ICompositeComponentIMutableCompositeComponent,让第三方复合组件只要实现现有契约就能被 owned 生命周期覆盖,不需要 Runtime 继续硬编码具体组件类型

本次当前更好的点:

  • C# 侧显式区分“进入 Runtime 生命周期”和“Runtime 拥有释放责任”,比隐式 dispose 所有 IDisposable 用户组件更安全
  • owned 释放覆盖动态子组件和第三方复合组件子树,能力范围比参考实现只递归 ContainercontainsComponent 判断更通用
  • 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 移植总结:

  • 新增 SettingItemSettingsList,设置项保留 IdLabelDescriptionCurrentValue、可循环 Values 和同步 submenu 工厂
  • SettingsList 接入 IInputComponentIFocusableComponentIKeybindingComponentIThemeComponentIMutableCompositeComponent,主列表处理标准化输入,激活 submenu 后把输入完全委托给子组件
  • 新增 FuzzyMatcher / FuzzyMatch,按 tmp/tui/src/fuzzy.ts 的顺序匹配、连续奖励、边界奖励、间隔惩罚和字母数字换序兼容实现搜索过滤
  • ITuiTheme 补充 SettingDescription,描述行使用现有 ANSI 感知 TerminalTextMeasurer.Wrap 换行,不额外引入完整主题分区系统
  • 新增 SettingsListTests 覆盖渲染与描述、搜索过滤、值循环、取消、submenu 输入转发和 done 后恢复选择

本次为什么这样做:

  • 对比 tmp/tui/src/components/settings-list.tsC# 侧没有把搜索实现为嵌套 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 过滤,后续如果要匹配 IdCurrentValueDescription,需要明确排序权重
  • SettingsList 自身没有引入布局缓存,复杂列表大量刷新时仍依赖后续统一 cache key 设计

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

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

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

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

本次增量已完成:

  • 新增 TinyTUI.Autocomplete 模块,定义 IAutocompleteProviderAutocompleteRequestAutocompleteSuggestionsAutocompleteApplyContextAutocompleteApplyResult,先把“查询候选”和“应用候选”从 Editor 中拆成可复用契约
  • 新增 AutocompleteProviderBase,提供默认的前缀替换实现,后续文件路径、变量、命令参数等 provider 可以只覆盖查询或特殊应用逻辑
  • 新增 SlashCommandAutocompleteProviderSlashCommand,支持行首 / 命令名候选,确认后自动写回 /{command} 并把光标放到参数位置
  • 新增 FilePathAutocompleteProvider,支持普通路径和 @ 附件路径补全,覆盖 ./..//~/、含路径分隔符的 token、quoted path 和路径含空格自动加引号
  • 新增 CompositeAutocompleteProvider,按顺序组合 slash command、文件路径或后续自定义 provider,让 Editor 仍只依赖一个 IAutocompleteProvider
  • SlashCommand 新增参数补全入口,支持 /command argPrefix 场景调用命令自己的补全回调,返回候选时只把 argPrefix 作为待替换前缀
  • Editor 增加 AutocompleteProviderAutocompleteListIsAutocompleteActiveStartAutocompleteConfirmAutocompleteCancelAutocompleteRenderAutocomplete,补全列表复用现有 SelectList 的选择、滚动和渲染能力
  • 新增 tui.autocomplete.triggertui.autocomplete.confirmtui.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 负责判断当前 tokenResolveSearchScope 负责把显示路径映射到真实目录,ApplyCompletion 负责目录不补空格、文件补空格和 closing quote 去重
  • 如果要同时启用命令和路径补全,看 Autocomplete/CompositeAutocompleteProvider.cs,provider 顺序就是优先级,先返回候选的 provider 会负责后续确认应用
  • 最后看 Components/Editor/Editor.Autocomplete.cs,这里是 Editor 的补全状态机:启动查询、用 SelectList 展示候选、确认应用、取消清理
  • Components/Editor/Editor.csHandleInput 入口可以看到补全输入优先级:活动补全先消费导航和确认,普通 Tab 强制触发补全,普通文本输入后尝试自然补全

对比 tmp/tui/src/autocomplete.tstmp/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 使用 CancellationTokenValueTask 预留异步 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.tstmp/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 当作可见文本导致溢出或切坏协议序列
  • DifferentialRendererFullScreenRenderer 增加 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.tstmp/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 残余断言完全迁移到 ComponentRegressionTestsDifferentialRendererTests,补齐 SelectList focused marker、Spacer 负数 clamp、TruncatedText padding/首行截断、CancellableLoader 取消、PNG dimensions、长 iTerm2 image line overflow bypass
  • ttui.slnx 移除 test/TinyTUI.TextCheckstest/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 私有状态,能观察 ClearScreenMoveCursorToHideCursor、Kitty cleanup 等真实输出
  • VirtualTerminalOutput 同时保留完整 buffer 和 write 片段,既能断言最终序列,也能检查删除图像必须早于新内容写入这类顺序问题
  • 正式测试项目已覆盖 text/input/overlay/rendering/components 五个基础面,后续新增回归时有明确目录,不需要继续把所有断言塞进单个 Program.cs
  • 相比 console check 手写 AssertTrue,xUnit 测试按模块命名,失败时能直接定位到文本、组件或 renderer 的具体行为
  • 现有 C# 写法用 collection expression、target-typed newusing var 保持测试输入短小,断言结构更接近被测行为本身

后续仍需补齐:

  • 还没有像 tmp/tui/test/virtual-terminal.ts 那样基于真实终端 emulator 解析 ANSI 后得到 viewport、scrollback、cell style 和 cursor position;当前只能断言写出的序列,不能验证终端最终屏幕状态
  • TextChecksComponentChecks 已删除,后续新增回归应直接进入 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.mdpackage.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-changesdotnet 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 文本
  • PackageProjectUrlRepositoryUrl 先使用占位仓库地址,真实发布前需要替换成实际远端
  • 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 基本稳定后再做