Replace the spacer-estimate scrollback model with an explicit history row buffer owned by TerminalCore, rendered with absolute-positioned virtual scrolling so huge outputs (cat of 50k-line files) stay smooth. core.rs: - TerminalCore owns history_rows + a cached screen, captured after each process() via a bounded "conveyor belt" read of vt100 scrollback - feed bytes in newline-bounded safe slices so a capture never reads deeper than viewport_rows (works around vt100 0.15 visible_rows subtract-overflow panic) - handle clear (ESC[3J) by wiping our own history buffer - TerminalSegment carries grid column count for exact-width rendering - collect_window() clones only the visible slice component.rs: - TerminalCore lives in an RwSignal; render driven by an rAF-coalesced render_tick so packet bursts collapse into one frame - virtual_window pins to the bottom in live mode, follows scroll_top in history mode; absolute-positioned rows in a fixed-height sizer keep scroll geometry constant (fixes the "keeps drifting up" bounce) - position-based programmatic-scroll detection replaces the racy ignore-next-scroll flag - alt-screen wheel events translate to arrow keys (vim/less paging) - try_* signal access guards against disposal during hydration style: lock row height to 18px, segment width to Nch (exact grid so fallback-font glyphs like btm braille can't push the row sideways), absolute-position the virtual rows layer. Adds 12 unit tests (capture, clear, alt-screen, window slicing, wheel mapping) and TERMINAL_ROADMAP.md tracking xterm.js parity work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.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 滚轮 → 方向键翻页 | component.rs::handle_wheel |
| 基础按键:Ctrl-C/D/L、方向键、Enter/Tab/Esc/Backspace | component.rs::map_key_to_terminal_input |
2. 追平项(按优先级)
P0 — 复制 / 粘贴 + 文本选区 ⭐ 投入产出比最高
现状:完全没有。无法选中终端文本、无法粘贴。这是「能用 vs 好用」的分界线。
计划实现:
- 粘贴(先做,最简单)
- 监听
on:paste(ClipboardEvent),读clipboard_data().get_data("text")。 - 直接作为
ClientTerminalMessage::Input发送。 - 若应用开启 bracketed paste 模式(vt100 的
screen().bracketed_paste()为真),把粘贴内容包进ESC[200~ … ESC[201~,避免 vim 等把粘贴当连续输入触发自动缩进。
- 监听
- 选区(较复杂)
- 因为渲染是 DOM(每行一个
<div>、每段一个<span>),可优先尝试浏览器原生选区:给可见行允许user-select: text,监听copy事件,从window.getSelection()取文本。 - 但虚拟滚动只渲染可见窗口,跨窗口选区会断 → 需要把「逻辑行号 → 文本」的映射暴露给选区逻辑,从
TerminalCore的history_rows + screen_rows直接取纯文本(已有row→ 文本的能力,测试里row_text即是)。 - MVP 可只支持可见范围内选区 + 复制,跨滚动选区列为后续。
- 因为渲染是 DOM(每行一个
- 快捷键:Ctrl-Shift-C / Ctrl-Shift-V(终端惯例,避免和 Ctrl-C 中断冲突)。
风险:选区与虚拟滚动的交互是主要难点;建议 MVP 限定可见区,先把粘贴 + 可见区复制跑通。
P1 — 完整功能键 + application-cursor 模式
现状:只映射了方向键、Ctrl-C/D/L、Enter/Tab/Esc/Backspace。缺 F1–F12、Home/End/Insert/Delete、PageUp/PageDown、大量 Ctrl/Alt 组合。
计划实现:
- 扩展
map_key_to_terminal_input,补齐:F1–F12→ESC O P/Q/R/S(F1–4)、ESC[15~…(F5–12)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+ 字符
- 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 点击都用不了。
计划实现:
- 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:: 锁定,浏览器只做交互验证。