@@ -22,13 +22,17 @@ class ClickThroughTerminalView: LocalProcessTerminalView {
2222 private var pendingURL : URL ?
2323 private var pendingCWD : String ?
2424
25- // SwiftTerm forces yDisp back to yBase on every line scroll because its
26- // `userScrolling` flag is never set from the scroll wheel path. We track
27- // the live bottom (yBase) ourselves: after super.dataReceived runs, if
28- // yDisp moved we know it now equals yBase, so we record it. Status
29- // detection reads from this row so it sees the latest output even while
30- // the user is browsing scrollback; the dataReceived override also uses
31- // it to restore the user's manual scroll position after each chunk.
25+ // The buffer row of the live bottom (SwiftTerm's `yBase`, which it keeps
26+ // internal). Status detection reads the grid from this row so it sees the
27+ // latest output even while the user is browsing scrollback.
28+ //
29+ // We snapshot it from `yDisp` on every chunk that arrives while the
30+ // viewport is at the bottom — the one moment the two are equal. Recording
31+ // on *every* such chunk (rather than only when yDisp moved) matters: once
32+ // the scrollback fills up, `Terminal.scroll` stops advancing yBase and
33+ // recycles lines instead, so yDisp never changes again and a value that
34+ // went stale — from a resize, a font change, or a spell in scrollback —
35+ // would otherwise never be corrected.
3236 private var latestYBase : Int = 0
3337
3438 // SwiftTerm's NSTextInputClient implementation drops marked (preedit)
@@ -691,36 +695,29 @@ class ClickThroughTerminalView: LocalProcessTerminalView {
691695 allowMouseReporting = terminal. mouseMode != . off
692696
693697 let wasAlternate = terminal. isCurrentBufferAlternate
694- let preYDisp = terminal. buffer. yDisp
695- // Only treat the viewport as "in scrollback" on the normal buffer.
696- // The alternate buffer (vim/less) has no scrollback and its yDisp is
697- // unrelated to latestYBase, so comparing them would spuriously fire.
698- let wasInScrollback = !wasAlternate && preYDisp < latestYBase
699698
700699 super. dataReceived ( slice: slice)
701700 hasNewData = true
702701
703702 // Re-read the buffer: super may have switched buffers (entering or
704- // leaving vim via \e[?1049h/l). yDisp isn't comparable across that
705- // switch, so skip all scroll bookkeeping unless we stayed on the
706- // normal buffer for the whole chunk. Without this guard, leaving the
707- // alternate screen runs scrollTo on the freshly-restored normal
708- // buffer and snaps it to the top.
703+ // leaving vim via \e[?1049h/l). The alternate buffer's yDisp is
704+ // unrelated to the normal buffer's live bottom, so only record when we
705+ // stayed on the normal buffer for the whole chunk.
709706 guard !wasAlternate, !terminal. isCurrentBufferAlternate else { return }
710707
711- // Snapshot the new yBase so extractAllLines can read the live bottom
712- // even when the viewport is parked in scrollback.
713- if terminal. buffer. yDisp != preYDisp {
708+ if isViewportAtBottom {
714709 latestYBase = terminal. buffer. yDisp
715710 }
711+ }
716712
717- // Only restore the viewport if the user was already browsing
718- // scrollback before this chunk arrived. When the user is at the
719- // bottom (preYDisp == old latestYBase), let SwiftTerm's auto-scroll
720- // keep them there.
721- if wasInScrollback {
722- scrollTo ( row: preYDisp, notifyAccessibility: false )
723- }
713+ /// Whether the viewport is following the live bottom rather than parked in
714+ /// scrollback — the only moment `yDisp` is guaranteed to equal `yBase`.
715+ ///
716+ /// `scrollPosition` reports 1 once `yDisp` reaches the last scrollback row,
717+ /// and `canScroll` is false when the whole buffer fits on screen, which
718+ /// makes the viewport trivially "at the bottom".
719+ private var isViewportAtBottom : Bool {
720+ !canScroll || scrollPosition >= 1
724721 }
725722
726723 private func evaluateStatus( for id: UUID ) {
0 commit comments