diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7ef7902 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,2 @@ +- 默认使用最新的C#语法糖简化代码的写法 +- C#的summary注释使用中文,并且行尾不添加类似 `。` `,` 之类的中文符号 diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..491a8de --- /dev/null +++ b/TODO.md @@ -0,0 +1,65 @@ +# TODO + +## Success Criteria + +最终成功应该具备这些能力: + +- 能作为一个 C# TUI 类库被其他控制台程序引用。 +- 能启动和停止终端运行时,并在退出时恢复终端状态。 +- 能读取键盘输入,并把普通字符、方向键、组合键、回车、退格、Esc 等转换成统一事件。 +- 能处理粘贴输入,避免大段粘贴被拆成混乱的按键事件。 +- 能监听终端窗口尺寸变化,并触发重新布局和重新渲染。 +- 能用组件树描述界面,而不是让组件直接写控制台。 +- 能支持焦点管理,让当前焦点组件接收输入。 +- 能渲染基础组件,包括文本、容器、单行输入框、多行编辑器、选择列表和加载状态。 +- 能支持 overlay 弹层,用于菜单、弹窗、提示框和临时输入。 +- 能正确显示中文、emoji、ANSI 样式文本,并避免宽度计算错位。 +- 能进行全量渲染,保证最小版本可以稳定显示。 +- 能进行差分渲染,只刷新变化的行,减少闪烁。 +- 能提供一组示例程序,展示输入、编辑、列表选择、overlay 和动态刷新。 +- 能提供测试覆盖核心模块,包括输入解析、宽度计算、渲染输出和组件行为。 +- 能在 Windows Terminal / PowerShell 环境下稳定运行。 + +## Architecture + +整体分成几个大模块: + +- STDIO 模块:负责读取键盘输入、监听窗口尺寸变化、管理 raw mode 和生命周期。 +- STDOUT 模块:负责向终端输出文本和 ANSI 控制序列。 +- Input 模块:负责把原始输入转换成统一的按键、粘贴、控制事件。 +- Runtime 模块:负责管理组件树、焦点、事件分发和渲染调度。 +- Renderer 模块:负责把组件输出同步到终端,先全量渲染,后续升级成差分渲染。 +- Text Width 模块:负责计算终端显示宽度,处理 ANSI、中文、emoji、截断和换行。 +- Component 模块:负责提供 Text、Input、Container、Box 等基础 UI 组件。 +- Overlay 模块:负责弹层显示、遮盖合成、焦点切换和恢复。 +- Example 模块:负责验证框架 API 和真实交互流程。 +- Test 模块:负责覆盖输入解析、渲染输出、宽度计算和组件行为。 + +大致依赖方向: + +```text +Example + -> Runtime + -> Component + -> Overlay + -> Renderer + -> STDOUT + -> Text Width + -> Input + -> STDIO +``` + +## Steps + +1. 定义核心模块边界:STDIO/STDOUT、输入解析、运行时、渲染器、组件、示例。 +2. 实现终端 IO 模块,统一封装控制台输入、输出、尺寸和生命周期。 +3. 实现输入解析模块,把原始按键和控制序列转换成框架事件。 +4. 实现组件运行时,管理组件树、焦点、事件分发和渲染请求。 +5. 实现基础渲染器,先完成全量渲染和清屏重画。 +6. 实现文本宽度模块,统一处理 ANSI、中文、emoji 和截断。 +7. 实现差分渲染器,只更新变化的终端行。 +8. 实现基础组件集,包括 Text、Input、Container 和 Box。 +9. 实现 Overlay 模块,支持弹层显示、遮盖合成和焦点切换。 +10. 实现高级组件集,包括 Editor、SelectList、Markdown 和 Loader。 +11. 完善示例程序,用真实场景验证输入、渲染、焦点和 overlay。 +12. 补齐测试和基准,覆盖输入解析、宽度计算、渲染输出和组件行为。 diff --git a/src/TinyTUI/Components/Container.cs b/src/TinyTUI/Components/Container.cs new file mode 100644 index 0000000..379737c --- /dev/null +++ b/src/TinyTUI/Components/Container.cs @@ -0,0 +1,58 @@ +namespace TinyTUI.Components; + +/// +/// 按添加顺序纵向渲染子组件的容器组件 +/// +public class Container : IComponent +{ + /// + /// 获取当前子组件列表 + /// + public List Children { get; } = []; + + /// + /// 向容器添加子组件 + /// + public void Add(IComponent component) + { + Children.Add(component); + } + + /// + /// 从容器移除子组件 + /// + public void Remove(IComponent component) + { + Children.Remove(component); + } + + /// + /// 移除容器中的所有子组件 + /// + public void Clear() + { + Children.Clear(); + } + + /// + public virtual IReadOnlyList Render(int width) + { + var lines = new List(); + + foreach (var child in Children) + { + lines.AddRange(child.Render(width)); + } + + return lines; + } + + /// + public virtual void Invalidate() + { + foreach (var child in Children) + { + child.Invalidate(); + } + } +} diff --git a/src/TinyTUI/Components/IComponent.cs b/src/TinyTUI/Components/IComponent.cs new file mode 100644 index 0000000..3d2f406 --- /dev/null +++ b/src/TinyTUI/Components/IComponent.cs @@ -0,0 +1,17 @@ +namespace TinyTUI.Components; + +/// +/// 定义 TUI 组件树中的最小可渲染单元 +/// +public interface IComponent +{ + /// + /// 按给定视口宽度将组件渲染为终端行 + /// + IReadOnlyList Render(int width); + + /// + /// 清除组件持有的渲染缓存状态 + /// + void Invalidate() { } +} diff --git a/src/TinyTUI/Components/IInputComponent.cs b/src/TinyTUI/Components/IInputComponent.cs new file mode 100644 index 0000000..8680034 --- /dev/null +++ b/src/TinyTUI/Components/IInputComponent.cs @@ -0,0 +1,14 @@ +using TinyTUI.Input; + +namespace TinyTUI.Components; + +/// +/// 定义聚焦后可以接收标准化输入事件的组件 +/// +public interface IInputComponent : IComponent +{ + /// + /// 处理运行时分发的标准化输入事件 + /// + void HandleInput(TuiInputEvent input); +} diff --git a/src/TinyTUI/Input/IInputParser.cs b/src/TinyTUI/Input/IInputParser.cs new file mode 100644 index 0000000..c61b9f8 --- /dev/null +++ b/src/TinyTUI/Input/IInputParser.cs @@ -0,0 +1,12 @@ +namespace TinyTUI.Input; + +/// +/// 将终端原始输入数据转换为标准化 TUI 输入事件 +/// +public interface IInputParser +{ + /// + /// 将一段原始输入解析为零个或多个标准化输入事件 + /// + IReadOnlyList Parse(string data); +} diff --git a/src/TinyTUI/Input/TuiInputEvent.cs b/src/TinyTUI/Input/TuiInputEvent.cs new file mode 100644 index 0000000..eb9aa35 --- /dev/null +++ b/src/TinyTUI/Input/TuiInputEvent.cs @@ -0,0 +1,32 @@ +namespace TinyTUI.Input; + +/// +/// 标识 TUI 运行时消费的高级输入事件类型 +/// +public enum TuiInputEventKind +{ + /// + /// 可打印文本输入 + /// + Text, + + /// + /// 非文本按键或组合键 + /// + Key, + + /// + /// 成组的粘贴输入 + /// + Paste, + + /// + /// 终端尺寸变化输入事件 + /// + Resize, +} + +/// +/// 表示原始终端数据解析后的标准化输入事件 +/// +public sealed record TuiInputEvent(TuiInputEventKind Kind, string Value); diff --git a/src/TinyTUI/Overlay/IOverlayHandle.cs b/src/TinyTUI/Overlay/IOverlayHandle.cs new file mode 100644 index 0000000..8d91c90 --- /dev/null +++ b/src/TinyTUI/Overlay/IOverlayHandle.cs @@ -0,0 +1,17 @@ +namespace TinyTUI.Overlay; + +/// +/// 控制已显示 overlay 的生命周期和可见性 +/// +public interface IOverlayHandle +{ + /// + /// 永久隐藏并移除 overlay + /// + void Hide(); + + /// + /// 临时切换 overlay 是否参与渲染和焦点处理 + /// + void SetHidden(bool hidden); +} diff --git a/src/TinyTUI/Overlay/IOverlayManager.cs b/src/TinyTUI/Overlay/IOverlayManager.cs new file mode 100644 index 0000000..bd4ff22 --- /dev/null +++ b/src/TinyTUI/Overlay/IOverlayManager.cs @@ -0,0 +1,24 @@ +using TinyTUI.Components; + +namespace TinyTUI.Overlay; + +/// +/// 管理渲染在基础组件树之上的临时组件 +/// +public interface IOverlayManager +{ + /// + /// 获取当前是否存在至少一个 overlay + /// + bool HasOverlay { get; } + + /// + /// 显示 overlay 组件并返回控制句柄 + /// + IOverlayHandle Show(IComponent component); + + /// + /// 隐藏最上层 overlay + /// + void HideTop(); +} diff --git a/src/TinyTUI/Rendering/IRenderer.cs b/src/TinyTUI/Rendering/IRenderer.cs new file mode 100644 index 0000000..a2088f1 --- /dev/null +++ b/src/TinyTUI/Rendering/IRenderer.cs @@ -0,0 +1,17 @@ +namespace TinyTUI.Rendering; + +/// +/// 将渲染后的终端行同步到终端输出 +/// +public interface IRenderer +{ + /// + /// 将给定的逻辑终端行渲染到当前终端视口 + /// + void Render(IReadOnlyList lines, TerminalSize size); + + /// + /// 清除渲染器状态 让下一次渲染从干净基线开始 + /// + void Reset(); +} diff --git a/src/TinyTUI/Runtime/ITuiRuntime.cs b/src/TinyTUI/Runtime/ITuiRuntime.cs new file mode 100644 index 0000000..1f6fa3c --- /dev/null +++ b/src/TinyTUI/Runtime/ITuiRuntime.cs @@ -0,0 +1,39 @@ +using TinyTUI.Components; + +namespace TinyTUI.Runtime; + +/// +/// 协调组件状态 输入分发 渲染和终端生命周期 +/// +public interface ITuiRuntime : IDisposable +{ + /// + /// 向根组件树添加组件 + /// + void Add(IComponent component); + + /// + /// 从根组件树移除组件 + /// + void Remove(IComponent component); + + /// + /// 设置接收输入事件的组件 + /// + void SetFocus(IComponent? component); + + /// + /// 请求一次渲染 + /// + void RequestRender(); + + /// + /// 启动 TUI 运行时 + /// + void Start(); + + /// + /// 停止 TUI 运行时并恢复终端状态 + /// + void Stop(); +} diff --git a/src/TinyTUI/Stdio/ITerminalInput.cs b/src/TinyTUI/Stdio/ITerminalInput.cs new file mode 100644 index 0000000..7b8d205 --- /dev/null +++ b/src/TinyTUI/Stdio/ITerminalInput.cs @@ -0,0 +1,32 @@ +namespace TinyTUI.Stdio; + +/// +/// 提供终端原始输入 尺寸变化通知和输入生命周期控制 +/// +public interface ITerminalInput : IDisposable +{ + /// + /// 获取最近一次记录的终端视口尺寸 + /// + TerminalSize CurrentSize { get; } + + /// + /// 在终端收到原始输入数据时触发 + /// + event EventHandler? DataReceived; + + /// + /// 在终端视口尺寸变化时触发 + /// + event EventHandler? Resized; + + /// + /// 开始读取终端输入和尺寸变化事件 + /// + void Start(); + + /// + /// 停止读取终端输入并恢复输入相关的终端状态 + /// + void Stop(); +} diff --git a/src/TinyTUI/Stdout/ITerminalOutput.cs b/src/TinyTUI/Stdout/ITerminalOutput.cs new file mode 100644 index 0000000..569b4e4 --- /dev/null +++ b/src/TinyTUI/Stdout/ITerminalOutput.cs @@ -0,0 +1,32 @@ +namespace TinyTUI.Stdout; + +/// +/// 提供渲染器和运行时使用的终端输出操作 +/// +public interface ITerminalOutput +{ + /// + /// 向输出流写入原始文本或终端控制序列 + /// + void Write(string value); + + /// + /// 将缓冲的输出刷新到终端 + /// + void Flush(); + + /// + /// 清空可见终端屏幕 + /// + void ClearScreen(); + + /// + /// 隐藏硬件光标 + /// + void HideCursor(); + + /// + /// 显示硬件光标 + /// + void ShowCursor(); +} diff --git a/src/TinyTUI/TerminalSize.cs b/src/TinyTUI/TerminalSize.cs new file mode 100644 index 0000000..7f23b37 --- /dev/null +++ b/src/TinyTUI/TerminalSize.cs @@ -0,0 +1,6 @@ +namespace TinyTUI; + +/// +/// 表示当前终端视口的字符单元尺寸 +/// +public readonly record struct TerminalSize(int Columns, int Rows); diff --git a/src/TinyTUI/Text/ITextMeasurer.cs b/src/TinyTUI/Text/ITextMeasurer.cs new file mode 100644 index 0000000..bc1a11d --- /dev/null +++ b/src/TinyTUI/Text/ITextMeasurer.cs @@ -0,0 +1,17 @@ +namespace TinyTUI.Text; + +/// +/// 按终端字符单元宽度规则测量和裁剪字符串 +/// +public interface ITextMeasurer +{ + /// + /// 获取字符串占用的终端字符单元数量 + /// + int GetWidth(string value); + + /// + /// 裁剪字符串使其渲染宽度不超过给定字符单元宽度 + /// + string Truncate(string value, int maxWidth); +} diff --git a/ttui.slnx b/ttui.slnx index e128125..358266e 100644 --- a/ttui.slnx +++ b/ttui.slnx @@ -1,9 +1,11 @@ + +