feat: initialize interfaces and model

This commit is contained in:
chuan
2026-06-03 21:35:48 +08:00
Unverified
parent ca09cf7273
commit 49e368de72
16 changed files with 386 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
- 默认使用最新的C#语法糖简化代码的写法
- C#的summary注释使用中文,并且行尾不添加类似 `。` `` 之类的中文符号
+65
View File
@@ -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. 补齐测试和基准,覆盖输入解析、宽度计算、渲染输出和组件行为。
+58
View File
@@ -0,0 +1,58 @@
namespace TinyTUI.Components;
/// <summary>
/// 按添加顺序纵向渲染子组件的容器组件
/// </summary>
public class Container : IComponent
{
/// <summary>
/// 获取当前子组件列表
/// </summary>
public List<IComponent> Children { get; } = [];
/// <summary>
/// 向容器添加子组件
/// </summary>
public void Add(IComponent component)
{
Children.Add(component);
}
/// <summary>
/// 从容器移除子组件
/// </summary>
public void Remove(IComponent component)
{
Children.Remove(component);
}
/// <summary>
/// 移除容器中的所有子组件
/// </summary>
public void Clear()
{
Children.Clear();
}
/// <inheritdoc />
public virtual IReadOnlyList<string> Render(int width)
{
var lines = new List<string>();
foreach (var child in Children)
{
lines.AddRange(child.Render(width));
}
return lines;
}
/// <inheritdoc />
public virtual void Invalidate()
{
foreach (var child in Children)
{
child.Invalidate();
}
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace TinyTUI.Components;
/// <summary>
/// 定义 TUI 组件树中的最小可渲染单元
/// </summary>
public interface IComponent
{
/// <summary>
/// 按给定视口宽度将组件渲染为终端行
/// </summary>
IReadOnlyList<string> Render(int width);
/// <summary>
/// 清除组件持有的渲染缓存状态
/// </summary>
void Invalidate() { }
}
+14
View File
@@ -0,0 +1,14 @@
using TinyTUI.Input;
namespace TinyTUI.Components;
/// <summary>
/// 定义聚焦后可以接收标准化输入事件的组件
/// </summary>
public interface IInputComponent : IComponent
{
/// <summary>
/// 处理运行时分发的标准化输入事件
/// </summary>
void HandleInput(TuiInputEvent input);
}
+12
View File
@@ -0,0 +1,12 @@
namespace TinyTUI.Input;
/// <summary>
/// 将终端原始输入数据转换为标准化 TUI 输入事件
/// </summary>
public interface IInputParser
{
/// <summary>
/// 将一段原始输入解析为零个或多个标准化输入事件
/// </summary>
IReadOnlyList<TuiInputEvent> Parse(string data);
}
+32
View File
@@ -0,0 +1,32 @@
namespace TinyTUI.Input;
/// <summary>
/// 标识 TUI 运行时消费的高级输入事件类型
/// </summary>
public enum TuiInputEventKind
{
/// <summary>
/// 可打印文本输入
/// </summary>
Text,
/// <summary>
/// 非文本按键或组合键
/// </summary>
Key,
/// <summary>
/// 成组的粘贴输入
/// </summary>
Paste,
/// <summary>
/// 终端尺寸变化输入事件
/// </summary>
Resize,
}
/// <summary>
/// 表示原始终端数据解析后的标准化输入事件
/// </summary>
public sealed record TuiInputEvent(TuiInputEventKind Kind, string Value);
+17
View File
@@ -0,0 +1,17 @@
namespace TinyTUI.Overlay;
/// <summary>
/// 控制已显示 overlay 的生命周期和可见性
/// </summary>
public interface IOverlayHandle
{
/// <summary>
/// 永久隐藏并移除 overlay
/// </summary>
void Hide();
/// <summary>
/// 临时切换 overlay 是否参与渲染和焦点处理
/// </summary>
void SetHidden(bool hidden);
}
+24
View File
@@ -0,0 +1,24 @@
using TinyTUI.Components;
namespace TinyTUI.Overlay;
/// <summary>
/// 管理渲染在基础组件树之上的临时组件
/// </summary>
public interface IOverlayManager
{
/// <summary>
/// 获取当前是否存在至少一个 overlay
/// </summary>
bool HasOverlay { get; }
/// <summary>
/// 显示 overlay 组件并返回控制句柄
/// </summary>
IOverlayHandle Show(IComponent component);
/// <summary>
/// 隐藏最上层 overlay
/// </summary>
void HideTop();
}
+17
View File
@@ -0,0 +1,17 @@
namespace TinyTUI.Rendering;
/// <summary>
/// 将渲染后的终端行同步到终端输出
/// </summary>
public interface IRenderer
{
/// <summary>
/// 将给定的逻辑终端行渲染到当前终端视口
/// </summary>
void Render(IReadOnlyList<string> lines, TerminalSize size);
/// <summary>
/// 清除渲染器状态 让下一次渲染从干净基线开始
/// </summary>
void Reset();
}
+39
View File
@@ -0,0 +1,39 @@
using TinyTUI.Components;
namespace TinyTUI.Runtime;
/// <summary>
/// 协调组件状态 输入分发 渲染和终端生命周期
/// </summary>
public interface ITuiRuntime : IDisposable
{
/// <summary>
/// 向根组件树添加组件
/// </summary>
void Add(IComponent component);
/// <summary>
/// 从根组件树移除组件
/// </summary>
void Remove(IComponent component);
/// <summary>
/// 设置接收输入事件的组件
/// </summary>
void SetFocus(IComponent? component);
/// <summary>
/// 请求一次渲染
/// </summary>
void RequestRender();
/// <summary>
/// 启动 TUI 运行时
/// </summary>
void Start();
/// <summary>
/// 停止 TUI 运行时并恢复终端状态
/// </summary>
void Stop();
}
+32
View File
@@ -0,0 +1,32 @@
namespace TinyTUI.Stdio;
/// <summary>
/// 提供终端原始输入 尺寸变化通知和输入生命周期控制
/// </summary>
public interface ITerminalInput : IDisposable
{
/// <summary>
/// 获取最近一次记录的终端视口尺寸
/// </summary>
TerminalSize CurrentSize { get; }
/// <summary>
/// 在终端收到原始输入数据时触发
/// </summary>
event EventHandler<string>? DataReceived;
/// <summary>
/// 在终端视口尺寸变化时触发
/// </summary>
event EventHandler<TerminalSize>? Resized;
/// <summary>
/// 开始读取终端输入和尺寸变化事件
/// </summary>
void Start();
/// <summary>
/// 停止读取终端输入并恢复输入相关的终端状态
/// </summary>
void Stop();
}
+32
View File
@@ -0,0 +1,32 @@
namespace TinyTUI.Stdout;
/// <summary>
/// 提供渲染器和运行时使用的终端输出操作
/// </summary>
public interface ITerminalOutput
{
/// <summary>
/// 向输出流写入原始文本或终端控制序列
/// </summary>
void Write(string value);
/// <summary>
/// 将缓冲的输出刷新到终端
/// </summary>
void Flush();
/// <summary>
/// 清空可见终端屏幕
/// </summary>
void ClearScreen();
/// <summary>
/// 隐藏硬件光标
/// </summary>
void HideCursor();
/// <summary>
/// 显示硬件光标
/// </summary>
void ShowCursor();
}
+6
View File
@@ -0,0 +1,6 @@
namespace TinyTUI;
/// <summary>
/// 表示当前终端视口的字符单元尺寸
/// </summary>
public readonly record struct TerminalSize(int Columns, int Rows);
+17
View File
@@ -0,0 +1,17 @@
namespace TinyTUI.Text;
/// <summary>
/// 按终端字符单元宽度规则测量和裁剪字符串
/// </summary>
public interface ITextMeasurer
{
/// <summary>
/// 获取字符串占用的终端字符单元数量
/// </summary>
int GetWidth(string value);
/// <summary>
/// 裁剪字符串使其渲染宽度不超过给定字符单元宽度
/// </summary>
string Truncate(string value, int maxWidth);
}
+2
View File
@@ -1,9 +1,11 @@
<Solution>
<Folder Name="/_/">
<File Path=".gitignore" />
<File Path="AGENTS.md" />
<File Path="Directory.Build.props" />
<File Path="nuget.config" />
<File Path="README.md" />
<File Path="TODO.md" />
</Folder>
<Project Path="src/Example/Example.csproj" Id="6656c1ec-b220-4e7a-8875-5a97a85afbc1" />
<Project Path="src/TinyTUI/TinyTUI.csproj" Id="f39199aa-6947-4951-b952-a1588045505e" />