Files
rustui-playground/TERMINAL_ROADMAP.md
2026-06-24 15:31:58 +08:00

277 lines
19 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 面。
>
> **性能目标**:终端默认渲染器已切到 WebGL glyph atlas / per-cell quad保留 Canvas 2D 与 DOM renderer 作为回退。Canvas 2D 解决了大部分 DOM 节点膨胀、长输出重排和高频输出卡顿问题WebGL 路径现在按字形 atlas 缓存并批量绘制单元格 quad。
---
## 1. 覆盖度自评(截至本文档)
按用途粗估:
- **「跑通真实终端会话」核心用途**:约 **85%**
- **xterm.js 完整 API / 特性面**:约 **40%**
差距集中在 **API/插件生态、accessibility、搜索/链接、Unicode 深度精度、运行时可配置性、工程化性能基准**,而非「能不能用」。
### 已实现(有代码 + 单元测试)
| 能力 | 位置 |
|---|---|
| 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 滚轮 → 方向键翻页rAF 合并多次 wheel 事件,单次滑动最多一屏) | `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 / Ctrl-Shift-V / 右键菜单,含 bracketed paste | `component.rs::handle_paste` / `prepare_paste` |
| 文本选区 + 复制自建模型Ctrl-Shift-C / 选区存在时 Ctrl+C跨滚动 | `component.rs` 鼠标处理 / `core.rs::selection_text` |
| 鼠标上报vim/htop/tmux 鼠标SGR/Default/UTF-8 编码,滚轮) | `component.rs` 鼠标处理 / `encode_mouse_report` |
| 中文 / IME 输入(拼音/日文/韩文,隐藏 textarea + composition 事件) | `component.rs::handle_input` / `handle_composition_*` |
| WebGL glyph atlas 默认渲染器per-cell quad、DPR glyph atlas、背景/underline/cursor backdrop、Canvas/DOM 回退) | `component.rs::Renderer` / `render_webgl` / `GlyphAtlas` / `render_canvas` / `canvas_render_plan` |
---
## 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**当选区存在时 Ctrl+C 优先复制**(与 VS Code / Windows Terminal 一致),无选区时才发送 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. **浏览器快捷键仲裁**`handle_keydown` 对 bare `Ctrl+字母`(无 Shift/Alt/Meta调用 `preventDefault()`,阻止浏览器吃掉 `Ctrl-R` 刷新、`Ctrl-F` 查找、`Ctrl-A` 全选等。`Ctrl+Shift` 组合(如 `Ctrl-Shift-C` 复制)和 `Ctrl+Alt`/`Ctrl+Meta` 组合保留给浏览器/系统。
**浏览器预留键(硬限制,无法拦截)**`Ctrl-W`(关标签)、`Ctrl-T`(新标签)、`Ctrl-N`(新窗口)从 Chrome 4 起被浏览器保留,`preventDefault()` 对它们无效,浏览器在 `keydown` 到达页面前就消费了。这是所有网页终端的共同限制xterm.js 同样如此),只能在浏览器之外解决(系统全局快捷键 / 扩展 / Electron 包装。readline 退格删词可用 `Alt+Backspace` 替代 `Ctrl-W`
4. **测试**`key_to_bytes_table` 单测覆盖整套键表23 个 terminal 测试全过、clippy 干净。
**有意调整的行为**(出于浏览器终端的可用性,与原生 xterm 不同):
- `Ctrl-C` (0x03) → 无选区时发 SIGINT 中断前台进程;**有选区时优先复制**,不发送 0x03。
- `Ctrl-Z` (0x1a) 与 `Ctrl-\` (0x1c) → 被前端过滤为 `None`,不发送到 PTY。原因是这两个键在默认 `stty` 下会触发 SIGTSTP / SIGQUIT在浏览器标签页里极易误触导致前台进程被挂起或杀掉需要这两个信号的用户可用 shell 内建命令(如 `kill -STOP` / `kill -QUIT`)替代。
---
### P2 — 鼠标上报mouse reporting ✅ 已完成
**现状**:已实现并验证。`vim`/`htop`/`btm`/`tmux` 的鼠标交互、tmux 选区、vim 滚轮均可以工作。
**已实现**
1. **vt100 mouse-mode accessors**`core.rs::mouse_protocol_mode` / `mouse_protocol_encoding`)—— 暴露 `None/Press/PressRelease/ButtonMotion/AnyMotion``Default/Utf8/Sgr`
2. **事件监听与坐标换算**`component.rs`
- `on:mousedown` / `on:mouseup` / `on:mousemove` 复用现有的 DOM 事件。
- `pixel_to_grid``(clientX, clientY)` 换算成 0-based 可见网格 `(row, col)`(注意:与选区用的 `point_to_cell` 不同,这里**不加 scroll_top**,因为鼠标报告坐标是相对于应用正在绘制的当前屏幕,而非 scrollback 缓冲区)。
3. **编码**`encode_mouse_report` 纯函数)
- **SGR1006**`ESC[<b;col;row;M`(按下/移动)/ `ESC[<b;col;row;m`释放。ASCII-only任意屏幕尺寸都正确。
- **Default / UTF-8**`ESC[M` + 三字节(各 +32。坐标 ≤94 时完全正确并 ASCII-safe更大坐标被钳位到 94大多数终端 likewise现代应用都走 SGR
- **滚轮**:垂直/水平滚轮映射为按钮 64/65/66/67 的 `Press` 事件。
4. **模式仲裁**
- 应用开启 mouse reporting`mode != None`)时,鼠标事件优先发给应用;否则走本地选区。
- 按住 **Shift** 点击可绕过鼠标上报、强制本地选区xterm 行为)。
- 鼠标移动按网格单元去重,避免每像素都发报告。
5. **测试**`encode_mouse_report``pixel_to_grid``with_mouse_modifiers``mouse_button_base` 均有单测23 个 terminal 测试全过clippy 干净。
**已知限制**
- 主屏幕(非 alt-screen滚动后鼠标报告坐标仍相对于可见网格顶部因为开启鼠标上报的应用通常运行在 alt-screen这覆盖主要使用场景。
- 与浏览器 `Ctrl+W/T/N` 类似,浏览器可能保留某些鼠标手势(如右键菜单、拖拽文本),这些在网页终端内无法 100% 拦截。应用内鼠标操作应正常。
---
### P3 — 中文 / IME 输入 ✅ 已完成
**现状**:已实现。拼音、日文、韩文等 IME 组合输入可以正常工作;普通英文/数字输入也迁到了 `textarea``input` 事件,确保和 IME 走同一条路径。
**已实现**
1. **隐藏 `<textarea>` IME 载体**`component.rs` view
- 绝对定位、透明、无边框、大小跟随终端光标单元格。
- 通过 `tabindex="0"` 成为焦点目标;`terminal-screen` 改用 `:focus-within` 显示焦点环。
2. **事件模型迁移**
- `on:input`:普通字符/数字/空格输入,读取 textarea value 后清空并发送。
- `on:compositionstart` / `on:compositionend`IME 组合期间不发字节,`compositionend` 时发送最终文本。
- `on:keydown`仍处理方向键、Enter、Esc、Backspace、F 键、Ctrl 组合等**非打印键**。
- `on:paste`:从 `terminal-screen` 移到 textarea确保 focus 在 textarea 时右键/菜单粘贴也能触发。
3. **焦点管理**
- 点击 terminal-screen、WebSocket 连接成功、窗口 resize 后都聚焦 IME textarea。
4. **测试**:新增 `is_regular_text_input_detects_printable_keys` 单测,确保打印字符被正确交给 textarea24 个 terminal 测试全过clippy 干净。
**已知限制**
- 主屏幕 scrollback 滚动后光标可能位于可见区域下方IME 候选框位置会对应到光标逻辑位置可能偏下。IME 主要用于当前输入, scrolled-back 场景输入较少见。
- 目前不支持 textarea 自动滚动延伸选区(和 P0 同一限制)。
---
### P4 — Canvas / WebGL 渲染器(性能目标)✅ 已完成
**现状**WebGL glyph atlas / per-cell quad 已作为默认渲染器Canvas 2D 与 DOM renderer 保留为回退路径。长输出、输入后 live/history 稳定性、IME textarea buffer 坐标、DPR backing store、虚拟滚动 sizer、selection overlay、cursor overlay 均已验证。
**已实现**
1. **WebGL glyph atlas 默认渲染器**
- `Renderer::WebGl` 默认,`Renderer::Canvas` / `Renderer::Dom` 作为回退。
- WebGL2 初始化 shader/program/vertex buffer/atlas texture/fallback texture。
- `GlyphAtlas` 按 grapheme + bold/italic + cell width 缓存字形ASCII、CJK wide 字符、组合字符按 `unicode-segmentation` / `unicode-width` 拆成 cell glyph。
- 文本通过 per-cell quad 批量绘制,颜色作为 vertex attribute小字号不再走整屏纹理二次采样。
- 背景、underline、cursor 背景由 Canvas backdrop 绘制后作为底层 texture 合成cursor 前景文字仍进 atlas。
- atlas 溢出或遇到不可拆分复杂段时自动回退整屏 Canvas texture避免真实终端内容白屏。
- WebGL2 不可用时自动落回 Canvas 2D。
2. **Canvas 2D 渲染器**
- 保留现有 `TerminalCore` 数据模型,渲染层切换为 Canvas。
- 一屏/一窗口 `fillText` / `fillRect` 绘制,避免每个 segment 都生成 DOM 节点。
- DPR backing store 与 CSS 尺寸分离,避免高 DPI 模糊。
- `CanvasRenderPlan` 统一计算 backing store、CSS size、canvas top、content offset。
- `CanvasDomState` 缓存 DOM 尺寸状态,避免每帧无意义重设 backing store。
- 保留 `terminal-virtual-sizer` 维持完整 scroll geometry`cat` 后 live 模式稳定贴底。
- selection overlay 保持 DOM 绝对定位canvas 只负责文本/背景/光标。
- IME textarea 改用 buffer 坐标定位,长 scrollback 后输入不会触发 live/history 抖动。
3. **测试**
- `canvas_render_plan``canvas_fill_style``terminal_font``canvas_viewport_top_px``ime_cursor_cell``webgl_quad_vertices``webgl_shader_sources``GlyphAtlas``split_segment_glyphs``push_quad` 均有单测。
- `cargo test -p app --lib terminal::` 当前 38 个 terminal 测试通过。
**后续深化(进入新一轮 xterm.js 对标计划)**
1. WebGL context loss/recovery 与 atlas 重建。
2. 渲染性能基准:固定脚本覆盖 10k/50k 行输出、200×80、240×100、持续输出 FPS / frame time。
3. 浏览器截图 + canvas/WebGL pixel smoke test 固化到工程化脚本。
4. emoji / ZWJ / variation selector / ligature 的 advanced shaping 策略。
---
### P5 — xterm.js API / 配置面(新一轮)✅ 已完成
**目标**:把当前 `TerminalPanel` 从页面内组件推进成可复用终端组件。
**已实现**
1. `TerminalOptions`
- cols/rows、scrollback、renderer、font family/size/line-height、theme、cursor style/blink、title/subtitle、show_header。
- 支持 runtime options signal 更新字体、行高、theme 同步到 DOM / Canvas / WebGL glyph atlas / 虚拟滚动计算。
- `TerminalCore::new_with_scrollback` 让 scrollback 容量进入 parser 层,而不只是 UI 参数。
2. `TerminalHandle`
- `write``paste_text``clear``focus``resize``scroll_to_bottom``copy_selection`
3. 事件回调 ✅
- `on_data``on_resize``on_selection_change``on_title_change``on_render`
4. 测试 ✅
- 新增 options 解析 / CSS 变量 / 行高参数化 / cursor shape / cursor glyph color 相关单测。
- `cargo test -p app --lib terminal::` 当前 41 个 terminal 测试通过。
5. 第一轮模块拆分 ✅
- `keyboard.rs`paste normalization、key mapping、printable key 判断。
- `mouse.rs`wheel rows、mouse report 编码、visible grid mapping。
- `selection.rs`selection model、selection rect 计算。
- `ime.rs`IME cursor buffer 坐标和 hidden textarea helpers。
**后续进入 P6**
1. `TerminalHandle` 增强:`fit()`、serialize/search/link addon 接口。
2. 继续把 WebGL / Canvas renderer 从 `component.rs` 拆成专门模块。
### P6 — xterm.js addon parity搜索 / 链接 / 序列化 / fit
**当前进度**
1. **Fit addon 等价能力**
-`TerminalHandle::fit()`,根据容器内容区重新测量 rows/cols更新 core viewport 并同步 PTY resize。
2. **Search**
-`TerminalHandle::search_next/search_previous/clear_search`
- ✅ literal search 跨 `history + screen`,支持 wrap搜索结果通过 DOM overlay 高亮WebGL/Canvas/DOM renderer 共用。
3. **Serialize**
-`TerminalHandle::serialize_text()``TerminalCore::buffer_text()`,导出当前 retained buffer 文本。
-`TerminalHandle::serialize_ansi()``TerminalCore::buffer_ansi()`,按当前 row/style 模型导出 ANSI-styled snapshot。
4. **Web links / OSC 8**
- ✅ URL 自动识别、点击打开WebGL/Canvas/DOM renderer 共用链接 hitbox overlay。
- ✅ OSC 8 hyperlink 解析:支持 BEL / ST 终止,使用 cell-grid sidecar 记录链接,覆盖写入、常见 CSI 光标移动、清行/清屏、滚屏进 history 后仍能正确映射到链接 hitbox overlay。
- 后续可继续扩展非常规控制序列覆盖面,但 P6 addon parity 的核心能力已完成。
### P7 — Accessibility / 可访问性
**计划实现**
1. screen reader buffer / aria-live 策略。
2. keyboard-only selection / copy。
3. high contrast / minimum contrast ratio。
4. reduced motion 与焦点可见性。
### P8 — Unicode / 字形精度
**计划实现**
1. emoji / ZWJ / variation selector 的 grapheme cluster 宽度策略。
2. CJK ambiguous width 配置。
3. ligature 可选支持。
4. fallback font metrics 校准。
### P9 — WebGL Renderer 工程化深化
**计划实现**
1. **WebGL context loss/recovery 与 atlas 重建**
- 监听 `webglcontextlost` / `webglcontextrestored`lost 时阻止默认行为并清空 `WebGlRendererState`
- context lost 或 WebGL 初始化失败时显示独立 Canvas 2D fallback surface避免同一 canvas 上 WebGL lost 后无法获取 2D context 导致白屏。
- restore 后清空旧 state 并触发重绘,下一帧重建 shader/program/buffer/texture/glyph atlas。
- context lifecycle listener 由 `WebGlContextLifecycle` 持有,组件卸载或 renderer 切换时自动移除。
2. **渲染性能 benchmark 和自动截图 / pixel smoke test**
- `scripts/terminal-bench.mjs` 连接已启动的 `/rustui/terminal`,不负责启动/关闭服务。
- 固定覆盖 `200x80``240x100` viewport支持 `--lines``--viewport``--output` 参数。
- 记录行输出耗时、rows/sec、requestAnimationFrame frame time 摘要、`window.__terminalBench` render probe。
- 截图 smoke 默认走 Playwright screenshot PNG 非空/熵检查Canvas 2D fallback 可读时额外做像素采样。
3. **Canvas 2D / WebGL runtime switch**
- `/rustui/terminal` 页面提供 WebGL / Canvas / DOM runtime 切换。
- 支持 `?renderer=webgl|canvas|dom` 初始化,便于浏览器回归和 benchmark 固定模式。
- `scripts/terminal-bench.mjs --renderer webgl|canvas|dom` 复用同一入口跑对比,并将截图按 renderer 命名。
4. 与 Canvas 2D 做 benchmark 对比,按 viewport size / output rate 自动选择 renderer。
---
## 4. 建议推进顺序
```
P0 复制粘贴(先粘贴→可见区复制) ← 最先,体验提升最大
P1 完整功能键 + application-cursor
P2 鼠标上报
P3 中文 IME(放在功能键之后,避免输入逻辑冲突)
P4 Canvas / WebGL 渲染器(满足大终端 + 高刷新率性能目标)
P5 API / 配置面
P6 addon parityfit/search/serialize/web-links
P7 accessibility
P8 Unicode / 字形精度
P9 WebGL renderer 工程化深化
```
每一项都应延续现有做法:**纯逻辑抽成可单测的函数**(如 `map_key_to_terminal_input``wheel_delta_to_rows``virtual_window`),用 `cargo test -p app --lib terminal::` 锁定,浏览器只做交互验证。