Files
rustui-playground/TERMINAL_ROADMAP.md
zhangheng c886befac7 feat(terminal): clipboard paste with bracketed-paste support
Pasting (Ctrl-V or browser menu) now sends clipboard text to the PTY.

- on:paste reads clipboard_data().get_data("text"); newlines normalized
  to CR (the line-submit byte) via the pure, unit-tested prepare_paste()
- when the app enabled bracketed paste (DECSET 2004, exposed via the new
  TerminalCore::bracketed_paste() proxy), the text is wrapped in
  ESC[200~ / ESC[201~ so vim et al. don't auto-indent each pasted line
- pasting returns the view to live mode + scrolls to bottom, matching the
  keydown handler
- Cargo.toml: add ClipboardEvent/DataTransfer (and explicit WheelEvent)
  to the web-sys feature allowlist
- right-click is left to the browser's default menu (avoids requiring
  clipboard-read permission); Ctrl-V is the primary path

Adds a prepare_paste unit test (14 terminal tests total) and marks paste
done in TERMINAL_ROADMAP.md. Also fixes a clippy manual_contains lint in
an existing test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 20:59:03 +08:00

138 lines
7.7 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` |
| 粘贴Ctrl-V / 浏览器菜单,含 bracketed paste | `component.rs::handle_paste` / `prepare_paste` |
---
## 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. **选区(较复杂,下一步)**
- 因为渲染是 DOM每行一个 `<div>`、每段一个 `<span>`),可优先尝试**浏览器原生选区**:给可见行允许 `user-select: text`,监听 `copy` 事件,从 `window.getSelection()` 取文本。
- 但虚拟滚动只渲染可见窗口,跨窗口选区会断 → 需要把「逻辑行号 → 文本」的映射暴露给选区逻辑,从 `TerminalCore``history_rows + screen_rows` 直接取纯文本(已有 `row` → 文本的能力,测试里 `row_text` 即是)。
- MVP 可只支持**可见范围内选区 + 复制**,跨滚动选区列为后续。
3. **快捷键**Ctrl-Shift-C / Ctrl-Shift-V终端惯例避免和 Ctrl-C 中断冲突)。
**风险**:选区与虚拟滚动的交互是主要难点;建议 MVP 限定可见区,先把粘贴 + 可见区复制跑通。
---
### P1 — 完整功能键 + application-cursor 模式
**现状**只映射了方向键、Ctrl-C/D/L、Enter/Tab/Esc/Backspace。缺 F1F12、Home/End/Insert/Delete、PageUp/PageDown、大量 Ctrl/Alt 组合。
**计划实现**
1. 扩展 `map_key_to_terminal_input`,补齐:
- `F1``F12``ESC O P/Q/R/S`F14`ESC[15~`F512
- `Home`/`End``ESC[H` / `ESC[F`(或 `ESC[1~` / `ESC[4~`
- `Insert`/`Delete``ESC[2~` / `ESC[3~`
- `PageUp`/`PageDown``ESC[5~` / `ESC[6~`
- `Ctrl+A..Z` → 控制字符 `0x01..0x1A`(通用化,不再只 C/D/L
- `Alt+<key>` → 前缀 `ESC` + 字符
2. **application-cursor 模式**vt100 暴露 `screen().application_cursor()`。为真时,方向键要发 **SS3 形式 `ESC O A/B/C/D`** 而非 `ESC [ A/B/C/D``handle_keydown``handle_wheel`alt-screen 滚轮翻页)都要据此切换序列。
- 这能修掉「某些 vim 配置 / 全屏程序里方向键行为怪异」的潜在问题。
**风险**:低。纯查表扩展 + 一个模式判断。建议把按键表抽成数据驱动并加单元测试覆盖。
---
### 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::` 锁定,浏览器只做交互验证。