Skip to content

Commit c45f2f3

Browse files
committed
Keep latestYBase fresh once scrollback is full
Recording the live bottom only when yDisp moved worked until the scrollback filled up: from then on Terminal.scroll recycles lines instead of advancing yBase, so yDisp stops changing and any stale value — from a resize, a font change, or a spell in scrollback — was never corrected, leaving status detection reading the wrong row. Snapshot yDisp on every chunk that arrives while the viewport is at the bottom instead, which is the one moment yDisp equals yBase. That also removes the manual scrollTo restore: SwiftTerm already keeps the viewport put when the user is browsing scrollback.
1 parent aff3e12 commit c45f2f3

1 file changed

Lines changed: 24 additions & 27 deletions

File tree

Notchy/TerminalManager.swift

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)