CLAUDE.md@packages/tui · git:20260817.7056cd2 · 2026-08-17 · sha256 e4649ce838ac241b
CLAUDE.md@packages/tui git:20260817.7056cd2A
Immutable. This exact content never changes and is served at /api/v1/blob/e4649ce838ac241b.
# @c4a/tui
基于 Ink 5.x 的终端 UI 共享组件库,供 `dev-cli` 使用。
## Ink stdin 生命周期陷阱(必须遵守)
Ink 在 unmount 时会对 `process.stdin` 执行以下操作:
1. `stdin.setRawMode(false)` — 关闭 raw mode
2. `stdin.removeListener("readable", ...)` — 移除数据监听
3. **`stdin.unref()`** — 取消事件循环引用
第 3 步会导致:Ink 退出后如果立即使用 `readline.createInterface` 读取用户输入,Node.js 事件循环因无活跃 ref handle 而**直接退出进程**,readline 来不及等待输入。
**任何在 Ink render/unmount 之后需要读取 stdin 的场景,都必须先恢复 stdin 状态:**
```typescript
function resetStdin(): void {
const { stdin } = process;
if (stdin.isTTY) {
try {
stdin.setRawMode(false);
} catch {
// ignore
}
}
if (typeof stdin.ref === "function") {
stdin.ref(); // 重新引用,防止事件循环退出
}
if (stdin.isPaused()) {
stdin.resume(); // 恢复流
}
// 清理 Ink 遗留的 listener
stdin.removeAllListeners("readable");
stdin.removeAllListeners("data");
}
```
**调用时机**:在 `inkRender` 返回的 `waitUntilExit()` resolve 之后、执行任何 readline 操作之前。
典型模式:
```typescript
const selection = await showMenu(); // 内部 inkRender + waitUntilExit
resetStdin(); // 必须在此恢复
await executeCommand(selection); // 内部可安全使用 readline
```
## Unicode 宽字符与 Ink 布局(必须遵守)
Ink 的底层布局引擎(Yoga)按**字符数**计算宽度,不感知终端实际列宽。以下字符在终端中占 2 列,但 Yoga 只算 1 列:
- CJK 汉字(charCode > 0x7F 的大部分字符)
- 特殊符号如 `▶`(U+25B6)、`→`(U+2192)等
这会导致:将宽字符和普通空格写在同一个 `<Text>` 中,选中/未选中状态切换时文字左右跳动。
**错误做法**:将指示符和标签放在同一个 Text 中
```tsx
// ❌ "▶ " 在终端占 3 列,但 Yoga 认为占 2 列
// " " 在终端和 Yoga 中都占 3 列 → 选中时文字向左偏移 1 字符
<Text>
{selected ? "▶ " : " "}
{label}
</Text>
```
**正确做法**:将指示符放入固定宽度的独立 Box
```tsx
// ✅ Box 的 width 由 Yoga 强制固定,内容不影响布局
<Box>
<Box width={3}>
<Text>{selected ? "▶" : " "}</Text>
</Box>
<Text>{label}</Text>
</Box>
```
**CJK 文本宽度计算**:在需要手动对齐(如 padEnd)时,必须按终端列宽计算:
```typescript
function displayWidth(str: string): number {
let w = 0;
for (const ch of str) {
w += ch.charCodeAt(0) > 0x7f ? 2 : 1;
}
return w;
}
```