282 lines
7.9 KiB
Markdown
282 lines
7.9 KiB
Markdown
# 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 后历史视图是否稳定
|
||
|