From d01d37c8c0c7c723b9e2c90aa43fd81b3f67f80c Mon Sep 17 00:00:00 2001 From: Zabir Raihan Date: Thu, 26 Mar 2026 20:26:11 +0600 Subject: [PATCH] fix auto-scroll and selection during active output Set terminal.userScrolling when the user scrolls away from the bottom so incoming output no longer yanks the viewport back down. Also stop clearing selection on every feed/linefeed unless the hosted app has actually requested mouse reporting (mouseMode != .off). Co-Authored-By: Claude Haiku 4.5 --- .../SwiftTerm/Apple/AppleTerminalView.swift | 14 +++++++---- Sources/SwiftTerm/Mac/MacTerminalView.swift | 6 +++-- Sources/SwiftTerm/iOS/iOSTerminalView.swift | 24 ++++++++++++++----- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/Sources/SwiftTerm/Apple/AppleTerminalView.swift b/Sources/SwiftTerm/Apple/AppleTerminalView.swift index 2cfbda2f3..890cd71b4 100644 --- a/Sources/SwiftTerm/Apple/AppleTerminalView.swift +++ b/Sources/SwiftTerm/Apple/AppleTerminalView.swift @@ -1927,10 +1927,14 @@ extension TerminalView { let displayBuffer = terminal.displayBuffer if row != displayBuffer.yDisp { terminal.setViewYDisp (row) - + + // Track whether the user has scrolled away from the bottom so that + // incoming output does not yank the viewport back down. + terminal.userScrolling = displayBuffer.yDisp != displayBuffer.yBase + // tell the terminal we want to refresh all the rows terminal.refresh (startRow: 0, endRow: terminal.rows) - + // do the display update updateDisplay (notifyAccessibility: notifyAccessibility) //selectionView.notifyScrolled(source: terminal) @@ -1978,8 +1982,10 @@ extension TerminalView { func feedPrepare() { search.invalidate() - // Preserve manual selection while output is streaming when mouse reporting is disabled. - if allowMouseReporting { + // Only clear selection when the hosted app has actually requested mouse + // reporting — otherwise the user should be able to select text while + // output is streaming. + if allowMouseReporting && terminal.mouseMode != .off { selection.active = false } startDisplayUpdates() diff --git a/Sources/SwiftTerm/Mac/MacTerminalView.swift b/Sources/SwiftTerm/Mac/MacTerminalView.swift index 93eac10cf..ffe931920 100644 --- a/Sources/SwiftTerm/Mac/MacTerminalView.swift +++ b/Sources/SwiftTerm/Mac/MacTerminalView.swift @@ -762,8 +762,10 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations, } open func linefeed(source: Terminal) { - // Preserve manual selection while output is streaming when mouse reporting is disabled. - if allowMouseReporting { + // Only clear selection when the hosted app has actually requested mouse + // reporting — otherwise the user should be able to select text while + // output is streaming. + if allowMouseReporting && terminal.mouseMode != .off { selection.selectNone() } } diff --git a/Sources/SwiftTerm/iOS/iOSTerminalView.swift b/Sources/SwiftTerm/iOS/iOSTerminalView.swift index ccc2048d1..92821a25e 100644 --- a/Sources/SwiftTerm/iOS/iOSTerminalView.swift +++ b/Sources/SwiftTerm/iOS/iOSTerminalView.swift @@ -1418,8 +1418,10 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi } open func linefeed(source: Terminal) { - // Preserve manual selection while output is streaming when mouse reporting is disabled. - if allowMouseReporting { + // Only clear selection when the hosted app has actually requested mouse + // reporting — otherwise the user should be able to select text while + // output is streaming. + if allowMouseReporting && terminal.mouseMode != .off { selection.selectNone() disableSelectionPanGesture() } @@ -1430,10 +1432,10 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi let displayBuffer = terminal.displayBuffer contentSize = CGSize (width: CGFloat (displayBuffer.cols) * cellDimension.width, height: CGFloat (displayBuffer.lines.count) * cellDimension.height) - //contentOffset = CGPoint (x: 0, y: CGFloat (displayBuffer.lines.count-displayBuffer.rows)*cellDimension.height) - contentOffset = CGPoint (x: 0, y: CGFloat (displayBuffer.lines.count-displayBuffer.rows)*cellDimension.height) - //Xscroller.doubleValue = scrollPosition - //Xscroller.knobProportion = scrollThumbsize + // Only scroll to bottom if the user hasn't explicitly scrolled up + if !terminal.userScrolling { + contentOffset = CGPoint (x: 0, y: CGFloat (displayBuffer.lines.count-displayBuffer.rows)*cellDimension.height) + } } #if canImport(MetalKit) @@ -1541,6 +1543,15 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi requestMetalDisplay() } #endif + // When the user is actively scrolling (dragging or decelerating), + // track whether they have scrolled away from the bottom so incoming + // output does not yank the viewport back down. + if isDragging || isDecelerating { + let displayBuffer = terminal.displayBuffer + let bottomOffset = CGFloat(displayBuffer.lines.count - displayBuffer.rows) * cellDimension.height + let atBottom = contentOffset.y >= bottomOffset - 1 + terminal.userScrolling = !atBottom + } } } @@ -2178,6 +2189,7 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi guard !terminal.synchronizedOutputActive else { return } let displayBuffer = terminal.displayBuffer contentOffset = CGPoint (x: 0, y: CGFloat (displayBuffer.lines.count-displayBuffer.rows)*cellDimension.height) + terminal.userScrolling = false } public func deleteBackward() {