Files
rustui-playground/TERMINAL_ROADMAP.md
zhangheng 511b212334 docs(terminal): mark P1 done in roadmap + document stty control bytes
Ctrl-C/Ctrl-Z/Ctrl-\ reach the PTY as 0x03/0x1a/0x1c and are then
interpreted by bash's stty (SIGINT/SIGTSTP/SIGQUIT), which is real-terminal
behavior and preserved on purpose per user confirmation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:38:53 +08:00

8.5 KiB
Raw Blame History

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. 覆盖度自评(截至本文档)

按用途粗估:

  • 「跑通真实终端会话」核心用途:约 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
完整键表F1F12/Home/End/PageUp/Down/Ctrl AZ/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 — 复制 / 粘贴 + 文本选区 已完成

现状:粘贴、选区、复制全部完成并验证通过

已实现

  1. 粘贴
    • on:pasteClipboardEvent)读 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. 选区 + 复制 自建选区模型xterm.js 风格)
    • 选区按逻辑行列坐标跟踪(Selection { anchor, head }),鼠标 mousedown/move/up 拖拽更新;point_to_cell 做像素→单元格换算(含 padding/scroll
    • 高亮 overlayselection_rects 算每行矩形,渲染在虚拟滚动层内(共享 translateY跟随滚动不拆分文本段。
    • 复制取 TerminalCore::selection_text(start, end) —— 直接从 history_rows + screen_cache 数据模型按行列取文本,跨滚动、输出流动时都正确(不依赖 DOM
    • Ctrl-Shift-C 复制Ctrl-C 保持 SIGINT打字清除选区。
    • 实现于 component.rsSelection/selection_rects/point_to_cell/鼠标处理)+ core.rs::selection_text + core.rs::TerminalRow::text_in_cols,均有单元测试。
  3. 快捷键Ctrl-Shift-C 复制 / Ctrl-V 粘贴。

已知后续项(非阻塞):拖拽到视窗边缘时没有自动滚动延伸选区(需先滚到位再选);行内 wide+组合字符混排的列切片为近似。


P1 — 完整功能键 + application-cursor 模式 已完成

现状:完整键表已实现并验证通过(key_to_bytes_table 单测覆盖)。

已实现

  1. 完整键表component.rs::key_to_bytes
    • F1F4 SS3ESC O P/Q/R/S、F5F12 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 编码)。
  2. application-cursor 模式DECCKMcursor_seq(byte, app_cursor) 助手;为真时方向键 / Home / End 发 SS3ESC O X),否则 CSIESC [ X)。handle_keydownhandle_wheel 都据此切换。
  3. 测试key_to_bytes_table 单测覆盖整套键表17 个 terminal 测试全过、clippy 干净。

有意保留的"看起来像 bug"行为(与真终端/xterm 一致):

  • Ctrl-C (0x03) → SIGINT 中断前台进程
  • Ctrl-Z (0x1a) → SIGTSTP 暂停前台进程shell 提示符立刻返回)
  • Ctrl-\ (0x1c) → SIGQUIT 杀掉前台进程shell 提示符返回)

这些是 bash 等的 stty 默认行为intr/quit/susp由内核 tty 纪律触发,与 xterm/SSH 进真终端一样——不是本组件 bug也按用户确认保留。


P2 — 鼠标上报mouse reporting

现状:没有。点击 / 拖拽位置不会发给应用vim 鼠标、tmux 选区、btm 点击都用不了。

计划实现

  1. vt100 已解析鼠标模式:screen().mouse_protocol_mode()None/Press/PressRelease/ButtonMotion/AnyMotionmouse_protocol_encoding()Default/Utf8/Sgr
  2. terminal-screen 上监听 on:mousedown / on:mouseup / on:mousemove
    • 把像素坐标换算成终端 行列(用已测量的 cell 宽高 + 容器 rect
    • 按当前 encoding 编码:
      • SGR(最常用):ESC[<b;col;row;M(按下)/ m(释放)。
      • DefaultESC[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_inputwheel_delta_to_rowsvirtual_window),用 cargo test -p app --lib terminal:: 锁定,浏览器只做交互验证。