feat: add rust browser terminal prototype

This commit is contained in:
zhangheng
2026-06-08 16:34:13 +08:00
commit fd056ce502
43 changed files with 10312 additions and 0 deletions

281
TERMINAL_HANDOFF.md Normal file
View File

@@ -0,0 +1,281 @@
# Terminal Handoff
本文档用于把 `base-path-demo` 当前浏览器终端实现的状态、已验证结论、未解决问题和下一步建议整理清楚,方便后续继续开发。
## 1. 当前目标
项目目标不是简单接一个现成 JS 终端,而是在 `base-path-demo` 中逐步沉淀一套 Rust 终端能力:
- 后端:`Axum + portable-pty`
- 前端:`Leptos + Rust/WASM`
- 长期方向:抽出可复用 Rust 终端组件库
当前已经完成:
- PTY 启动与 shell 会话
- WebSocket 双向通信
- Rust/WASM 终端渲染 MVP
- ANSI 样式分段渲染
- 宽高测量与 PTY resize
- 基础 scrollback/history 浏览能力
当前仍未稳定完成:
- 长历史滚动后的视口一致性
- 从 history 模式返回 live 模式时的可靠定位
- 超长输出下的 scrollback / DOM / parser 三者完全一致
## 2. 当前实现位置
### 后端
- `server/src/terminal/pty_session.rs`
- `server/src/terminal/ws.rs`
- `server/src/main.rs`
### 前端
- `app/src/terminal/core.rs`
- `app/src/terminal/component.rs`
- `app/src/terminal/scrollback.rs`
- `app/src/terminal/protocol.rs`
- `app/src/pages/terminal.rs`
- `style/tailwind.css`
## 3. 当前前端结构
当前前端已经从“组件里直接推导滚动”改成三层:
1. `TerminalCore`
- 基于 `vt100::Parser`
- 负责终端字节流解析
- 负责从 parser 生成 `TerminalSnapshot`
- 暴露 `snapshot_live()``snapshot_for_top_row()`
2. `ScrollbackModel`
- 位于 `app/src/terminal/scrollback.rs`
- 负责管理:
- `live/history` 状态
- `top_row`
- `ScrollMetrics`
- `scrollTop <-> top_row` 映射
- spacer 渲染计划
3. `TerminalPanel`
- 负责 DOM 事件和 WebSocket
- 把滚动、输入、resize 转换为 model 更新
- 再根据 model 请求 `TerminalCore` 产出 snapshot
这是一次结构性重构不再依赖前面几轮“scrollback_offset / scrollTop / spacer”互相修补的逻辑。
## 4. 已确认有效的部分
这些能力在当前实现里基本是成立的:
- WebSocket 通信链路稳定
- shell 启动和 PTY 读写稳定
- 普通命令行输入输出稳定
- `btm` 这种全屏 TUI 在 live 模式下效果已经明显优于最初版本
- 宽度和高度测量相比初版更合理
- 终端样式、ANSI 颜色和 cursor 基本可用
## 5. 当前未解决问题
用户最新反馈:滚动相关问题仍然存在。
典型症状:
1. 滚动到高处后,再滚回底部,视图仍可能错位。
2. 认为已经回到底部,但看不到当前 prompt 或新输出。
3. 某些情况下顶部会出现大块空白。
4. history 模式和 live 模式切换仍然不完全符合真实终端体验。
## 6. 为什么当前结构仍可能不稳
虽然已经抽出了 `ScrollbackModel`,但当前方案本质上仍然依赖一个前提:
> 用 DOM 滚动条 + spacer 高度,去模拟“完整终端历史缓冲区”。
这条路的优势是实现快,但仍有天然风险:
### 6.1 `vt100` 只给出“当前视口”
`vt100` 当前更适合表达:
- 当前可见 screen
- 当前 scrollback offset
它不是为浏览器里的“无限历史虚拟滚动列表”直接设计的 UI 数据结构。
也就是说,浏览器里看到的“长历史列表”不是 parser 原生给出来的,而是我们通过 spacer + 视口切换模拟出来的。
### 6.2 DOM 滚动不是终端滚动
浏览器滚动条是像素系统,终端历史是行系统。
即使我们做了:
- 行高量化
- near-bottom 阈值
- top row model
- 程序滚动忽略回流
仍然可能在以下场景出现边界问题:
- resize 后 scrollHeight 改变
- 某些行因字符宽度或字体 fallback 产生高度/宽度偏差
- cursor / prompt / wrapped line 在 parser 里和 DOM 里对应关系不完全一致
### 6.3 spacer 模型仍然是“UI 侧估计”
现在的 spacer 高度来自:
- `scrollback_rows`
- `viewport_rows`
- `top_row`
但这仍然是假设“每一逻辑行高度固定、所有历史行都可用同一高度表达”。
对于普通命令输出通常够用,但当用户期待系统终端级别的一致性时,仍可能暴露问题。
## 7. 建议的下一步方向
不建议继续围绕当前 spacer 方案做大量微调。
### 推荐方向 A显式历史行缓冲
最推荐。
思路:
1. 在前端维护一份显式的“历史行缓冲”。
2. 每次收到 PTY 输出时,不只保留当前 snapshot还要把可确认滚出屏幕的行纳入历史缓冲。
3. 浏览器滚动条直接绑定:
- `history_rows`
- `visible_rows`
4. 当前视图渲染为:
- 历史区真实行
- 当前 screen 行
这样浏览器滚动就不再依赖 spacer 去“伪装总高度”,而是真正拥有一份历史数据模型。
更具体地说,应当抽成类似:
```text
terminal-core
parser
screen
scrollback_store
viewport_state
terminal-leptos
renderer
dom scroll adapter
input adapter
```
建议引入一个新的模型,例如:
```rust
pub struct TerminalBufferModel {
pub history_rows: Vec<RenderedRow>,
pub screen_rows: Vec<RenderedRow>,
pub viewport_rows: usize,
pub mode: ViewMode,
}
```
其中:
- `history_rows` 是真实可滚动历史
- `screen_rows` 是当前 parser screen
- `ViewMode` 表示 live/history
然后滚动行为只是在这份显式数据上切片不再通过“scrollback offset + spacer”来推导。
### 推荐方向 B虚拟列表化
如果历史很多,后续要避免一次性渲染全部历史行。
那就基于显式历史行缓冲再做:
- 虚拟滚动窗口
- 仅渲染当前可见行切片
- 上下填充高度由真实行数计算
这个方向适合在方向 A 稳定后再做。
### 不推荐继续投入的方向
除非只是临时验证,否则不建议继续在下面这些点上做大量时间投入:
- 单纯继续调 `scrollTop -> topRow` 的换算公式
- 继续加更多“忽略下一次 scroll 事件”之类的回流保护
- 继续依赖 spacer 修正 live/history 错位
这些都只能减轻问题,不太可能从结构上彻底解决。
## 8. 一个更稳的实施顺序
建议下一位接手的人按这个顺序推进:
1. 先保留当前 PTY / WS / 输入输出链路,不动后端。
2. 在前端新增显式历史缓冲层,不急着删除现有 `ScrollbackModel`
3. 把当前渲染改成:
- `history_rows + screen_rows`
- 不再通过 `snapshot.scrollback_offset` 去制造大块 spacer
4. 先让滚动逻辑只在显式数据上成立。
5. 确认:
- 滚到顶部没有空白
- 滚回底部一定能恢复 prompt
- history 模式继续输入一定回 live
6. 再考虑是否保留 `vt100` 的 scrollback offset 接口,还是让它只负责 screen而历史完全交给自有 buffer model。
## 9. 当前代码是否值得保留
值得保留的部分:
- `portable-pty` 后端
- Axum WebSocket 协议与会话层
- `TerminalCore` 的 ANSI 分段渲染能力
- 宽高测量和 PTY resize
- `ScrollbackModel` 里关于 live/history 的概念划分
可以重做的部分:
- 终端历史滚动的底层表现方式
- spacer 方案本身
- DOM scroll 与 parser scrollback 的耦合方式
## 10. 建议的 handoff 结论
当前项目已经证明:
- Rust 后端 PTY 是可行的
- Rust/WASM 终端组件是可行的
- `btm` 和普通 shell 已经能跑
但要让它真正成为一个稳定的 Rust 终端组件库下一步不应继续在“spacer + 当前视口推导”上做小修小补,而应转向:
> 显式历史缓冲模型 + 真实终端行数据结构 + 可选虚拟滚动
这会更接近真正终端组件库内部的设计,也更利于后续抽成 crate。
## 11. 当前验证命令
建议继续用下面这些命令回归:
- `pwd`
- `ls`
- `cat <long-file>`
- `btm`
- `top` / `htop`(如果环境可用)
- 连续回车制造长历史
重点检查:
- 滚到顶部是否有空白
- 滚回底部是否恢复 prompt
- history 模式输入后是否回到 live
- resize 后历史视图是否稳定