20 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 面。性能目标:终端默认渲染器已回切到 Canvas 2D,保留 WebGL glyph atlas / per-cell quad 与 DOM renderer 作为可切换路径。最新基准显示 Canvas 2D 在 10k 行输出、200×80 / 240×100 viewport 下吞吐和 frame time 都明显优于当前 WebGL 实现;WebGL 继续作为优化回归目标保留。
1. 覆盖度自评(截至本文档)
按用途粗估:
- 「跑通真实终端会话」核心用途:约 85%
- xterm.js 完整 API / 特性面:约 40%
差距集中在 API/插件生态、accessibility、搜索/链接、Unicode 深度精度、运行时可配置性、工程化性能基准,而非「能不能用」。
已实现(有代码 + 单元测试)
| 能力 | 位置 |
|---|---|
| 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 / Ctrl-Shift-V / 右键菜单,含 bracketed paste) | component.rs::handle_paste / prepare_paste |
| 文本选区 + 复制(自建模型,Ctrl-Shift-C / 选区存在时 Ctrl+C,跨滚动) | component.rs 鼠标处理 / core.rs::selection_text |
| 鼠标上报(vim/htop/tmux 鼠标,SGR/Default/UTF-8 编码,滚轮) | component.rs 鼠标处理 / encode_mouse_report |
| 中文 / IME 输入(拼音/日文/韩文,隐藏 textarea + composition 事件) | component.rs::handle_input / handle_composition_* |
| Canvas 2D 默认渲染器 + WebGL glyph atlas 可选路径(per-cell quad、DPR glyph atlas、背景/underline/cursor backdrop、DOM 回退) | component.rs::Renderer / render_canvas / canvas_render_plan / render_webgl / GlyphAtlas |
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单测覆盖整套键表,23 个 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/htop/btm/tmux 的鼠标交互、tmux 选区、vim 滚轮均可以工作。
已实现:
- vt100 mouse-mode accessors(
core.rs::mouse_protocol_mode/mouse_protocol_encoding)—— 暴露None/Press/PressRelease/ButtonMotion/AnyMotion和Default/Utf8/Sgr。 - 事件监听与坐标换算(
component.rs)on:mousedown/on:mouseup/on:mousemove复用现有的 DOM 事件。pixel_to_grid把(clientX, clientY)换算成 0-based 可见网格(row, col)(注意:与选区用的point_to_cell不同,这里不加 scroll_top,因为鼠标报告坐标是相对于应用正在绘制的当前屏幕,而非 scrollback 缓冲区)。
- 编码(
encode_mouse_report纯函数)- SGR(1006):
ESC[<b;col;row;M(按下/移动)/ESC[<b;col;row;m(释放)。ASCII-only,任意屏幕尺寸都正确。 - Default / UTF-8:
ESC[M+ 三字节(各 +32)。坐标 ≤94 时完全正确并 ASCII-safe;更大坐标被钳位到 94(大多数终端 likewise;现代应用都走 SGR)。 - 滚轮:垂直/水平滚轮映射为按钮 64/65/66/67 的
Press事件。
- SGR(1006):
- 模式仲裁
- 应用开启 mouse reporting(
mode != None)时,鼠标事件优先发给应用;否则走本地选区。 - 按住 Shift 点击可绕过鼠标上报、强制本地选区(xterm 行为)。
- 鼠标移动按网格单元去重,避免每像素都发报告。
- 应用开启 mouse reporting(
- 测试:
encode_mouse_report、pixel_to_grid、with_mouse_modifiers、mouse_button_base均有单测;23 个 terminal 测试全过,clippy 干净。
已知限制:
- 主屏幕(非 alt-screen)滚动后,鼠标报告坐标仍相对于可见网格顶部;因为开启鼠标上报的应用通常运行在 alt-screen,这覆盖主要使用场景。
- 与浏览器
Ctrl+W/T/N类似,浏览器可能保留某些鼠标手势(如右键菜单、拖拽文本),这些在网页终端内无法 100% 拦截。应用内鼠标操作应正常。
P3 — 中文 / IME 输入 ✅ 已完成
现状:已实现。拼音、日文、韩文等 IME 组合输入可以正常工作;普通英文/数字输入也迁到了 textarea 的 input 事件,确保和 IME 走同一条路径。
已实现:
- 隐藏
<textarea>IME 载体(component.rsview)- 绝对定位、透明、无边框、大小跟随终端光标单元格。
- 通过
tabindex="0"成为焦点目标;terminal-screen改用:focus-within显示焦点环。
- 事件模型迁移
on:input:普通字符/数字/空格输入,读取 textarea value 后清空并发送。on:compositionstart/on:compositionend:IME 组合期间不发字节,compositionend时发送最终文本。on:keydown:仍处理方向键、Enter、Esc、Backspace、F 键、Ctrl 组合等非打印键。on:paste:从terminal-screen移到 textarea,确保 focus 在 textarea 时右键/菜单粘贴也能触发。
- 焦点管理
- 点击 terminal-screen、WebSocket 连接成功、窗口 resize 后都聚焦 IME textarea。
- 测试:新增
is_regular_text_input_detects_printable_keys单测,确保打印字符被正确交给 textarea;24 个 terminal 测试全过,clippy 干净。
已知限制:
- 主屏幕 scrollback 滚动后,光标可能位于可见区域下方,IME 候选框位置会对应到光标逻辑位置(可能偏下)。IME 主要用于当前输入, scrolled-back 场景输入较少见。
- 目前不支持 textarea 自动滚动延伸选区(和 P0 同一限制)。
P4 — Canvas / WebGL 渲染器(性能目标)✅ 已完成
现状:Canvas 2D 已作为默认渲染器;WebGL glyph atlas / per-cell quad 与 DOM renderer 保留为可切换路径。长输出、输入后 live/history 稳定性、IME textarea buffer 坐标、DPR backing store、虚拟滚动 sizer、selection overlay、cursor overlay 均已验证。2026-06-24 benchmark 显示 Canvas 2D 在当前实现下优于 WebGL,因此默认策略先以 Canvas 为准。
已实现:
- Canvas 2D 默认渲染器 + WebGL 可选路径 ✅
Renderer::Canvas默认,Renderer::WebGl/Renderer::Dom作为可切换回归路径。- WebGL2 初始化 shader/program/vertex buffer/atlas texture/fallback texture。
GlyphAtlas按 grapheme + bold/italic + cell width 缓存字形;ASCII、CJK wide 字符、组合字符按unicode-segmentation/unicode-width拆成 cell glyph。- 文本通过 per-cell quad 批量绘制,颜色作为 vertex attribute;小字号不再走整屏纹理二次采样。
- 背景、underline、cursor 背景由 Canvas backdrop 绘制后作为底层 texture 合成;cursor 前景文字仍进 atlas。
- atlas 溢出或遇到不可拆分复杂段时自动回退整屏 Canvas texture,避免真实终端内容白屏。
- WebGL2 不可用时自动落回 Canvas 2D。
- Canvas 2D 渲染器 ✅
- 保留现有
TerminalCore数据模型,渲染层切换为 Canvas。 - 一屏/一窗口
fillText/fillRect绘制,避免每个 segment 都生成 DOM 节点。 - DPR backing store 与 CSS 尺寸分离,避免高 DPI 模糊。
CanvasRenderPlan统一计算 backing store、CSS size、canvas top、content offset。CanvasDomState缓存 DOM 尺寸状态,避免每帧无意义重设 backing store。- 保留
terminal-virtual-sizer维持完整 scroll geometry,长cat后 live 模式稳定贴底。 - selection overlay 保持 DOM 绝对定位,canvas 只负责文本/背景/光标。
- IME textarea 改用 buffer 坐标定位,长 scrollback 后输入不会触发 live/history 抖动。
- 保留现有
- 测试 ✅
canvas_render_plan、canvas_fill_style、terminal_font、canvas_viewport_top_px、ime_cursor_cell、webgl_quad_vertices、webgl_shader_sources、GlyphAtlas、split_segment_glyphs、push_quad均有单测。cargo test -p app --lib terminal::当前 38 个 terminal 测试通过。
后续深化(进入新一轮 xterm.js 对标计划):
- WebGL context loss/recovery 与 atlas 重建。
- 渲染性能基准:固定脚本覆盖 10k/50k 行输出、200×80、240×100、持续输出 FPS / frame time。
- 浏览器截图 + canvas/WebGL pixel smoke test 固化到工程化脚本。
- emoji / ZWJ / variation selector / ligature 的 advanced shaping 策略。
P5 — xterm.js API / 配置面(新一轮)✅ 已完成
目标:把当前 TerminalPanel 从页面内组件推进成可复用终端组件。
已实现:
TerminalOptions✅- cols/rows、scrollback、renderer、font family/size/line-height、theme、cursor style/blink、title/subtitle、show_header。
- 支持 runtime options signal 更新;字体、行高、theme 同步到 DOM / Canvas / WebGL glyph atlas / 虚拟滚动计算。
TerminalCore::new_with_scrollback让 scrollback 容量进入 parser 层,而不只是 UI 参数。
TerminalHandle✅write、paste_text、clear、focus、resize、scroll_to_bottom、copy_selection。
- 事件回调 ✅
on_data、on_resize、on_selection_change、on_title_change、on_render。
- 测试 ✅
- 新增 options 解析 / CSS 变量 / 行高参数化 / cursor shape / cursor glyph color 相关单测。
cargo test -p app --lib terminal::当前 41 个 terminal 测试通过。
- 第一轮模块拆分 ✅
keyboard.rs:paste normalization、key mapping、printable key 判断。mouse.rs:wheel rows、mouse report 编码、visible grid mapping。selection.rs:selection model、selection rect 计算。ime.rs:IME cursor buffer 坐标和 hidden textarea helpers。
后续进入 P6:
TerminalHandle增强:fit()、serialize/search/link addon 接口。- 继续把 WebGL / Canvas renderer 从
component.rs拆成专门模块。
P6 — xterm.js addon parity(搜索 / 链接 / 序列化 / fit)
当前进度:
- Fit addon 等价能力
- ✅
TerminalHandle::fit(),根据容器内容区重新测量 rows/cols,更新 core viewport 并同步 PTY resize。
- ✅
- Search
- ✅
TerminalHandle::search_next/search_previous/clear_search。 - ✅ literal search 跨
history + screen,支持 wrap,搜索结果通过 DOM overlay 高亮,WebGL/Canvas/DOM renderer 共用。
- ✅
- Serialize
- ✅
TerminalHandle::serialize_text()与TerminalCore::buffer_text(),导出当前 retained buffer 文本。 - ✅
TerminalHandle::serialize_ansi()与TerminalCore::buffer_ansi(),按当前 row/style 模型导出 ANSI-styled snapshot。
- ✅
- Web links / OSC 8
- ✅ URL 自动识别、点击打开,WebGL/Canvas/DOM renderer 共用链接 hitbox overlay。
- ✅ OSC 8 hyperlink 解析:支持 BEL / ST 终止,使用 cell-grid sidecar 记录链接,覆盖写入、常见 CSI 光标移动、清行/清屏、滚屏进 history 后仍能正确映射到链接 hitbox overlay。
- 后续可继续扩展非常规控制序列覆盖面,但 P6 addon parity 的核心能力已完成。
P7 — Accessibility / 可访问性
计划实现:
- screen reader buffer / aria-live 策略。
- keyboard-only selection / copy。
- high contrast / minimum contrast ratio。
- reduced motion 与焦点可见性。
P8 — Unicode / 字形精度
计划实现:
- emoji / ZWJ / variation selector 的 grapheme cluster 宽度策略。
- CJK ambiguous width 配置。
- ligature 可选支持。
- fallback font metrics 校准。
P9 — WebGL Renderer 工程化深化
计划实现:
- WebGL context loss/recovery 与 atlas 重建 ✅
- 监听
webglcontextlost/webglcontextrestored,lost 时阻止默认行为并清空WebGlRendererState。 - context lost 或 WebGL 初始化失败时显示独立 Canvas 2D fallback surface,避免同一 canvas 上 WebGL lost 后无法获取 2D context 导致白屏。
- restore 后清空旧 state 并触发重绘,下一帧重建 shader/program/buffer/texture/glyph atlas。
- context lifecycle listener 由
WebGlContextLifecycle持有,组件卸载或 renderer 切换时自动移除。
- 监听
- 渲染性能 benchmark 和自动截图 / pixel smoke test ✅
scripts/terminal-bench.mjs连接已启动的/rustui/terminal,不负责启动/关闭服务。- 固定覆盖
200x80、240x100viewport,支持--lines、--viewport、--output参数。 - 记录行输出耗时、rows/sec、requestAnimationFrame frame time 摘要、
window.__terminalBenchrender probe。 - 截图 smoke 默认走 Playwright screenshot PNG 非空/熵检查;Canvas 2D fallback 可读时额外做像素采样。
- Canvas 2D / WebGL runtime switch ✅
/rustui/terminal页面提供 WebGL / Canvas / DOM runtime 切换。- 支持
?renderer=webgl|canvas|dom初始化,便于浏览器回归和 benchmark 固定模式。 scripts/terminal-bench.mjs --renderer webgl|canvas|dom复用同一入口跑对比,并将截图按 renderer 命名。
- 与 Canvas 2D 做 benchmark 对比,按 viewport size / output rate 自动选择 renderer ✅
- 2026-06-24 基线,10k 行输出:
200x80:WebGL155.27 rows/sec,frame p95283.3ms;Canvas311.67 rows/sec,frame p95116.7ms。240x100:WebGL92.48 rows/sec,frame p95399.9ms;Canvas157.65 rows/sec,frame p95116.8ms。
- 两种 renderer 的 pixel smoke 均通过;Canvas 可直接读取 canvas 像素,WebGL 走 screenshot smoke。
- 当前策略:默认 Canvas;WebGL 继续通过页面切换、
?renderer=webgl和--renderer webgl保留优化回归入口。
- 2026-06-24 基线,10k 行输出:
- WebGL renderer profiling / 慢路径拆解 ✅
- WebGL render path 记录阶段耗时:DOM/canvas 同步、backdrop 绘制、glyph collect、backdrop texture pass、glyph atlas、atlas upload、vertex build/upload、draw call。
- 记录计数:cells、atlas entries、atlas insertions/resets、full-canvas fallback 次数、glyph draw failed 次数。
window.__terminalBench.webglFrames暴露每帧采样,scripts/terminal-bench.mjs汇总为rendererStats,用于判断慢点是整屏 backdrop texture、atlas、vertex buffer,还是 fallback。
4. 建议推进顺序
P0 复制粘贴(先粘贴→可见区复制) ← 最先,体验提升最大
↓
P1 完整功能键 + application-cursor
↓
P2 鼠标上报
↓
P3 中文 IME(放在功能键之后,避免输入逻辑冲突)
↓
P4 Canvas / WebGL 渲染器(满足大终端 + 高刷新率性能目标)
↓
P5 API / 配置面
↓
P6 addon parity(fit/search/serialize/web-links)
↓
P7 accessibility
↓
P8 Unicode / 字形精度
↓
P9 WebGL renderer 工程化深化
每一项都应延续现有做法:纯逻辑抽成可单测的函数(如 map_key_to_terminal_input、wheel_delta_to_rows、virtual_window),用 cargo test -p app --lib terminal:: 锁定,浏览器只做交互验证。