refactor(terminal): explicit history buffer + virtual scrolling

Replace the spacer-estimate scrollback model with an explicit history
row buffer owned by TerminalCore, rendered with absolute-positioned
virtual scrolling so huge outputs (cat of 50k-line files) stay smooth.

core.rs:
- TerminalCore owns history_rows + a cached screen, captured after each
  process() via a bounded "conveyor belt" read of vt100 scrollback
- feed bytes in newline-bounded safe slices so a capture never reads
  deeper than viewport_rows (works around vt100 0.15 visible_rows
  subtract-overflow panic)
- handle clear (ESC[3J) by wiping our own history buffer
- TerminalSegment carries grid column count for exact-width rendering
- collect_window() clones only the visible slice

component.rs:
- TerminalCore lives in an RwSignal; render driven by an rAF-coalesced
  render_tick so packet bursts collapse into one frame
- virtual_window pins to the bottom in live mode, follows scroll_top in
  history mode; absolute-positioned rows in a fixed-height sizer keep
  scroll geometry constant (fixes the "keeps drifting up" bounce)
- position-based programmatic-scroll detection replaces the racy
  ignore-next-scroll flag
- alt-screen wheel events translate to arrow keys (vim/less paging)
- try_* signal access guards against disposal during hydration

style: lock row height to 18px, segment width to Nch (exact grid so
fallback-font glyphs like btm braille can't push the row sideways),
absolute-position the virtual rows layer.

Adds 12 unit tests (capture, clear, alt-screen, window slicing, wheel
mapping) and TERMINAL_ROADMAP.md tracking xterm.js parity work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zhangheng
2026-06-11 19:51:06 +08:00
parent fd056ce502
commit a018609a28
5 changed files with 1410 additions and 607 deletions

View File

@@ -631,16 +631,23 @@
}
.terminal-grid {
@apply flex min-h-full flex-col;
position: relative;
display: block;
min-height: 100%;
}
.terminal-row {
height: 18px;
min-height: 18px;
max-height: 18px;
overflow: hidden;
white-space: pre;
}
.terminal-segment {
display: inline-block;
overflow: hidden;
vertical-align: top;
white-space: pre;
}
@@ -650,11 +657,23 @@
box-shadow: 0 0 0 1px rgb(219 234 254 / 30%);
}
.terminal-scroll-spacer {
/* Absolute-positioning virtual scroll: the sizer holds the full content
height so the scroll geometry is constant; the rows layer is translated
to the visible window's offset. */
.terminal-virtual-sizer {
display: block;
width: 100%;
pointer-events: none;
}
.terminal-virtual-rows {
position: absolute;
top: 0;
left: 0;
right: 0;
will-change: transform;
}
.terminal-measure {
position: absolute;
left: -9999px;