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>
146 lines
8.5 KiB
Markdown
146 lines
8.5 KiB
Markdown
# 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. 覆盖度自评(截至本文档)
|
||
|
||
按用途粗估:
|
||
|
||
- **「跑通真实终端会话」核心用途**:约 50–60%
|
||
- **xterm.js 完整 API / 特性面**:约 15–20%
|
||
|
||
差距集中在**输入完整性**、**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-screen(vim / 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` |
|
||
| 完整键表(F1–F12/Home/End/PageUp/Down/Ctrl A–Z/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`)
|
||
- F1–F4 SS3(`ESC O P/Q/R/S`)、F5–F12 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::` 锁定,浏览器只做交互验证。
|