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>
9.5 KiB
Terminal Roadmap — 对照 xterm.js 的追平计划
本文档记录 base-path-demo 浏览器终端当前已实现的能力、相比 xterm.js 仍缺失的功能,以及每一项计划的实现方式与优先级,供后续开发接手。
定位提醒:本项目目标不是复刻 xterm.js 这个通用 JS 终端库,而是按 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 滚轮 → 方向键翻页(rAF 合并多次 wheel 事件,单次滑动最多一屏) | 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 — 复制 / 粘贴 + 文本选区 ⭐ ✅ 已完成
现状:粘贴、选区、复制全部完成并验证通过。
已实现:
- 粘贴 ✅
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 为主路径。
- 选区 + 复制 ✅(自建选区模型,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,均有单元测试。
- 选区按逻辑行列坐标跟踪(
- 快捷键:Ctrl-Shift-C 复制 / Ctrl-V 粘贴。
已知后续项(非阻塞):拖拽到视窗边缘时没有自动滚动延伸选区(需先滚到位再选);行内 wide+组合字符混排的列切片为近似。
P1 — 完整功能键 + application-cursor 模式 ✅ 已完成
现状:完整键表已实现并验证通过(key_to_bytes_table 单测覆盖)。
已实现:
-
完整键表(
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 编码)。
- F1–F4 SS3(
-
application-cursor 模式(DECCKM):
cursor_seq(byte, app_cursor)助手;为真时方向键 / Home / End 发 SS3(ESC O X),否则 CSI(ESC [ X)。handle_keydown和handle_wheel都据此切换。 -
浏览器快捷键仲裁:
handle_keydown对 bareCtrl+字母(无 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。 -
测试:
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 点击都用不了。
计划实现:
- vt100 已解析鼠标模式:
screen().mouse_protocol_mode()(None/Press/PressRelease/ButtonMotion/AnyMotion)和mouse_protocol_encoding()(Default/Utf8/Sgr)。 - 在
terminal-screen上监听on:mousedown/on:mouseup/on:mousemove:- 把像素坐标换算成终端 行列(用已测量的 cell 宽高 + 容器 rect)。
- 按当前 encoding 编码:
- SGR(最常用):
ESC[<b;col;row;M(按下)/m(释放)。 - Default:
ESC[M+ 三字节。
- SGR(最常用):
- 只在
mouse_protocol_mode != None时拦截,否则放行(不破坏选区/原生行为)。
- 与 P0 选区冲突处理:应用开启鼠标上报时,鼠标事件优先发给应用;否则用于本地选区。
风险:中。坐标换算边界 + 与选区的优先级仲裁需要仔细测。
P3 — 中文 / IME 输入
现状:map_key_to_terminal_input 只处理 key.chars().count() == 1 的单字符,IME 组合输入(拼音候选)完全没接。
计划实现:
- 改用
composition事件 + 隐藏<textarea>的标准方案(xterm.js 也是这套):- 一个 1×1 透明、跟随光标的
textarea承接 IME。 - 监听
compositionstart/compositionupdate/compositionend。 - 组合进行中不发字节;
compositionend时把最终文本一次性作为 Input 发送。 - 普通
input事件(非组合)也走这个 textarea,替代部分keydown逐字符逻辑。
- 一个 1×1 透明、跟随光标的
- 光标定位:把隐藏 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:: 锁定,浏览器只做交互验证。