Files
rustui-playground/TERMINAL_ROADMAP.md
zhangheng f76d341501 feat(terminal): Chinese / IME input (P3)
Implement IME composition support and migrate regular character input to
a hidden textarea, matching the xterm.js approach.

Key changes:
- Add a hidden, absolutely-positioned `<textarea>` inside `.terminal-screen`
  that follows the terminal cursor cell. It receives focus on click, connect,
  and resize so the OS/browser IME candidate window appears at the cursor.
- `on:input` now sends regular printable characters; `on:keydown` only handles
  special keys (arrows, Enter, Esc, Backspace, F-keys, Ctrl combos).
- `on:compositionstart/end` track IME sessions and send the final composed
  string at the end, avoiding partial pinyin/jamo/kana leaks.
- Move `on:paste` and `on:keydown` from `.terminal-screen` to the textarea so
  focus stays on the IME target.
- Replace `.terminal-screen:focus` with `:focus-within` since the screen div
  no longer holds focus.
- Add `is_regular_text_input` helper and unit test.

Web-sys features extended with `InputEvent` and `CompositionEvent`.

Tests: 24/24 pass, clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 16:09:45 +08:00

12 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 面。

性能目标:当前 DOM 渲染在 80×24160×40 的常规终端下足够流畅但大终端≥200×100或高刷新率连续输出场景会出现 DOM 节点过多、重排重绘开销大的问题。因此渲染器最终要迁到 Canvas / WebGL,这是明确的长期目标,会在 P3 之后按优先级推进。


1. 覆盖度自评(截至本文档)

按用途粗估:

  • 「跑通真实终端会话」核心用途:约 7580%
  • xterm.js 完整 API / 特性面:约 2025%

差距集中在输入完整性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 滚轮 → 方向键翻页rAF 合并多次 wheel 事件,单次滑动最多一屏) 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 / 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_*

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当选区存在时 Ctrl+C 优先复制(与 VS Code / Windows Terminal 一致),无选区时才发送 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. 浏览器快捷键仲裁handle_keydown 对 bare Ctrl+字母(无 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

  4. 测试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 滚轮均可以工作。

已实现

  1. vt100 mouse-mode accessorscore.rs::mouse_protocol_mode / mouse_protocol_encoding)—— 暴露 None/Press/PressRelease/ButtonMotion/AnyMotionDefault/Utf8/Sgr
  2. 事件监听与坐标换算component.rs
    • on:mousedown / on:mouseup / on:mousemove 复用现有的 DOM 事件。
    • pixel_to_grid(clientX, clientY) 换算成 0-based 可见网格 (row, col)(注意:与选区用的 point_to_cell 不同,这里不加 scroll_top,因为鼠标报告坐标是相对于应用正在绘制的当前屏幕,而非 scrollback 缓冲区)。
  3. 编码encode_mouse_report 纯函数)
    • SGR1006ESC[<b;col;row;M(按下/移动)/ ESC[<b;col;row;m释放。ASCII-only任意屏幕尺寸都正确。
    • Default / UTF-8ESC[M + 三字节(各 +32。坐标 ≤94 时完全正确并 ASCII-safe更大坐标被钳位到 94大多数终端 likewise现代应用都走 SGR
    • 滚轮:垂直/水平滚轮映射为按钮 64/65/66/67 的 Press 事件。
  4. 模式仲裁
    • 应用开启 mouse reportingmode != None)时,鼠标事件优先发给应用;否则走本地选区。
    • 按住 Shift 点击可绕过鼠标上报、强制本地选区xterm 行为)。
    • 鼠标移动按网格单元去重,避免每像素都发报告。
  5. 测试encode_mouse_reportpixel_to_gridwith_mouse_modifiersmouse_button_base 均有单测23 个 terminal 测试全过clippy 干净。

已知限制

  • 主屏幕(非 alt-screen滚动后鼠标报告坐标仍相对于可见网格顶部因为开启鼠标上报的应用通常运行在 alt-screen这覆盖主要使用场景。
  • 与浏览器 Ctrl+W/T/N 类似,浏览器可能保留某些鼠标手势(如右键菜单、拖拽文本),这些在网页终端内无法 100% 拦截。应用内鼠标操作应正常。

P3 — 中文 / IME 输入 已完成

现状:已实现。拼音、日文、韩文等 IME 组合输入可以正常工作;普通英文/数字输入也迁到了 textareainput 事件,确保和 IME 走同一条路径。

已实现

  1. 隐藏 <textarea> IME 载体component.rs view
    • 绝对定位、透明、无边框、大小跟随终端光标单元格。
    • 通过 tabindex="0" 成为焦点目标;terminal-screen 改用 :focus-within 显示焦点环。
  2. 事件模型迁移
    • on:input:普通字符/数字/空格输入,读取 textarea value 后清空并发送。
    • on:compositionstart / on:compositionendIME 组合期间不发字节,compositionend 时发送最终文本。
    • on:keydown仍处理方向键、Enter、Esc、Backspace、F 键、Ctrl 组合等非打印键
    • on:paste:从 terminal-screen 移到 textarea确保 focus 在 textarea 时右键/菜单粘贴也能触发。
  3. 焦点管理
    • 点击 terminal-screen、WebSocket 连接成功、窗口 resize 后都聚焦 IME textarea。
  4. 测试:新增 is_regular_text_input_detects_printable_keys 单测,确保打印字符被正确交给 textarea24 个 terminal 测试全过clippy 干净。

已知限制

  • 主屏幕 scrollback 滚动后光标可能位于可见区域下方IME 候选框位置会对应到光标逻辑位置可能偏下。IME 主要用于当前输入, scrolled-back 场景输入较少见。
  • 目前不支持 textarea 自动滚动延伸选区(和 P0 同一限制)。

P4 — Canvas / WebGL 渲染器(性能目标)

现状DOM 渲染。常规 80×24160×40 终端流畅但大终端≥200×100或高刷新率连续输出时DOM 节点多、重排重绘开销大。

计划实现

  1. Canvas 2D 渲染器(先做)
    • 一屏单元格一次 fillText / fillRect 绘制,减少 DOM 节点。
    • 保留现有 TerminalCore 数据模型,渲染层换成 Canvas。
    • 文本选区用 Canvas 半透明矩形;光标用 Canvas 反色或额外 DOM overlay。
  2. WebGL 渲染器(后做)
    • 把字符集预渲染到 texture atlas用 shader 批量绘制。
    • 适合超大终端 + 高刷新率场景,但实现复杂度高。
  3. 可切换
    • 默认 DOM提供 prop/api 切到 Canvas/WebGL。

风险:高。需要重新实现渲染层、文本测量、选区高亮、滚动逻辑,同时保证和现有 TerminalCore 数据模型兼容。


4. 建议推进顺序

P0 复制粘贴(先粘贴→可见区复制)  ← 最先,体验提升最大
  ↓
P1 完整功能键 + application-cursor
  ↓
P2 鼠标上报
  ↓
P3 中文 IME(放在功能键之后,避免输入逻辑冲突)
  ↓
P4 Canvas/WebGL 渲染器(满足大终端 + 高刷新率性能目标)

每一项都应延续现有做法:纯逻辑抽成可单测的函数(如 map_key_to_terminal_inputwheel_delta_to_rowsvirtual_window),用 cargo test -p app --lib terminal:: 锁定,浏览器只做交互验证。