Files
rustui-playground/TERMINAL_ROADMAP.md
zhangheng 511b212334 docs(terminal): mark P1 done in roadmap + document stty control bytes
Ctrl-C/Ctrl-Z/Ctrl-\ reach the PTY as 0x03/0x1a/0x1c and are then
interpreted by bash's stty (SIGINT/SIGTSTP/SIGQUIT), which is real-terminal
behavior and preserved on purpose per user confirmation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:38:53 +08:00

146 lines
8.5 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.

# Terminal Roadmap — 对照 xterm.js 的追平计划
本文档记录 `base-path-demo` 浏览器终端**当前已实现的能力**、**相比 xterm.js 仍缺失的功能**,以及每一项**计划的实现方式与优先级**,供后续开发接手。
> 定位提醒:本项目目标不是复刻 xterm.js 这个通用 JS 终端库,而是按 [TERMINAL_HANDOFF.md](TERMINAL_HANDOFF.md) 的方向,用 `Axum + portable-pty`(后端)+ `Leptos + Rust/WASM`(前端)沉淀一套**可复用的 Rust 终端组件**。因此追平的重点是「让真实终端会话好用」,而非铺平 xterm.js 的全部 API 面。
---
## 1. 覆盖度自评(截至本文档)
按用途粗估:
- **「跑通真实终端会话」核心用途**:约 5060%
- **xterm.js 完整 API / 特性面**:约 1520%
差距集中在**输入完整性**、**API/生态**、**可配置性**,而非「能不能用」。
### 已实现(有代码 + 单元测试)
| 能力 | 位置 |
|---|---|
| PTY 会话 + WebSocket 双向流 | `server/src/terminal/{pty_session,ws}.rs` |
| ANSI 解析vt100 crate颜色 / 粗体 / 斜体 / 下划线 / 反色、CSI 光标控制、擦除、滚动区 | `app/src/terminal/core.rs` |
| 256 色 + RGB 真彩 | `core.rs::indexed_color` / `resolve_*` |
| alt-screenvim / btm / htop含历史隔离 | `core.rs::process` |
| scrollback + 虚拟滚动(万行不卡,绝对定位) | `core.rs::collect_window` / `component.rs::virtual_window` |
| 宽字符 / 组合字符网格对齐 | `core.rs` `TerminalSegment.cols` |
| resize / reflow | `core.rs::resize` |
| `clear`(含 `ESC[3J` 清 scrollback | `core.rs::contains_erase_scrollback` |
| alt-screen 滚轮 → 方向键翻页 | `component.rs::handle_wheel` |
| 基础按键Ctrl-C/D/L、方向键、Enter/Tab/Esc/Backspace | `component.rs::map_key_to_terminal_input` |
| 完整键表F1F12/Home/End/PageUp/Down/Ctrl AZ/Alt-key | `component.rs::key_to_bytes` |
| application-cursor 模式DECCKM方向键切 SS3 | `component.rs::cursor_seq` + `TerminalCore::application_cursor` |
| 粘贴Ctrl-V / 浏览器菜单,含 bracketed paste | `component.rs::handle_paste` / `prepare_paste` |
| 文本选区 + 复制自建模型Ctrl-Shift-C跨滚动 | `component.rs` 鼠标处理 / `core.rs::selection_text` |
---
## 2. 追平项(按优先级)
### P0 — 复制 / 粘贴 + 文本选区 ⭐ ✅ 已完成
**现状**:粘贴、选区、复制**全部完成并验证通过**。
**已实现**
1. **粘贴**
- `on:paste``ClipboardEvent`)读 `clipboard_data().get_data("text")`,归一换行为 `\r` 后作为 `Input` 发送。
- bracketed paste应用开启时`core.bracketed_paste()`)包进 `ESC[200~ … ESC[201~`
- 实现于 `component.rs::handle_paste` + 纯函数 `prepare_paste`
- 右键走浏览器默认菜单避免依赖剪贴板读权限Ctrl-V 为主路径。
2. **选区 + 复制**自建选区模型xterm.js 风格)
- 选区按**逻辑行列坐标**跟踪(`Selection { anchor, head }`),鼠标 `mousedown/move/up` 拖拽更新;`point_to_cell` 做像素→单元格换算(含 padding/scroll
- 高亮 overlay`selection_rects` 算每行矩形,渲染在虚拟滚动层内(共享 translateY跟随滚动不拆分文本段。
- 复制取 `TerminalCore::selection_text(start, end)` —— 直接从 `history_rows + screen_cache` 数据模型按行列取文本,**跨滚动、输出流动时都正确**(不依赖 DOM
- **Ctrl-Shift-C** 复制Ctrl-C 保持 SIGINT打字清除选区。
- 实现于 `component.rs``Selection`/`selection_rects`/`point_to_cell`/鼠标处理)+ `core.rs::selection_text` + `core.rs::TerminalRow::text_in_cols`,均有单元测试。
3. **快捷键**Ctrl-Shift-C 复制 / Ctrl-V 粘贴。
**已知后续项(非阻塞)**:拖拽到视窗边缘时**没有自动滚动延伸选区**(需先滚到位再选);行内 wide+组合字符混排的列切片为近似。
---
### P1 — 完整功能键 + application-cursor 模式 ✅ 已完成
**现状**:完整键表已实现并验证通过(`key_to_bytes_table` 单测覆盖)。
**已实现**
1. **完整键表**`component.rs::key_to_bytes`
- F1F4 SS3`ESC O P/Q/R/S`、F5F12 CSI `~`(含 xterm 真实的 16/22 跳号)。
- Home/End、Insert/Delete、PageUp/PageDown、Enter/Tab/Esc/Backspace。
- Ctrl A..Z → 0x01..0x1a;以及 `@[\\]^_` 变体(→ 0x00/0x1b/0x1c/0x1d/0x1e/0x1f
- Alt+char → `ESC char`(标准 meta-as-ESC 编码)。
2. **application-cursor 模式DECCKM**`cursor_seq(byte, app_cursor)` 助手;为真时方向键 / Home / End 发 SS3`ESC O X`),否则 CSI`ESC [ X`)。`handle_keydown``handle_wheel` 都据此切换。
3. **测试**`key_to_bytes_table` 单测覆盖整套键表17 个 terminal 测试全过、clippy 干净。
**有意保留的"看起来像 bug"行为**(与真终端/xterm 一致):
- `Ctrl-C` (0x03) → SIGINT 中断前台进程
- `Ctrl-Z` (0x1a) → SIGTSTP 暂停前台进程shell 提示符立刻返回)
- `Ctrl-\` (0x1c) → SIGQUIT 杀掉前台进程shell 提示符返回)
这些是 bash 等的 `stty` 默认行为intr/quit/susp由内核 tty 纪律触发,与 xterm/SSH 进真终端一样——不是本组件 bug也按用户确认保留。
---
### P2 — 鼠标上报mouse reporting
**现状**:没有。点击 / 拖拽位置不会发给应用vim 鼠标、tmux 选区、btm 点击都用不了。
**计划实现**
1. vt100 已解析鼠标模式:`screen().mouse_protocol_mode()`None/Press/PressRelease/ButtonMotion/AnyMotion`mouse_protocol_encoding()`Default/Utf8/Sgr
2.`terminal-screen` 上监听 `on:mousedown` / `on:mouseup` / `on:mousemove`
- 把像素坐标换算成终端 **行列**(用已测量的 cell 宽高 + 容器 rect
- 按当前 encoding 编码:
- **SGR**(最常用):`ESC[<b;col;row;M`(按下)/ `m`(释放)。
- Default`ESC[M` + 三字节。
- 只在 `mouse_protocol_mode != None` 时拦截,否则放行(不破坏选区/原生行为)。
3. 与 P0 选区冲突处理:应用开启鼠标上报时,鼠标事件优先发给应用;否则用于本地选区。
**风险**:中。坐标换算边界 + 与选区的优先级仲裁需要仔细测。
---
### P3 — 中文 / IME 输入
**现状**`map_key_to_terminal_input` 只处理 `key.chars().count() == 1` 的单字符IME 组合输入(拼音候选)完全没接。
**计划实现**
1. 改用 **`composition` 事件 + 隐藏 `<textarea>`** 的标准方案xterm.js 也是这套):
- 一个 1×1 透明、跟随光标的 `textarea` 承接 IME。
- 监听 `compositionstart` / `compositionupdate` / `compositionend`
- 组合进行中不发字节;`compositionend` 时把最终文本一次性作为 Input 发送。
- 普通 `input` 事件(非组合)也走这个 textarea替代部分 `keydown` 逐字符逻辑。
2. 光标定位:把隐藏 textarea 定位到终端光标处,让候选框出现在正确位置。
**风险**中高。事件模型要从「keydown 逐键」迁一部分到「textarea + composition」需保证不回归现有按键。建议**先把 P1 功能键做完再动这块**,避免两套输入逻辑互相打架。
---
## 3. 其余缺口(暂不排期,记录备查)
xterm.js 还有这些,本项目暂列「按需再做」:
- **链接检测**(点 URL 打开)、**搜索 / 查找**Ctrl-F 高亮)
- **光标样式**:块 / 竖线 / 下划线 + 闪烁(目前固定块状反色)
- **Canvas / WebGL 渲染器**:当前是 DOM 渲染,够用但不如 Canvas 快;超大终端 + 高刷新率场景才需要
- **sixel / 图片协议**、**字形连字控制**
- **Unicode 11/15 宽度表**:目前依赖 vt100 的宽度判断,极端 emoji / 新字符可能不准
- **无障碍**(屏幕阅读器 live region
- **公开 API / 插件系统 / 配置项**:当前是项目内定制组件,无对外 API
---
## 4. 建议推进顺序
```
P0 复制粘贴(先粘贴→可见区复制) ← 最先,体验提升最大
P1 完整功能键 + application-cursor
P2 鼠标上报
P3 中文 IME(放在功能键之后,避免输入逻辑冲突)
```
每一项都应延续现有做法:**纯逻辑抽成可单测的函数**(如 `map_key_to_terminal_input``wheel_delta_to_rows``virtual_window`),用 `cargo test -p app --lib terminal::` 锁定,浏览器只做交互验证。