Skip to content

Commit 8f91842

Browse files
committed
Notify the view when a full reset replaces the buffer
resetToInitialState() rebuilds the buffer and then calls syncScrollArea() so the view can recompute its scroll geometry, but that method is an empty stub. On iOS contentSize/contentOffset are only recomputed by TerminalView.updateScroller(), and none of its four callers fire on this path: there is no buffer switch, no scrolled line, no keystroke and no resize. The scroll view therefore keeps the geometry of a buffer that no longer exists and renders blank until an unrelated layout pass corrects it. Quitting a full-screen TUI with Ctrl+C reproduces it: the program emits RIS while tearing down, and ensureCaretIsVisible() has already run at keypress time, before the reset. Notify bufferActivated instead of implementing syncScrollArea() itself: five of its eight call sites are keypad-mode changes that do not touch the buffer, and on iOS bufferActivated resets the scroll-follow state, so routing those through it would pull a user who has scrolled back down to the bottom.
1 parent 3917686 commit 8f91842

3 files changed

Lines changed: 58 additions & 1 deletion

File tree

Sources/SwiftTerm/Terminal.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5578,6 +5578,14 @@ open class Terminal {
55785578
cursorHidden = savedCursorHidden
55795579
refresh (startRow: 0, endRow: rows-1)
55805580
syncScrollArea ()
5581+
// A full reset replaces the buffer, so the view's scroll geometry — which
5582+
// is derived from `lines.count` and `yDisp` — is stale. `syncScrollArea()`
5583+
// is a no-op stub, and none of the other paths that recompute it fire
5584+
// here (no buffer switch, no scrolled line, no keystroke, no resize), so
5585+
// notify explicitly. Without this the view keeps the contentSize and
5586+
// contentOffset of a buffer that no longer exists and renders blank until
5587+
// some unrelated layout pass happens to correct it.
5588+
tdel?.bufferActivated (source: self)
55815589
}
55825590

55835591
// Support for:
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Testing
2+
@testable import SwiftTerm
3+
4+
/// A full reset (RIS, `ESC c`) replaces the buffer, so any view that derives its
5+
/// scroll geometry from `lines.count` / `yDisp` has to be told about it.
6+
/// `syncScrollArea()` is a no-op stub, and none of the other paths that make a
7+
/// view recompute that geometry run here — there is no buffer switch, no scrolled
8+
/// line, no keystroke and no resize — so `resetToInitialState()` notifies
9+
/// explicitly. Without that, a view keeps the contentSize/contentOffset of the
10+
/// buffer that was just discarded and renders blank.
11+
@Suite("Full reset scroll area")
12+
struct ResetScrollAreaTests {
13+
@Test("RIS notifies the delegate that the buffer changed")
14+
func fullResetNotifiesDelegate() {
15+
let (terminal, delegate) = TerminalTestHarness.makeTerminal(cols: 80, rows: 24, scrollback: 500)
16+
17+
// Build up scrollback so the reset is a real change in buffer size.
18+
for i in 0..<200 {
19+
terminal.feed(text: "line \(i)\r\n")
20+
}
21+
#expect(terminal.buffer.lines.count > terminal.rows)
22+
23+
let before = delegate.bufferActivatedCount
24+
terminal.feed(text: "\u{1b}c")
25+
26+
// The buffer really did shrink back to a single screen...
27+
#expect(terminal.buffer.lines.count == terminal.rows)
28+
#expect(terminal.buffer.yBase == 0)
29+
#expect(terminal.buffer.yDisp == 0)
30+
// ...and the delegate heard about it exactly once.
31+
#expect(delegate.bufferActivatedCount == before + 1)
32+
}
33+
34+
@Test("Keypad mode changes do not claim the buffer changed")
35+
func keypadModeDoesNotNotify() {
36+
let (terminal, delegate) = TerminalTestHarness.makeTerminal(cols: 80, rows: 24, scrollback: 500)
37+
38+
let before = delegate.bufferActivatedCount
39+
terminal.feed(text: "\u{1b}=") // application keypad
40+
terminal.feed(text: "\u{1b}>") // numeric keypad
41+
42+
// These also call syncScrollArea(), but they do not touch the buffer, so
43+
// they must not be reported as a buffer activation: on iOS that resets
44+
// the scroll-follow state and would yank a user who has scrolled back
45+
// down to the bottom.
46+
#expect(delegate.bufferActivatedCount == before)
47+
}
48+
}

Tests/SwiftTermTests/TerminalTestHarness.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Testing
33

44
final class TerminalTestDelegate: TerminalDelegate {
55
private(set) var sentData: [[UInt8]] = []
6+
private(set) var bufferActivatedCount = 0
67
var cellSizeInPixelsValue: (width: Int, height: Int)? = nil
78

89
func showCursor(source: Terminal) {}
@@ -13,7 +14,7 @@ final class TerminalTestDelegate: TerminalDelegate {
1314
func sizeChanged(source: Terminal) {}
1415
func scrolled(source: Terminal, yDisp: Int) {}
1516
func linefeed(source: Terminal) {}
16-
func bufferActivated(source: Terminal) {}
17+
func bufferActivated(source: Terminal) { bufferActivatedCount += 1 }
1718
func bell(source: Terminal) {}
1819

1920
func send(source: Terminal, data: ArraySlice<UInt8>) {

0 commit comments

Comments
 (0)