fix(terminal): prevent doomscroll when reviewing scrollback (#1616)

The synchronizeScrollView() method was constantly resetting the scroll
position to match the terminal's scrollbar state, even when the user had
manually scrolled up to review scrollback. This caused the 'doomscroll'
bug where panes would fight the user's scroll position.

- Add userScrolledAwayFromBottom flag to track scroll intent
- Only auto-scroll when at bottom or when scrollbar indicates following
- Reset flag when user scrolls back to bottom
- Add 5-point threshold to tolerate minor float drift

Fixes #1577

Co-authored-by: BillionClaw <267901332+BillionClaw@users.noreply.github.com>
This commit is contained in:
BillionToken 2026-03-18 08:52:59 +08:00 committed by GitHub
parent c0fd15cdd7
commit 1fabe9f33c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -6106,6 +6106,12 @@ final class GhosttySurfaceScrollView: NSView {
private var windowObservers: [NSObjectProtocol] = []
private var isLiveScrolling = false
private var lastSentRow: Int?
/// Tracks whether the user has scrolled away from the bottom to review scrollback.
/// When true, auto-scroll should be suspended to prevent the "doomscroll" bug
/// where the terminal fights the user's scroll position.
private var userScrolledAwayFromBottom = false
/// Threshold in points from bottom to consider "at bottom" (allows for minor float drift)
private static let scrollToBottomThreshold: CGFloat = 5.0
private var isActive = true
private var lastFocusRefreshAt: CFTimeInterval = 0
private var activeDropZone: DropZone?
@ -6433,6 +6439,8 @@ final class GhosttySurfaceScrollView: NSView {
queue: .main
) { [weak self] _ in
self?.isLiveScrolling = false
// Final scroll position check to update userScrolledAwayFromBottom state
self?.handleLiveScroll()
})
observers.append(NotificationCenter.default.addObserver(
@ -8349,11 +8357,29 @@ final class GhosttySurfaceScrollView: NSView {
let offsetY =
CGFloat(scrollbar.total - scrollbar.offset - scrollbar.len) * cellHeight
let targetOrigin = CGPoint(x: 0, y: offsetY)
if !pointApproximatelyEqual(scrollView.contentView.bounds.origin, targetOrigin) {
// Check if we're currently at the bottom (with threshold for float drift)
let currentOrigin = scrollView.contentView.bounds.origin
let documentHeight = documentView.frame.height
let viewportHeight = scrollView.contentView.bounds.height
let distanceFromBottom = documentHeight - currentOrigin.y - viewportHeight
let isAtBottom = distanceFromBottom <= Self.scrollToBottomThreshold
// Update userScrolledAwayFromBottom based on current position
if isAtBottom {
userScrolledAwayFromBottom = false
}
// Only auto-scroll if user hasn't manually scrolled away from bottom
// or if we're following terminal output (scrollbar shows we're at bottom)
let shouldAutoScroll = !userScrolledAwayFromBottom ||
(scrollbar.offset + scrollbar.len >= scrollbar.total)
if shouldAutoScroll && !pointApproximatelyEqual(currentOrigin, targetOrigin) {
#if DEBUG
logDragGeometryChange(
event: "scrollOrigin",
old: scrollView.contentView.bounds.origin,
old: currentOrigin,
new: targetOrigin
)
#endif
@ -8380,6 +8406,14 @@ final class GhosttySurfaceScrollView: NSView {
let visibleRect = scrollView.contentView.documentVisibleRect
let documentHeight = documentView.frame.height
let scrollOffset = documentHeight - visibleRect.origin.y - visibleRect.height
// Track if user has scrolled away from bottom to review scrollback
if scrollOffset > Self.scrollToBottomThreshold {
userScrolledAwayFromBottom = true
} else if scrollOffset <= 0 {
userScrolledAwayFromBottom = false
}
let row = Int(scrollOffset / cellHeight)
guard row != lastSentRow else { return }