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>
This commit is contained in:
zhangheng
2026-06-11 20:59:03 +08:00
parent a018609a28
commit c886befac7
4 changed files with 96 additions and 9 deletions

View File

@@ -29,6 +29,7 @@
| `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` |
---
@@ -36,14 +37,15 @@
### P0 — 复制 / 粘贴 + 文本选区 ⭐ 投入产出比最高
**现状**完全没有。无法选中终端文本、无法粘贴。这是「能用 vs 好用」的分界线
**现状**粘贴**已完成**(见下);选区 / 复制尚未做
**计划实现**
1. **粘贴(先做,最简单)**
- 监听 `on:paste``ClipboardEvent``clipboard_data().get_data("text")`
- 直接作为 `ClientTerminalMessage::Input` 发送
- 若应用开启 bracketed paste 模式vt100 的 `screen().bracketed_paste()` 为真),把粘贴内容包进 `ESC[200~ … ESC[201~`,避免 vim 等把粘贴当连续输入触发自动缩进
2. **选区(较复杂)**
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 可只支持**可见范围内选区 + 复制**,跨滚动选区列为后续。