Files
rustui-playground/TERMINAL_ROADMAP.md
zhangheng c3ddd9df4f fix(terminal): robust Ctrl/paste handling + alt-screen wheel coalescing
Keyboard input fixes addressing reported regressions:

- Pressing Ctrl alone no longer sends SIGINT: key_to_bytes now guards on
  a single-character key, so modifier keys "Control"/"Shift"/"Meta" are
  not misread as Ctrl-C (0x03).
- Ctrl-Shift-C copy restored: the copy branch runs before the generic
  Ctrl+letter suppression so the clipboard user-gesture stays intact.
- Ctrl+C copies when a selection exists (VS Code / Windows Terminal
  behaviour); without a selection it still sends SIGINT.
- Ctrl+V / Ctrl-Shift+V paste via navigator.clipboard.readText() —
  reliable across browsers that don't fire paste on a non-contenteditable
  div; on:paste retained for right-click / menu paste.
- Bare Ctrl+letter (no Shift/Alt/Meta) is preventDefault'd so the browser
  stops intercepting Ctrl-R/F/A etc. (Ctrl-W/T/N stay browser-reserved
  and cannot be intercepted by any web page).
- Filter Ctrl-Z (0x1a) / Ctrl-\ (0x1c) so stty SIGTSTP/SIGQUIT don't
  surprise-kill the foreground process; Ctrl-C (0x03) is kept.

Alt-screen wheel:

- Coalesce multiple wheel events per animation frame into a single
  arrow-key batch (Rc<Cell> accumulator + requestAnimationFrame), so one
  trackpad swipe pages at most one viewport instead of flying past.

Tests: 18/18 pass, clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 14:32:46 +08:00

147 lines
9.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 滚轮 → 方向键翻页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 / 浏览器菜单,含 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**当选区存在时 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` 单测覆盖整套键表18 个 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 鼠标、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::` 锁定,浏览器只做交互验证。