From fe3c6d09640ef79a1ae61120d7e9fa9fd1c96411 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 16 Sep 2026 16:08:40 -0700 Subject: [PATCH 01/16] test: cover terminal cell metrics across split reflow --- cmux.xcodeproj/project.pbxproj | 8 ++ .../TerminalPaneMetricInvalidationTests.swift | 76 +++++++++++ cmuxTests/TerminalPaneMetricsFixture.swift | 121 ++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 cmuxTests/TerminalPaneMetricInvalidationTests.swift create mode 100644 cmuxTests/TerminalPaneMetricsFixture.swift diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 3112d834a00b..257633d1d265 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -3341,6 +3341,8 @@ C0DE6B420000000000000001 /* TerminalPanelTextBoxState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE6B420000000000000002 /* TerminalPanelTextBoxState.swift */; }; 6819AF60AEA6455DB57CC7FE /* TerminalPanelUnavailableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811B03E4004A4D1FB6650158 /* TerminalPanelUnavailableView.swift */; }; A5001403 /* TerminalPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001413 /* TerminalPanelView.swift */; }; + 123810000000000000000002 /* TerminalPaneMetricInvalidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000001 /* TerminalPaneMetricInvalidationTests.swift */; }; + 123810000000000000000004 /* TerminalPaneMetricsFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000003 /* TerminalPaneMetricsFixture.swift */; }; B88351000000000000000001 /* TerminalPasteboardReadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88351000000000000000002 /* TerminalPasteboardReadRequest.swift */; }; B88355000000000000000001 /* TerminalPastePreparationDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88355000000000000000002 /* TerminalPastePreparationDestination.swift */; }; B88357000000000000000009 /* TerminalPastePreparationFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8835700000000000000000A /* TerminalPastePreparationFailure.swift */; }; @@ -7101,6 +7103,8 @@ C0DE6B420000000000000002 /* TerminalPanelTextBoxState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelTextBoxState.swift; sourceTree = ""; }; 811B03E4004A4D1FB6650158 /* TerminalPanelUnavailableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelUnavailableView.swift; sourceTree = ""; }; A5001413 /* TerminalPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelView.swift; sourceTree = ""; }; + 123810000000000000000001 /* TerminalPaneMetricInvalidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPaneMetricInvalidationTests.swift; sourceTree = ""; }; + 123810000000000000000003 /* TerminalPaneMetricsFixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPaneMetricsFixture.swift; sourceTree = ""; }; B88351000000000000000002 /* TerminalPasteboardReadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPasteboardReadRequest.swift; sourceTree = ""; }; B88355000000000000000002 /* TerminalPastePreparationDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPastePreparationDestination.swift; sourceTree = ""; }; B8835700000000000000000A /* TerminalPastePreparationFailure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPastePreparationFailure.swift; sourceTree = ""; }; @@ -10819,6 +10823,8 @@ C44550000000000000000002 /* PanelOwnedNativeViewSessionTests.swift */, 3069F1D10000000000000004 /* GhosttyPasteboardFidelityTests.swift */, D7AB34400000000000000004 /* GhosttyTerminalViewVisibilityPolicyTests.swift */, + 123810000000000000000003 /* TerminalPaneMetricsFixture.swift */, + 123810000000000000000001 /* TerminalPaneMetricInvalidationTests.swift */, D7AB00000000000000000012 /* WorkspaceAdjacentPaneMoveTests.swift */, 804100000000000000000004 /* ReorderShortcutActionTests.swift */, D7AB3605C10DEF0000000002 /* WorkspaceCloseTabsContextMenuTests.swift */, @@ -15678,6 +15684,8 @@ A5E380700000000000000001 /* TerminalNotificationSocketActionTests.swift in Sources */, 11189010A1B2C3D4E5F60718 /* TerminalNotificationSocketAttributionTests.swift in Sources */, 596100000000000000000007 /* TerminalNotificationStore+NativeNotificationDeliveryTesting.swift in Sources */, + 123810000000000000000002 /* TerminalPaneMetricInvalidationTests.swift in Sources */, + 123810000000000000000004 /* TerminalPaneMetricsFixture.swift in Sources */, C13519000000000000000013 /* TerminalPathEnvironmentTests.swift in Sources */, 126570000000000000000004 /* TerminalPortalTestWorkspace.swift in Sources */, D5671101D5671101D5671101 /* TerminalScrollSpeedSettingsFileStoreTests.swift in Sources */, diff --git a/cmuxTests/TerminalPaneMetricInvalidationTests.swift b/cmuxTests/TerminalPaneMetricInvalidationTests.swift new file mode 100644 index 000000000000..0780dff54020 --- /dev/null +++ b/cmuxTests/TerminalPaneMetricInvalidationTests.swift @@ -0,0 +1,76 @@ +import AppKit +import CmuxTerminal +import GhosttyKit +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +@MainActor +@Suite(.serialized) +struct TerminalPaneMetricInvalidationTests { + @Test func hiddenOutputReflowsThroughDividerDragAndSplitClose() throws { + let fixture = try TerminalPaneMetricsFixture() + defer { fixture.tearDown() } + // The original #12381 path: output arrives before the split is shown. + try fixture.writeRows() + try fixture.bind() + let originalCell = try #require(fixture.surface.cellSizePoints()) + let originalFont = ghostty_surface_font_size(try #require(fixture.surface.surface)) + for width: CGFloat in [280, 620, 360] { + try fixture.moveDivider(to: width) + try assertGridAndText(fixture, cell: originalCell, font: originalFont) + } + try fixture.closeSibling() + try assertGridAndText(fixture, cell: originalCell, font: originalFont) + } + + @Test func paneGeometryInvalidatesStaleLargerCellMetrics() throws { + let fixture = try TerminalPaneMetricsFixture() + defer { fixture.tearDown() } + try fixture.bind() + let expected = try #require(fixture.surface.cellSizePoints()) + // Simulate a delayed metric notification from the previous layout. + // Repair must come from each pane geometry boundary, with no window + // resize, config reload, or font-size mutation. + for closeSibling in [false, true] { + fixture.hosted.surfaceView.cellSize = CGSize(width: expected.width * 2, height: expected.height * 2) + if closeSibling { try fixture.closeSibling() } + else { try fixture.moveDivider(to: 340) } + #expect(fixture.hosted.surfaceView.cellSize == expected) + } + } + + @Test func sameFrameReconciliationRepairsMetricCache() throws { + let fixture = try TerminalPaneMetricsFixture() + defer { fixture.tearDown() } + try fixture.bind() + let expected = try #require(fixture.surface.cellSizePoints()) + let frame = fixture.hosted.frame + fixture.hosted.surfaceView.cellSize = CGSize(width: expected.width * 2, height: expected.height * 2) + _ = fixture.hosted.reconcileGeometryNow() + #expect(fixture.hosted.frame == frame) + #expect(fixture.hosted.surfaceView.cellSize == expected) + } + + private func assertGridAndText(_ fixture: TerminalPaneMetricsFixture, cell: CGSize, font: Float) throws { + let sample = try #require(fixture.surface.rawSizingSample()) + let runtime = try #require(fixture.surface.surface) + var grid = ghostty_surface_grid_metrics_s() + #expect(ghostty_surface_grid_metrics(runtime, &grid)) + #expect(Int(grid.columns) == sample.columns) + #expect(Int(grid.rows) == sample.rows) + #expect(fixture.surface.cellSizePoints() == cell) + #expect(ghostty_surface_font_size(runtime) == font) + let rows = try fixture.physicalRows() + #expect(rows.allSatisfy { $0.count <= sample.columns }) + let compact = rows.joined().filter { !$0.isWhitespace } + for index in 1...4 { + #expect(compact.components(separatedBy: "R\(index)").count == 2) + #expect(compact.components(separatedBy: "END\(index)").count == 2) + } + } +} diff --git a/cmuxTests/TerminalPaneMetricsFixture.swift b/cmuxTests/TerminalPaneMetricsFixture.swift new file mode 100644 index 000000000000..09b16d8460d9 --- /dev/null +++ b/cmuxTests/TerminalPaneMetricsFixture.swift @@ -0,0 +1,121 @@ +import AppKit +import CmuxTerminal +import GhosttyKit +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +/// A process-backed terminal in an actual vertical AppKit split. +@MainActor +final class TerminalPaneMetricsFixture { + let workspace = TerminalPortalTestWorkspace() + let window: NSWindow + let split = NSSplitView() + let anchor = NSView() + let sibling = NSView() + let surface: TerminalSurface + var hosted: GhosttySurfaceScrollView { surface.hostedView } + + init() throws { + window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 960, height: 640), + styleMask: [.titled, .closable], backing: .buffered, defer: false + ) + window.isReleasedWhenClosed = false + window.animationBehavior = .none + surface = TerminalSurface( + tabId: workspace.id, context: GHOSTTY_SURFACE_CONTEXT_SPLIT, + configTemplate: nil, initialCommand: "/bin/cat" + ) + try waitUntil { self.surface.surface != nil } + let content = try #require(window.contentView) + split.frame = content.bounds + split.isVertical = true + split.dividerStyle = .thin + content.addSubview(split) + split.addArrangedSubview(anchor) + split.addArrangedSubview(sibling) + split.adjustSubviews() + window.orderFront(nil) + window.displayIfNeeded() + } + + func bind() throws { + TerminalWindowPortalRegistry.bind( + hostedView: hosted, to: anchor, visibleInUI: true, + expectedSurfaceId: surface.id, + expectedGeneration: surface.portalBindingGeneration() + ) + hosted.setVisibleInUI(true) + try settle() + } + + func settle() throws { + try waitUntil { + self.window.contentView?.layoutSubtreeIfNeeded() + guard let sample = self.surface.rawSizingSample() else { return false } + let view = self.hosted.surfaceView + var grid = ghostty_surface_grid_metrics_s() + guard let runtime = self.surface.surface, + ghostty_surface_grid_metrics(runtime, &grid) else { return false } + return abs(self.hosted.frame.width - self.anchor.frame.width) < 1 && + abs(CGFloat(sample.surfaceWidthPx) - view.bounds.width * self.window.backingScaleFactor) < 2 && + sample.columns == Int(grid.columns) && sample.rows == Int(grid.rows) + } + } + + func moveDivider(to x: CGFloat) throws { + split.setPosition(x, ofDividerAt: 0) + TerminalWindowPortalRegistry.synchronizeForAnchor(anchor, syncLayout: false) + try settle() + } + + func closeSibling() throws { + sibling.removeFromSuperview() + split.adjustSubviews() + TerminalWindowPortalRegistry.synchronizeForAnchor(anchor, syncLayout: false) + try settle() + } + + func writeRows() throws { + let runtime = try #require(surface.surface) + let fill = String(repeating: "=", count: 74) + let output = "\u{1B}[2J\u{1B}[H" + (1...4).map { "R\($0)\(fill)END\($0)\r\n" }.joined() + output.withCString { ghostty_surface_process_output(runtime, $0, UInt(output.utf8.count)) } + } + + func physicalRows() throws -> [String] { + let runtime = try #require(surface.surface) + var text = ghostty_text_s() + let selection = ghostty_selection_s( + top_left: ghostty_point_s(tag: GHOSTTY_POINT_SURFACE, coord: GHOSTTY_POINT_COORD_TOP_LEFT, x: 0, y: 0), + bottom_right: ghostty_point_s(tag: GHOSTTY_POINT_SURFACE, coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT, x: 0, y: 0), + rectangle: true + ) + try #require(ghostty_surface_read_text(runtime, selection, &text)) + defer { ghostty_surface_free_text(runtime, &text) } + let pointer = try #require(text.text) + return String(decoding: UnsafeBufferPointer(start: pointer, count: Int(text.text_len)).map { UInt8(bitPattern: $0) }, as: UTF8.self) + .components(separatedBy: "\n") + } + + func waitUntil(_ predicate: () -> Bool) throws { + let deadline = ProcessInfo.processInfo.systemUptime + 5 + while !predicate(), ProcessInfo.processInfo.systemUptime < deadline { + RunLoop.main.run(until: Date().addingTimeInterval(0.005)) + } + try #require(predicate(), "Terminal geometry did not converge") + } + + func tearDown() { + TerminalWindowPortalRegistry.detach(hostedView: hosted) + surface.releaseSurfaceForTesting() + surface.teardownSurface() + window.close() + workspace.tearDown() + } +} From 6723ce05eab45820b03d3968d90bb84637ef2667 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 16 Sep 2026 16:15:05 -0700 Subject: [PATCH 02/16] test: reproduce lost resize completion and Retina metrics --- .../TerminalPortalResizeRecoveryTests.swift | 22 +++++++++++++ cmux.xcodeproj/project.pbxproj | 4 +++ .../TerminalPaneMetricInvalidationTests.swift | 29 ++++++++++++++++ cmuxTests/TerminalPaneMetricsFixture.swift | 33 +++++++++++-------- cmuxTests/TerminalPaneMetricsWindow.swift | 8 +++++ 5 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/TerminalPortalResizeRecoveryTests.swift create mode 100644 cmuxTests/TerminalPaneMetricsWindow.swift diff --git a/Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/TerminalPortalResizeRecoveryTests.swift b/Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/TerminalPortalResizeRecoveryTests.swift new file mode 100644 index 000000000000..cc0fa0e1a52c --- /dev/null +++ b/Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/TerminalPortalResizeRecoveryTests.swift @@ -0,0 +1,22 @@ +import CmuxTerminalCore +import Testing + +struct TerminalPortalResizeRecoveryTests { + @Test func observedResizeEndSchedulesFinalGeometryWithoutNotification() { + var phase = TerminalPortalResizePhase() + let acceptsGeometry3 = phase.observeNativeResize(active: true) + #expect(acceptsGeometry3) + #expect(phase.defersRenderer) + // Reparenting can remove a view before its end-live-resize callback. + // A later pane-layout event must finish the observed interaction. + let acceptsGeometry4 = phase.observeNativeResize(active: false) + #expect(acceptsGeometry4) + #expect(phase.isEnding) + #expect(phase.defersRenderer) + phase.commitEnd(nativeResizeActive: false) + #expect(!phase.defersRenderer) + let acceptsGeometry5 = phase.observeNativeResize(active: false) + #expect(acceptsGeometry5) + #expect(!phase.defersRenderer) + } +} diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 257633d1d265..f748904dd042 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -3343,6 +3343,7 @@ A5001403 /* TerminalPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001413 /* TerminalPanelView.swift */; }; 123810000000000000000002 /* TerminalPaneMetricInvalidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000001 /* TerminalPaneMetricInvalidationTests.swift */; }; 123810000000000000000004 /* TerminalPaneMetricsFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000003 /* TerminalPaneMetricsFixture.swift */; }; + 123810000000000000000008 /* TerminalPaneMetricsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000007 /* TerminalPaneMetricsWindow.swift */; }; B88351000000000000000001 /* TerminalPasteboardReadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88351000000000000000002 /* TerminalPasteboardReadRequest.swift */; }; B88355000000000000000001 /* TerminalPastePreparationDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88355000000000000000002 /* TerminalPastePreparationDestination.swift */; }; B88357000000000000000009 /* TerminalPastePreparationFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8835700000000000000000A /* TerminalPastePreparationFailure.swift */; }; @@ -7105,6 +7106,7 @@ A5001413 /* TerminalPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelView.swift; sourceTree = ""; }; 123810000000000000000001 /* TerminalPaneMetricInvalidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPaneMetricInvalidationTests.swift; sourceTree = ""; }; 123810000000000000000003 /* TerminalPaneMetricsFixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPaneMetricsFixture.swift; sourceTree = ""; }; + 123810000000000000000007 /* TerminalPaneMetricsWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPaneMetricsWindow.swift; sourceTree = ""; }; B88351000000000000000002 /* TerminalPasteboardReadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPasteboardReadRequest.swift; sourceTree = ""; }; B88355000000000000000002 /* TerminalPastePreparationDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPastePreparationDestination.swift; sourceTree = ""; }; B8835700000000000000000A /* TerminalPastePreparationFailure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPastePreparationFailure.swift; sourceTree = ""; }; @@ -10824,6 +10826,7 @@ 3069F1D10000000000000004 /* GhosttyPasteboardFidelityTests.swift */, D7AB34400000000000000004 /* GhosttyTerminalViewVisibilityPolicyTests.swift */, 123810000000000000000003 /* TerminalPaneMetricsFixture.swift */, + 123810000000000000000007 /* TerminalPaneMetricsWindow.swift */, 123810000000000000000001 /* TerminalPaneMetricInvalidationTests.swift */, D7AB00000000000000000012 /* WorkspaceAdjacentPaneMoveTests.swift */, 804100000000000000000004 /* ReorderShortcutActionTests.swift */, @@ -15686,6 +15689,7 @@ 596100000000000000000007 /* TerminalNotificationStore+NativeNotificationDeliveryTesting.swift in Sources */, 123810000000000000000002 /* TerminalPaneMetricInvalidationTests.swift in Sources */, 123810000000000000000004 /* TerminalPaneMetricsFixture.swift in Sources */, + 123810000000000000000008 /* TerminalPaneMetricsWindow.swift in Sources */, C13519000000000000000013 /* TerminalPathEnvironmentTests.swift in Sources */, 126570000000000000000004 /* TerminalPortalTestWorkspace.swift in Sources */, D5671101D5671101D5671101 /* TerminalScrollSpeedSettingsFileStoreTests.swift in Sources */, diff --git a/cmuxTests/TerminalPaneMetricInvalidationTests.swift b/cmuxTests/TerminalPaneMetricInvalidationTests.swift index 0780dff54020..d75d816791a6 100644 --- a/cmuxTests/TerminalPaneMetricInvalidationTests.swift +++ b/cmuxTests/TerminalPaneMetricInvalidationTests.swift @@ -12,6 +12,21 @@ import Testing @MainActor @Suite(.serialized) struct TerminalPaneMetricInvalidationTests { + @Test(arguments: [CGFloat(1), CGFloat(2)]) + func fontCallbacksUseCurrentLogicalCellSize(backingScale: CGFloat) throws { + let fixture = try TerminalPaneMetricsFixture(backingScale: backingScale) + defer { fixture.tearDown() } + try fixture.bind() + #expect(fixture.surface.performInternalBindingAction("set_font_size:24")) + #expect(fixture.surface.performInternalBindingAction("set_font_size:13")) + try fixture.waitUntil { + fixture.hosted.surfaceView.cellSize == fixture.surface.cellSizePoints() + } + let sample = try #require(fixture.surface.rawSizingSample()) + #expect(fixture.hosted.surfaceView.cellSize.width == CGFloat(sample.cellWidthPx) / backingScale) + #expect(fixture.hosted.surfaceView.cellSize.height == CGFloat(sample.cellHeightPx) / backingScale) + } + @Test func hiddenOutputReflowsThroughDividerDragAndSplitClose() throws { let fixture = try TerminalPaneMetricsFixture() defer { fixture.tearDown() } @@ -56,6 +71,20 @@ struct TerminalPaneMetricInvalidationTests { #expect(fixture.hosted.surfaceView.cellSize == expected) } + @Test func splitCloseRecoversAfterNativeResizeEndCallbackWasLost() throws { + let fixture = try TerminalPaneMetricsFixture() + defer { fixture.tearDown() } + try fixture.bind() + let portal = try #require(TerminalWindowPortalRegistry.portalsByWindowId[ObjectIdentifier(fixture.window)]) + portal.isWindowLiveResizeActiveOverrideForTesting = true + TerminalWindowPortalRegistry.synchronizeForAnchor(fixture.anchor, syncLayout: false) + #expect(portal.isRendererResizeDeferred) + portal.isWindowLiveResizeActiveOverrideForTesting = false + // No window notification: only the surviving pane's layout changes. + try fixture.closeSibling() + #expect(!portal.isRendererResizeDeferred) + } + private func assertGridAndText(_ fixture: TerminalPaneMetricsFixture, cell: CGSize, font: Float) throws { let sample = try #require(fixture.surface.rawSizingSample()) let runtime = try #require(fixture.surface.surface) diff --git a/cmuxTests/TerminalPaneMetricsFixture.swift b/cmuxTests/TerminalPaneMetricsFixture.swift index 09b16d8460d9..236daa047268 100644 --- a/cmuxTests/TerminalPaneMetricsFixture.swift +++ b/cmuxTests/TerminalPaneMetricsFixture.swift @@ -20,28 +20,35 @@ final class TerminalPaneMetricsFixture { let surface: TerminalSurface var hosted: GhosttySurfaceScrollView { surface.hostedView } - init() throws { - window = NSWindow( + init(backingScale: CGFloat = 1) throws { + let metricsWindow = TerminalPaneMetricsWindow( contentRect: NSRect(x: 0, y: 0, width: 960, height: 640), styleMask: [.titled, .closable], backing: .buffered, defer: false ) + metricsWindow.testBackingScale = backingScale + window = metricsWindow window.isReleasedWhenClosed = false window.animationBehavior = .none surface = TerminalSurface( tabId: workspace.id, context: GHOSTTY_SURFACE_CONTEXT_SPLIT, configTemplate: nil, initialCommand: "/bin/cat" ) - try waitUntil { self.surface.surface != nil } - let content = try #require(window.contentView) - split.frame = content.bounds - split.isVertical = true - split.dividerStyle = .thin - content.addSubview(split) - split.addArrangedSubview(anchor) - split.addArrangedSubview(sibling) - split.adjustSubviews() - window.orderFront(nil) - window.displayIfNeeded() + do { + try waitUntil { self.surface.surface != nil } + let content = try #require(window.contentView) + split.frame = content.bounds + split.isVertical = true + split.dividerStyle = .thin + content.addSubview(split) + split.addArrangedSubview(anchor) + split.addArrangedSubview(sibling) + split.adjustSubviews() + window.orderFront(nil) + window.displayIfNeeded() + } catch { + tearDown() + throw error + } } func bind() throws { diff --git a/cmuxTests/TerminalPaneMetricsWindow.swift b/cmuxTests/TerminalPaneMetricsWindow.swift new file mode 100644 index 000000000000..aac650e86c7e --- /dev/null +++ b/cmuxTests/TerminalPaneMetricsWindow.swift @@ -0,0 +1,8 @@ +import AppKit + +/// Exercises Retina conversion even when the fleet console has a 1x display. +@MainActor +final class TerminalPaneMetricsWindow: NSWindow { + var testBackingScale: CGFloat = 1 + override var backingScaleFactor: CGFloat { testBackingScale } +} From cf83c2ac4926a185aa42a5930c9ec9e74f6fb4f6 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 16 Sep 2026 16:20:05 -0700 Subject: [PATCH 03/16] fix: reconcile terminal metrics and finish inactive pane resizes --- .../TerminalPortalResizePhase.swift | 7 ++++++- .../TerminalPortalResizeRecoveryTests.swift | 14 +++++++++++++ Sources/GhosttyNSView+CellMetrics.swift | 21 +++++++++++++++++++ Sources/GhosttyTerminalView.swift | 20 ++++++------------ Sources/TerminalWindowPortal.swift | 2 +- cmux.xcodeproj/project.pbxproj | 4 ++++ .../TerminalPaneMetricInvalidationTests.swift | 15 +++++++++++++ 7 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 Sources/GhosttyNSView+CellMetrics.swift diff --git a/Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/SurfaceValues/TerminalPortalResizePhase.swift b/Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/SurfaceValues/TerminalPortalResizePhase.swift index 63645f7fcff3..905b247efef5 100644 --- a/Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/SurfaceValues/TerminalPortalResizePhase.swift +++ b/Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/SurfaceValues/TerminalPortalResizePhase.swift @@ -43,8 +43,9 @@ public struct TerminalPortalResizePhase: Sendable { /// or an explicit start establishes a new resize transaction. /// /// - Parameter active: The current native live-resize signal. + /// - Parameter interactiveResizeActive: Whether a divider/sidebar still owns the resize. /// - Returns: Whether the caller should reconcile geometry for this event. - public mutating func observeNativeResize(active: Bool) -> Bool { + public mutating func observeNativeResize(active: Bool, interactiveResizeActive: Bool = false) -> Bool { switch (phase, active) { case (.completedAwaitingNativeEnd, true): return false @@ -52,6 +53,10 @@ public struct TerminalPortalResizePhase: Sendable { phase = .idle case (.idle, true): phase = .resizing + case (.resizing, false) where !interactiveResizeActive: + // A reparented view may miss didEndLiveResize. Preserve the gate + // until the next geometry commit, but never await a lost callback. + phase = .ending default: break } diff --git a/Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/TerminalPortalResizeRecoveryTests.swift b/Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/TerminalPortalResizeRecoveryTests.swift index cc0fa0e1a52c..9bba658ce6e5 100644 --- a/Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/TerminalPortalResizeRecoveryTests.swift +++ b/Packages/macOS/CmuxTerminalCore/Tests/CmuxTerminalCoreTests/TerminalPortalResizeRecoveryTests.swift @@ -2,6 +2,20 @@ import CmuxTerminalCore import Testing struct TerminalPortalResizeRecoveryTests { + @Test func activeDividerKeepsPublicationDeferredUntilItsOwnEnd() { + var phase = TerminalPortalResizePhase() + phase.begin() + let acceptsGeometry1 = phase.observeNativeResize(active: false, interactiveResizeActive: true) + #expect(acceptsGeometry1) + #expect(phase.defersRenderer) + #expect(!phase.isEnding) + let acceptsGeometry2 = phase.observeNativeResize(active: false, interactiveResizeActive: false) + #expect(acceptsGeometry2) + #expect(phase.isEnding) + phase.commitEnd(nativeResizeActive: false) + #expect(!phase.defersRenderer) + } + @Test func observedResizeEndSchedulesFinalGeometryWithoutNotification() { var phase = TerminalPortalResizePhase() let acceptsGeometry3 = phase.observeNativeResize(active: true) diff --git a/Sources/GhosttyNSView+CellMetrics.swift b/Sources/GhosttyNSView+CellMetrics.swift new file mode 100644 index 000000000000..fc9412373990 --- /dev/null +++ b/Sources/GhosttyNSView+CellMetrics.swift @@ -0,0 +1,21 @@ +import AppKit +import CmuxTerminal + +extension GhosttyNSView { + /// Invalidates a queued metric snapshot by reading the current runtime. + /// Ghostty reports backing pixels; AppKit scrolling and hit testing use points. + @discardableResult + func synchronizeCellMetrics() -> Bool { + guard let size = terminalSurface?.cellSizePoints(), + size.width.isFinite, size.height.isFinite, + size.width > 0, size.height > 0, + cellSize != size else { return false } + cellSize = size + NotificationCenter.default.post( + name: .ghosttyDidUpdateCellSize, + object: self, + userInfo: [GhosttyNotificationKey.cellSize: size] + ) + return true + } +} diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 6bc3e767255a..34be218bece9 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -3291,19 +3291,10 @@ class GhosttyApp { surfaceView.enqueueScrollbarUpdate(scrollbar) return true case GHOSTTY_ACTION_CELL_SIZE: - let cellSize = CGSize( - width: CGFloat(action.action.cell_size.width), - height: CGFloat(action.action.cell_size.height) - ) - let terminalSurface = surfaceView.terminalSurface - DispatchQueue.main.async { - surfaceView.cellSize = cellSize - _ = terminalSurface?.fontSizeLineageSnapshot() - NotificationCenter.default.post( - name: .ghosttyDidUpdateCellSize, - object: surfaceView, - userInfo: [GhosttyNotificationKey.cellSize: cellSize] - ) + Task { @MainActor [weak surfaceView] in + guard let surfaceView else { return } + surfaceView.synchronizeCellMetrics() + _ = surfaceView.terminalSurface?.fontSizeLineageSnapshot() } return true case GHOSTTY_ACTION_START_SEARCH: @@ -5302,7 +5293,8 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { || TerminalWindowPortalRegistry.isInteractiveGeometryResizeActive(in: window), caller: caller ) - return didChange || surfaceSizeChanged + let cellMetricsChanged = synchronizeCellMetrics() + return didChange || surfaceSizeChanged || cellMetricsChanged } @discardableResult diff --git a/Sources/TerminalWindowPortal.swift b/Sources/TerminalWindowPortal.swift index 76424c7b4f11..005ded34a06d 100644 --- a/Sources/TerminalWindowPortal.swift +++ b/Sources/TerminalWindowPortal.swift @@ -1132,7 +1132,7 @@ final class WindowTerminalPortal: NSObject, TerminalSurfaceResizeAuthority { } } let nativeResizeActive = isWindowLiveResizeActive - guard resizePhase.observeNativeResize(active: nativeResizeActive) else { return } + guard resizePhase.observeNativeResize(active: nativeResizeActive, interactiveResizeActive: TerminalWindowPortalRegistry.isInteractiveGeometryResizeActive(in: window)) else { return } let endingLiveResize = resizePhase.isEnding guard ensureInstalled(syncLayout: false) else { if endingLiveResize { diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index f748904dd042..b280053d16fa 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -1820,6 +1820,7 @@ F0A110030000000000000001 /* GhosttyMouseSessionLedgerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A110040000000000000001 /* GhosttyMouseSessionLedgerTests.swift */; }; A11EAF000000000000000000 /* GhosttyNotificationDispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11EAF000000000000000001 /* GhosttyNotificationDispatcherTests.swift */; }; D78971000000000000000001 /* GhosttyNotificationScrollRuntimeBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78971000000000000000002 /* GhosttyNotificationScrollRuntimeBridge.swift */; }; + 123810000000000000000006 /* GhosttyNSView+CellMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000005 /* GhosttyNSView+CellMetrics.swift */; }; B88357000000000000000001 /* GhosttyNSView+ClipboardActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88357000000000000000002 /* GhosttyNSView+ClipboardActions.swift */; }; B88357000000000000000003 /* GhosttyNSView+ClipboardInputSequencing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88357000000000000000004 /* GhosttyNSView+ClipboardInputSequencing.swift */; }; F0ACC0DE0000000000000001 /* GhosttyNSView+ForkConversationContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ACC0DE0000000000000002 /* GhosttyNSView+ForkConversationContextMenu.swift */; }; @@ -5604,6 +5605,7 @@ F0A110040000000000000001 /* GhosttyMouseSessionLedgerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyMouseSessionLedgerTests.swift; sourceTree = ""; }; A11EAF000000000000000001 /* GhosttyNotificationDispatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyNotificationDispatcherTests.swift; sourceTree = ""; }; D78971000000000000000002 /* GhosttyNotificationScrollRuntimeBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyNotificationScrollRuntimeBridge.swift; sourceTree = ""; }; + 123810000000000000000005 /* GhosttyNSView+CellMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+CellMetrics.swift"; sourceTree = ""; }; B88357000000000000000002 /* GhosttyNSView+ClipboardActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+ClipboardActions.swift"; sourceTree = ""; }; B88357000000000000000004 /* GhosttyNSView+ClipboardInputSequencing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+ClipboardInputSequencing.swift"; sourceTree = ""; }; F0ACC0DE0000000000000002 /* GhosttyNSView+ForkConversationContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+ForkConversationContextMenu.swift"; sourceTree = ""; }; @@ -9056,6 +9058,7 @@ F0A1100C0000000000000001 /* GhosttyMouseSessionLedgerSession.swift */, F0A110080000000000000001 /* GhosttyMouseSessionLedgerSurfaceIdentity.swift */, A5001015 /* GhosttyTerminalView.swift */, + 123810000000000000000005 /* GhosttyNSView+CellMetrics.swift */, 126570000000000000000007 /* GhosttyTerminalView+Representable.swift */, 030176C8103741F8BDD27F26 /* CloudTerminalReconnectOverlayView.swift */, DF976CA511F242519ADA8204 /* CloudTerminalOverlayCoordinator.swift */, @@ -13152,6 +13155,7 @@ F0A1100B0000000000000001 /* GhosttyMouseSessionLedgerSession.swift in Sources */, F0A110070000000000000001 /* GhosttyMouseSessionLedgerSurfaceIdentity.swift in Sources */, D78971000000000000000001 /* GhosttyNotificationScrollRuntimeBridge.swift in Sources */, + 123810000000000000000006 /* GhosttyNSView+CellMetrics.swift in Sources */, B88357000000000000000001 /* GhosttyNSView+ClipboardActions.swift in Sources */, B88357000000000000000003 /* GhosttyNSView+ClipboardInputSequencing.swift in Sources */, F0ACC0DE0000000000000001 /* GhosttyNSView+ForkConversationContextMenu.swift in Sources */, diff --git a/cmuxTests/TerminalPaneMetricInvalidationTests.swift b/cmuxTests/TerminalPaneMetricInvalidationTests.swift index d75d816791a6..18e76443f44e 100644 --- a/cmuxTests/TerminalPaneMetricInvalidationTests.swift +++ b/cmuxTests/TerminalPaneMetricInvalidationTests.swift @@ -85,6 +85,21 @@ struct TerminalPaneMetricInvalidationTests { #expect(!portal.isRendererResizeDeferred) } + @Test func activeDividerDoesNotFinishWhenNativeWindowResizeIsInactive() throws { + let fixture = try TerminalPaneMetricsFixture() + defer { fixture.tearDown() } + try fixture.bind() + let portal = try #require(TerminalWindowPortalRegistry.portalsByWindowId[ObjectIdentifier(fixture.window)]) + TerminalWindowPortalRegistry.beginInteractiveGeometryResize(in: fixture.window) + defer { TerminalWindowPortalRegistry.endInteractiveGeometryResize(in: fixture.window) } + fixture.split.setPosition(320, ofDividerAt: 0) + TerminalWindowPortalRegistry.synchronizeExternalGeometryNow(for: fixture.window) + #expect(portal.isRendererResizeDeferred) + TerminalWindowPortalRegistry.endInteractiveGeometryResize(in: fixture.window) + try fixture.settle() + #expect(!portal.isRendererResizeDeferred) + } + private func assertGridAndText(_ fixture: TerminalPaneMetricsFixture, cell: CGSize, font: Float) throws { let sample = try #require(fixture.surface.rawSizingSample()) let runtime = try #require(fixture.surface.surface) From 5bbe5e7780a66a83aff52ca82ad29b25862bbf23 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 16 Sep 2026 16:48:29 -0700 Subject: [PATCH 04/16] test: yield actor execution while terminal geometry settles --- .../TerminalPaneMetricInvalidationTests.swift | 52 ++++++++++--------- cmuxTests/TerminalPaneMetricsFixture.swift | 31 ++++++----- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/cmuxTests/TerminalPaneMetricInvalidationTests.swift b/cmuxTests/TerminalPaneMetricInvalidationTests.swift index 18e76443f44e..0486a0c24c61 100644 --- a/cmuxTests/TerminalPaneMetricInvalidationTests.swift +++ b/cmuxTests/TerminalPaneMetricInvalidationTests.swift @@ -13,13 +13,13 @@ import Testing @Suite(.serialized) struct TerminalPaneMetricInvalidationTests { @Test(arguments: [CGFloat(1), CGFloat(2)]) - func fontCallbacksUseCurrentLogicalCellSize(backingScale: CGFloat) throws { - let fixture = try TerminalPaneMetricsFixture(backingScale: backingScale) + func fontCallbacksUseCurrentLogicalCellSize(backingScale: CGFloat) async throws { + let fixture = try await TerminalPaneMetricsFixture(backingScale: backingScale) defer { fixture.tearDown() } - try fixture.bind() + try await fixture.bind() #expect(fixture.surface.performInternalBindingAction("set_font_size:24")) #expect(fixture.surface.performInternalBindingAction("set_font_size:13")) - try fixture.waitUntil { + try await fixture.waitUntil("logical cell metrics") { fixture.hosted.surfaceView.cellSize == fixture.surface.cellSizePoints() } let sample = try #require(fixture.surface.rawSizingSample()) @@ -27,42 +27,43 @@ struct TerminalPaneMetricInvalidationTests { #expect(fixture.hosted.surfaceView.cellSize.height == CGFloat(sample.cellHeightPx) / backingScale) } - @Test func hiddenOutputReflowsThroughDividerDragAndSplitClose() throws { - let fixture = try TerminalPaneMetricsFixture() + @Test(arguments: [CGFloat(1), CGFloat(2)]) + func hiddenOutputReflowsThroughDividerDragAndSplitClose(backingScale: CGFloat) async throws { + let fixture = try await TerminalPaneMetricsFixture(backingScale: backingScale) defer { fixture.tearDown() } // The original #12381 path: output arrives before the split is shown. try fixture.writeRows() - try fixture.bind() + try await fixture.bind() let originalCell = try #require(fixture.surface.cellSizePoints()) let originalFont = ghostty_surface_font_size(try #require(fixture.surface.surface)) for width: CGFloat in [280, 620, 360] { - try fixture.moveDivider(to: width) + try await fixture.moveDivider(to: width) try assertGridAndText(fixture, cell: originalCell, font: originalFont) } - try fixture.closeSibling() + try await fixture.closeSibling() try assertGridAndText(fixture, cell: originalCell, font: originalFont) } - @Test func paneGeometryInvalidatesStaleLargerCellMetrics() throws { - let fixture = try TerminalPaneMetricsFixture() + @Test func paneGeometryInvalidatesStaleLargerCellMetrics() async throws { + let fixture = try await TerminalPaneMetricsFixture() defer { fixture.tearDown() } - try fixture.bind() + try await fixture.bind() let expected = try #require(fixture.surface.cellSizePoints()) // Simulate a delayed metric notification from the previous layout. // Repair must come from each pane geometry boundary, with no window // resize, config reload, or font-size mutation. for closeSibling in [false, true] { fixture.hosted.surfaceView.cellSize = CGSize(width: expected.width * 2, height: expected.height * 2) - if closeSibling { try fixture.closeSibling() } - else { try fixture.moveDivider(to: 340) } + if closeSibling { try await fixture.closeSibling() } + else { try await fixture.moveDivider(to: 340) } #expect(fixture.hosted.surfaceView.cellSize == expected) } } - @Test func sameFrameReconciliationRepairsMetricCache() throws { - let fixture = try TerminalPaneMetricsFixture() + @Test func sameFrameReconciliationRepairsMetricCache() async throws { + let fixture = try await TerminalPaneMetricsFixture() defer { fixture.tearDown() } - try fixture.bind() + try await fixture.bind() let expected = try #require(fixture.surface.cellSizePoints()) let frame = fixture.hosted.frame fixture.hosted.surfaceView.cellSize = CGSize(width: expected.width * 2, height: expected.height * 2) @@ -71,24 +72,24 @@ struct TerminalPaneMetricInvalidationTests { #expect(fixture.hosted.surfaceView.cellSize == expected) } - @Test func splitCloseRecoversAfterNativeResizeEndCallbackWasLost() throws { - let fixture = try TerminalPaneMetricsFixture() + @Test func splitCloseRecoversAfterNativeResizeEndCallbackWasLost() async throws { + let fixture = try await TerminalPaneMetricsFixture() defer { fixture.tearDown() } - try fixture.bind() + try await fixture.bind() let portal = try #require(TerminalWindowPortalRegistry.portalsByWindowId[ObjectIdentifier(fixture.window)]) portal.isWindowLiveResizeActiveOverrideForTesting = true TerminalWindowPortalRegistry.synchronizeForAnchor(fixture.anchor, syncLayout: false) #expect(portal.isRendererResizeDeferred) portal.isWindowLiveResizeActiveOverrideForTesting = false // No window notification: only the surviving pane's layout changes. - try fixture.closeSibling() + try await fixture.closeSibling() #expect(!portal.isRendererResizeDeferred) } - @Test func activeDividerDoesNotFinishWhenNativeWindowResizeIsInactive() throws { - let fixture = try TerminalPaneMetricsFixture() + @Test func activeDividerDoesNotFinishWhenNativeWindowResizeIsInactive() async throws { + let fixture = try await TerminalPaneMetricsFixture() defer { fixture.tearDown() } - try fixture.bind() + try await fixture.bind() let portal = try #require(TerminalWindowPortalRegistry.portalsByWindowId[ObjectIdentifier(fixture.window)]) TerminalWindowPortalRegistry.beginInteractiveGeometryResize(in: fixture.window) defer { TerminalWindowPortalRegistry.endInteractiveGeometryResize(in: fixture.window) } @@ -96,7 +97,7 @@ struct TerminalPaneMetricInvalidationTests { TerminalWindowPortalRegistry.synchronizeExternalGeometryNow(for: fixture.window) #expect(portal.isRendererResizeDeferred) TerminalWindowPortalRegistry.endInteractiveGeometryResize(in: fixture.window) - try fixture.settle() + try await fixture.settle() #expect(!portal.isRendererResizeDeferred) } @@ -108,6 +109,7 @@ struct TerminalPaneMetricInvalidationTests { #expect(Int(grid.columns) == sample.columns) #expect(Int(grid.rows) == sample.rows) #expect(fixture.surface.cellSizePoints() == cell) + #expect(fixture.hosted.surfaceView.cellSize == cell) #expect(ghostty_surface_font_size(runtime) == font) let rows = try fixture.physicalRows() #expect(rows.allSatisfy { $0.count <= sample.columns }) diff --git a/cmuxTests/TerminalPaneMetricsFixture.swift b/cmuxTests/TerminalPaneMetricsFixture.swift index 236daa047268..9074ded69ee4 100644 --- a/cmuxTests/TerminalPaneMetricsFixture.swift +++ b/cmuxTests/TerminalPaneMetricsFixture.swift @@ -20,7 +20,7 @@ final class TerminalPaneMetricsFixture { let surface: TerminalSurface var hosted: GhosttySurfaceScrollView { surface.hostedView } - init(backingScale: CGFloat = 1) throws { + init(backingScale: CGFloat = 1) async throws { let metricsWindow = TerminalPaneMetricsWindow( contentRect: NSRect(x: 0, y: 0, width: 960, height: 640), styleMask: [.titled, .closable], backing: .buffered, defer: false @@ -34,7 +34,7 @@ final class TerminalPaneMetricsFixture { configTemplate: nil, initialCommand: "/bin/cat" ) do { - try waitUntil { self.surface.surface != nil } + try await waitUntil("runtime creation") { self.surface.surface != nil } let content = try #require(window.contentView) split.frame = content.bounds split.isVertical = true @@ -51,41 +51,44 @@ final class TerminalPaneMetricsFixture { } } - func bind() throws { + func bind() async throws { TerminalWindowPortalRegistry.bind( hostedView: hosted, to: anchor, visibleInUI: true, expectedSurfaceId: surface.id, expectedGeneration: surface.portalBindingGeneration() ) hosted.setVisibleInUI(true) - try settle() + try await settle() } - func settle() throws { - try waitUntil { + func settle() async throws { + try await waitUntil("native grid convergence") { self.window.contentView?.layoutSubtreeIfNeeded() guard let sample = self.surface.rawSizingSample() else { return false } let view = self.hosted.surfaceView + guard let scroll = self.hosted.subviews.compactMap({ $0 as? NSScrollView }).first else { return false } var grid = ghostty_surface_grid_metrics_s() guard let runtime = self.surface.surface, ghostty_surface_grid_metrics(runtime, &grid) else { return false } return abs(self.hosted.frame.width - self.anchor.frame.width) < 1 && + abs(view.frame.width - scroll.contentView.bounds.width) < 1 && + abs(view.frame.height - scroll.contentView.bounds.height) < 1 && abs(CGFloat(sample.surfaceWidthPx) - view.bounds.width * self.window.backingScaleFactor) < 2 && sample.columns == Int(grid.columns) && sample.rows == Int(grid.rows) } } - func moveDivider(to x: CGFloat) throws { + func moveDivider(to x: CGFloat) async throws { split.setPosition(x, ofDividerAt: 0) TerminalWindowPortalRegistry.synchronizeForAnchor(anchor, syncLayout: false) - try settle() + try await settle() } - func closeSibling() throws { + func closeSibling() async throws { sibling.removeFromSuperview() split.adjustSubviews() TerminalWindowPortalRegistry.synchronizeForAnchor(anchor, syncLayout: false) - try settle() + try await settle() } func writeRows() throws { @@ -110,12 +113,14 @@ final class TerminalPaneMetricsFixture { .components(separatedBy: "\n") } - func waitUntil(_ predicate: () -> Bool) throws { + func waitUntil(_ stage: String = "condition", _ predicate: () -> Bool) async throws { let deadline = ProcessInfo.processInfo.systemUptime + 5 while !predicate(), ProcessInfo.processInfo.systemUptime < deadline { - RunLoop.main.run(until: Date().addingTimeInterval(0.005)) + await withCheckedContinuation { continuation in + RunLoop.main.perform(inModes: [.common]) { continuation.resume() } + } } - try #require(predicate(), "Terminal geometry did not converge") + try #require(predicate(), "Timed out during \(stage): runtime=\(surface.surface != nil), anchor=\(anchor.frame), hosted=\(hosted.frame), native=\(String(describing: surface.rawSizingSample()))") } func tearDown() { From 961ec1af8fd1b1a88e4a8173133fbf436901614a Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 16 Sep 2026 17:17:20 -0700 Subject: [PATCH 05/16] test: initialize AppKit before pane fixture setup --- cmuxTests/TerminalPaneMetricsFixture.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmuxTests/TerminalPaneMetricsFixture.swift b/cmuxTests/TerminalPaneMetricsFixture.swift index 9074ded69ee4..755139297152 100644 --- a/cmuxTests/TerminalPaneMetricsFixture.swift +++ b/cmuxTests/TerminalPaneMetricsFixture.swift @@ -21,6 +21,7 @@ final class TerminalPaneMetricsFixture { var hosted: GhosttySurfaceScrollView { surface.hostedView } init(backingScale: CGFloat = 1) async throws { + _ = NSApplication.shared let metricsWindow = TerminalPaneMetricsWindow( contentRect: NSRect(x: 0, y: 0, width: 960, height: 640), styleMask: [.titled, .closable], backing: .buffered, defer: false @@ -57,7 +58,6 @@ final class TerminalPaneMetricsFixture { expectedSurfaceId: surface.id, expectedGeneration: surface.portalBindingGeneration() ) - hosted.setVisibleInUI(true) try await settle() } From c23d91bcacd4672cae645e6b7ab44d664ced0b81 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 16 Sep 2026 18:41:26 -0700 Subject: [PATCH 06/16] test: read physical viewport rows and await divider completion --- cmuxTests/TerminalPaneMetricInvalidationTests.swift | 1 + cmuxTests/TerminalPaneMetricsFixture.swift | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cmuxTests/TerminalPaneMetricInvalidationTests.swift b/cmuxTests/TerminalPaneMetricInvalidationTests.swift index 0486a0c24c61..6f38589d5ca3 100644 --- a/cmuxTests/TerminalPaneMetricInvalidationTests.swift +++ b/cmuxTests/TerminalPaneMetricInvalidationTests.swift @@ -97,6 +97,7 @@ struct TerminalPaneMetricInvalidationTests { TerminalWindowPortalRegistry.synchronizeExternalGeometryNow(for: fixture.window) #expect(portal.isRendererResizeDeferred) TerminalWindowPortalRegistry.endInteractiveGeometryResize(in: fixture.window) + try await fixture.waitUntil("divider completion") { !portal.isRendererResizeDeferred } try await fixture.settle() #expect(!portal.isRendererResizeDeferred) } diff --git a/cmuxTests/TerminalPaneMetricsFixture.swift b/cmuxTests/TerminalPaneMetricsFixture.swift index 755139297152..6fdb18382404 100644 --- a/cmuxTests/TerminalPaneMetricsFixture.swift +++ b/cmuxTests/TerminalPaneMetricsFixture.swift @@ -58,6 +58,7 @@ final class TerminalPaneMetricsFixture { expectedSurfaceId: surface.id, expectedGeneration: surface.portalBindingGeneration() ) + hosted.setVisibleInUI(true) try await settle() } @@ -102,8 +103,8 @@ final class TerminalPaneMetricsFixture { let runtime = try #require(surface.surface) var text = ghostty_text_s() let selection = ghostty_selection_s( - top_left: ghostty_point_s(tag: GHOSTTY_POINT_SURFACE, coord: GHOSTTY_POINT_COORD_TOP_LEFT, x: 0, y: 0), - bottom_right: ghostty_point_s(tag: GHOSTTY_POINT_SURFACE, coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT, x: 0, y: 0), + top_left: ghostty_point_s(tag: GHOSTTY_POINT_VIEWPORT, coord: GHOSTTY_POINT_COORD_TOP_LEFT, x: 0, y: 0), + bottom_right: ghostty_point_s(tag: GHOSTTY_POINT_VIEWPORT, coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT, x: 0, y: 0), rectangle: true ) try #require(ghostty_surface_read_text(runtime, selection, &text)) From 959f16c86d79e98fa01201c32062b694218bfe16 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 16 Sep 2026 19:04:12 -0700 Subject: [PATCH 07/16] test: inspect native grid spans for physical reflow --- .../TerminalPaneMetricInvalidationTests.swift | 12 +++++++++++- cmuxTests/TerminalPaneMetricsFixture.swift | 15 --------------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/cmuxTests/TerminalPaneMetricInvalidationTests.swift b/cmuxTests/TerminalPaneMetricInvalidationTests.swift index 6f38589d5ca3..f6759e3947dd 100644 --- a/cmuxTests/TerminalPaneMetricInvalidationTests.swift +++ b/cmuxTests/TerminalPaneMetricInvalidationTests.swift @@ -112,10 +112,20 @@ struct TerminalPaneMetricInvalidationTests { #expect(fixture.surface.cellSizePoints() == cell) #expect(fixture.hosted.surfaceView.cellSize == cell) #expect(ghostty_surface_font_size(runtime) == font) - let rows = try fixture.physicalRows() + // Text selection unwraps soft-wrapped lines even for rectangles. + // Inspect the native grid export, including each span's cell extent. + let rendered = try #require(fixture.surface.mobileRenderGridFrame(stateSeq: 0, includeTheme: false)) + #expect(rendered.frame.columns == sample.columns) + #expect(rendered.frame.rows == sample.rows) + #expect(rendered.frame.rowSpans.allSatisfy { + $0.row < sample.rows && $0.column + $0.gridCellWidth <= sample.columns + }) + let rows = rendered.rows #expect(rows.allSatisfy { $0.count <= sample.columns }) let compact = rows.joined().filter { !$0.isWhitespace } for index in 1...4 { + let expected = "R\(index)" + String(repeating: "=", count: 74) + "END\(index)" + #expect(compact.components(separatedBy: expected).count == 2) #expect(compact.components(separatedBy: "R\(index)").count == 2) #expect(compact.components(separatedBy: "END\(index)").count == 2) } diff --git a/cmuxTests/TerminalPaneMetricsFixture.swift b/cmuxTests/TerminalPaneMetricsFixture.swift index 6fdb18382404..f155e189c375 100644 --- a/cmuxTests/TerminalPaneMetricsFixture.swift +++ b/cmuxTests/TerminalPaneMetricsFixture.swift @@ -99,21 +99,6 @@ final class TerminalPaneMetricsFixture { output.withCString { ghostty_surface_process_output(runtime, $0, UInt(output.utf8.count)) } } - func physicalRows() throws -> [String] { - let runtime = try #require(surface.surface) - var text = ghostty_text_s() - let selection = ghostty_selection_s( - top_left: ghostty_point_s(tag: GHOSTTY_POINT_VIEWPORT, coord: GHOSTTY_POINT_COORD_TOP_LEFT, x: 0, y: 0), - bottom_right: ghostty_point_s(tag: GHOSTTY_POINT_VIEWPORT, coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT, x: 0, y: 0), - rectangle: true - ) - try #require(ghostty_surface_read_text(runtime, selection, &text)) - defer { ghostty_surface_free_text(runtime, &text) } - let pointer = try #require(text.text) - return String(decoding: UnsafeBufferPointer(start: pointer, count: Int(text.text_len)).map { UInt8(bitPattern: $0) }, as: UTF8.self) - .components(separatedBy: "\n") - } - func waitUntil(_ stage: String = "condition", _ predicate: () -> Bool) async throws { let deadline = ProcessInfo.processInfo.systemUptime + 5 while !predicate(), ProcessInfo.processInfo.systemUptime < deadline { From 5209dd758c06f38f49eaa21d2edf9f455a4f3296 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 16 Sep 2026 19:07:44 -0700 Subject: [PATCH 08/16] test: assert exported native cell widths directly --- cmuxTests/TerminalPaneMetricInvalidationTests.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmuxTests/TerminalPaneMetricInvalidationTests.swift b/cmuxTests/TerminalPaneMetricInvalidationTests.swift index f6759e3947dd..f36c1f8cfc7e 100644 --- a/cmuxTests/TerminalPaneMetricInvalidationTests.swift +++ b/cmuxTests/TerminalPaneMetricInvalidationTests.swift @@ -117,9 +117,11 @@ struct TerminalPaneMetricInvalidationTests { let rendered = try #require(fixture.surface.mobileRenderGridFrame(stateSeq: 0, includeTheme: false)) #expect(rendered.frame.columns == sample.columns) #expect(rendered.frame.rows == sample.rows) - #expect(rendered.frame.rowSpans.allSatisfy { - $0.row < sample.rows && $0.column + $0.gridCellWidth <= sample.columns - }) + for span in rendered.frame.rowSpans { + let cellWidth = try #require(span.cellWidth) + #expect(span.row < sample.rows) + #expect(span.column + cellWidth <= sample.columns) + } let rows = rendered.rows #expect(rows.allSatisfy { $0.count <= sample.columns }) let compact = rows.joined().filter { !$0.isWhitespace } From caba6aa0c3167a24f50d9071a540725fc424d497 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 18 Sep 2026 19:54:04 -0700 Subject: [PATCH 09/16] test: satisfy required deinit lint --- cmuxTests/TerminalPaneMetricsFixture.swift | 2 ++ cmuxTests/TerminalPaneMetricsWindow.swift | 3 +++ 2 files changed, 5 insertions(+) diff --git a/cmuxTests/TerminalPaneMetricsFixture.swift b/cmuxTests/TerminalPaneMetricsFixture.swift index f155e189c375..370165b2f0f9 100644 --- a/cmuxTests/TerminalPaneMetricsFixture.swift +++ b/cmuxTests/TerminalPaneMetricsFixture.swift @@ -20,6 +20,8 @@ final class TerminalPaneMetricsFixture { let surface: TerminalSurface var hosted: GhosttySurfaceScrollView { surface.hostedView } + deinit {} + init(backingScale: CGFloat = 1) async throws { _ = NSApplication.shared let metricsWindow = TerminalPaneMetricsWindow( diff --git a/cmuxTests/TerminalPaneMetricsWindow.swift b/cmuxTests/TerminalPaneMetricsWindow.swift index aac650e86c7e..7f973c72beab 100644 --- a/cmuxTests/TerminalPaneMetricsWindow.swift +++ b/cmuxTests/TerminalPaneMetricsWindow.swift @@ -4,5 +4,8 @@ import AppKit @MainActor final class TerminalPaneMetricsWindow: NSWindow { var testBackingScale: CGFloat = 1 + + deinit {} + override var backingScaleFactor: CGFloat { testBackingScale } } From 80a95713d8a875e1629b0c7fed540d31c6788ef0 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sun, 20 Sep 2026 20:21:16 -0700 Subject: [PATCH 10/16] fix: restore test target membership after main merge --- cmux.xcodeproj/project.pbxproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 4f361c434081..03c88b66d074 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -3556,6 +3556,8 @@ C0DE6B420000000000000001 /* TerminalPanelTextBoxState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE6B420000000000000002 /* TerminalPanelTextBoxState.swift */; }; 6819AF60AEA6455DB57CC7FE /* TerminalPanelUnavailableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811B03E4004A4D1FB6650158 /* TerminalPanelUnavailableView.swift */; }; A5001403 /* TerminalPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001413 /* TerminalPanelView.swift */; }; + 123810000000000000000002 /* TerminalPaneMetricInvalidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000001 /* TerminalPaneMetricInvalidationTests.swift */; }; + 123810000000000000000004 /* TerminalPaneMetricsFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000003 /* TerminalPaneMetricsFixture.swift */; }; B88351000000000000000001 /* TerminalPasteboardReadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88351000000000000000002 /* TerminalPasteboardReadRequest.swift */; }; B88355000000000000000001 /* TerminalPastePreparationDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88355000000000000000002 /* TerminalPastePreparationDestination.swift */; }; B88357000000000000000009 /* TerminalPastePreparationFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8835700000000000000000A /* TerminalPastePreparationFailure.swift */; }; @@ -7552,6 +7554,8 @@ C0DE6B420000000000000002 /* TerminalPanelTextBoxState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelTextBoxState.swift; sourceTree = ""; }; 811B03E4004A4D1FB6650158 /* TerminalPanelUnavailableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelUnavailableView.swift; sourceTree = ""; }; A5001413 /* TerminalPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelView.swift; sourceTree = ""; }; + 123810000000000000000001 /* TerminalPaneMetricInvalidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPaneMetricInvalidationTests.swift; sourceTree = ""; }; + 123810000000000000000003 /* TerminalPaneMetricsFixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPaneMetricsFixture.swift; sourceTree = ""; }; B88351000000000000000002 /* TerminalPasteboardReadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPasteboardReadRequest.swift; sourceTree = ""; }; B88355000000000000000002 /* TerminalPastePreparationDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPastePreparationDestination.swift; sourceTree = ""; }; B8835700000000000000000A /* TerminalPastePreparationFailure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPastePreparationFailure.swift; sourceTree = ""; }; @@ -11475,6 +11479,8 @@ C12758000000000000000009 /* PlainPastePTYFixture.swift */, C12758000000000000000007 /* TerminalPlainTextPastePTYTests.swift */, D7AB34400000000000000004 /* GhosttyTerminalViewVisibilityPolicyTests.swift */, + 123810000000000000000003 /* TerminalPaneMetricsFixture.swift */, + 123810000000000000000001 /* TerminalPaneMetricInvalidationTests.swift */, D7AB00000000000000000012 /* WorkspaceAdjacentPaneMoveTests.swift */, 804100000000000000000004 /* ReorderShortcutActionTests.swift */, D7AB3605C10DEF0000000002 /* WorkspaceCloseTabsContextMenuTests.swift */, @@ -16644,6 +16650,8 @@ A5E380700000000000000001 /* TerminalNotificationSocketActionTests.swift in Sources */, 11189010A1B2C3D4E5F60718 /* TerminalNotificationSocketAttributionTests.swift in Sources */, 596100000000000000000007 /* TerminalNotificationStore+NativeNotificationDeliveryTesting.swift in Sources */, + 123810000000000000000002 /* TerminalPaneMetricInvalidationTests.swift in Sources */, + 123810000000000000000004 /* TerminalPaneMetricsFixture.swift in Sources */, B12715000000000000000001 /* TerminalPastePreparationReapingTests.swift in Sources */, C12758000000000000000010 /* TerminalPasteWorkerStartupBenchmark.swift in Sources */, C13519000000000000000013 /* TerminalPathEnvironmentTests.swift in Sources */, From e1c70e24b7b43fc802671bf9a23c861971d60844 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sun, 20 Sep 2026 20:26:39 -0700 Subject: [PATCH 11/16] fix: restore metrics test target membership --- cmux.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 03c88b66d074..bb16e3c0ff1c 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -3558,6 +3558,7 @@ A5001403 /* TerminalPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001413 /* TerminalPanelView.swift */; }; 123810000000000000000002 /* TerminalPaneMetricInvalidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000001 /* TerminalPaneMetricInvalidationTests.swift */; }; 123810000000000000000004 /* TerminalPaneMetricsFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000003 /* TerminalPaneMetricsFixture.swift */; }; + 123810000000000000000008 /* TerminalPaneMetricsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000007 /* TerminalPaneMetricsWindow.swift */; }; B88351000000000000000001 /* TerminalPasteboardReadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88351000000000000000002 /* TerminalPasteboardReadRequest.swift */; }; B88355000000000000000001 /* TerminalPastePreparationDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88355000000000000000002 /* TerminalPastePreparationDestination.swift */; }; B88357000000000000000009 /* TerminalPastePreparationFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8835700000000000000000A /* TerminalPastePreparationFailure.swift */; }; @@ -7556,6 +7557,7 @@ A5001413 /* TerminalPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelView.swift; sourceTree = ""; }; 123810000000000000000001 /* TerminalPaneMetricInvalidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPaneMetricInvalidationTests.swift; sourceTree = ""; }; 123810000000000000000003 /* TerminalPaneMetricsFixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPaneMetricsFixture.swift; sourceTree = ""; }; + 123810000000000000000007 /* TerminalPaneMetricsWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPaneMetricsWindow.swift; sourceTree = ""; }; B88351000000000000000002 /* TerminalPasteboardReadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPasteboardReadRequest.swift; sourceTree = ""; }; B88355000000000000000002 /* TerminalPastePreparationDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPastePreparationDestination.swift; sourceTree = ""; }; B8835700000000000000000A /* TerminalPastePreparationFailure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalPastePreparationFailure.swift; sourceTree = ""; }; @@ -11480,6 +11482,7 @@ C12758000000000000000007 /* TerminalPlainTextPastePTYTests.swift */, D7AB34400000000000000004 /* GhosttyTerminalViewVisibilityPolicyTests.swift */, 123810000000000000000003 /* TerminalPaneMetricsFixture.swift */, + 123810000000000000000007 /* TerminalPaneMetricsWindow.swift */, 123810000000000000000001 /* TerminalPaneMetricInvalidationTests.swift */, D7AB00000000000000000012 /* WorkspaceAdjacentPaneMoveTests.swift */, 804100000000000000000004 /* ReorderShortcutActionTests.swift */, @@ -16652,6 +16655,7 @@ 596100000000000000000007 /* TerminalNotificationStore+NativeNotificationDeliveryTesting.swift in Sources */, 123810000000000000000002 /* TerminalPaneMetricInvalidationTests.swift in Sources */, 123810000000000000000004 /* TerminalPaneMetricsFixture.swift in Sources */, + 123810000000000000000008 /* TerminalPaneMetricsWindow.swift in Sources */, B12715000000000000000001 /* TerminalPastePreparationReapingTests.swift in Sources */, C12758000000000000000010 /* TerminalPasteWorkerStartupBenchmark.swift in Sources */, C13519000000000000000013 /* TerminalPathEnvironmentTests.swift in Sources */, From 8f2fe4a39fed34e8e6ee297770d006e700af3868 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sun, 20 Sep 2026 20:41:02 -0700 Subject: [PATCH 12/16] fix: restore app source membership after main merge --- cmux.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index bb16e3c0ff1c..ac9481014fdd 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -1967,6 +1967,7 @@ F0A110030000000000000001 /* GhosttyMouseSessionLedgerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A110040000000000000001 /* GhosttyMouseSessionLedgerTests.swift */; }; A11EAF000000000000000000 /* GhosttyNotificationDispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11EAF000000000000000001 /* GhosttyNotificationDispatcherTests.swift */; }; D78971000000000000000001 /* GhosttyNotificationScrollRuntimeBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78971000000000000000002 /* GhosttyNotificationScrollRuntimeBridge.swift */; }; + 123810000000000000000006 /* GhosttyNSView+CellMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000005 /* GhosttyNSView+CellMetrics.swift */; }; B88357000000000000000001 /* GhosttyNSView+ClipboardActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88357000000000000000002 /* GhosttyNSView+ClipboardActions.swift */; }; B88357000000000000000003 /* GhosttyNSView+ClipboardInputSequencing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88357000000000000000004 /* GhosttyNSView+ClipboardInputSequencing.swift */; }; F0ACC0DE0000000000000001 /* GhosttyNSView+ForkConversationContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ACC0DE0000000000000002 /* GhosttyNSView+ForkConversationContextMenu.swift */; }; @@ -5989,6 +5990,7 @@ F0A110040000000000000001 /* GhosttyMouseSessionLedgerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyMouseSessionLedgerTests.swift; sourceTree = ""; }; A11EAF000000000000000001 /* GhosttyNotificationDispatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyNotificationDispatcherTests.swift; sourceTree = ""; }; D78971000000000000000002 /* GhosttyNotificationScrollRuntimeBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyNotificationScrollRuntimeBridge.swift; sourceTree = ""; }; + 123810000000000000000005 /* GhosttyNSView+CellMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+CellMetrics.swift"; sourceTree = ""; }; B88357000000000000000002 /* GhosttyNSView+ClipboardActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+ClipboardActions.swift"; sourceTree = ""; }; B88357000000000000000004 /* GhosttyNSView+ClipboardInputSequencing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+ClipboardInputSequencing.swift"; sourceTree = ""; }; F0ACC0DE0000000000000002 /* GhosttyNSView+ForkConversationContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+ForkConversationContextMenu.swift"; sourceTree = ""; }; @@ -9649,6 +9651,7 @@ F0A1100C0000000000000001 /* GhosttyMouseSessionLedgerSession.swift */, F0A110080000000000000001 /* GhosttyMouseSessionLedgerSurfaceIdentity.swift */, A5001015 /* GhosttyTerminalView.swift */, + 123810000000000000000005 /* GhosttyNSView+CellMetrics.swift */, 126570000000000000000007 /* GhosttyTerminalView+Representable.swift */, 030176C8103741F8BDD27F26 /* CloudTerminalReconnectOverlayView.swift */, DF976CA511F242519ADA8204 /* CloudTerminalOverlayCoordinator.swift */, @@ -13964,6 +13967,7 @@ F0A1100B0000000000000001 /* GhosttyMouseSessionLedgerSession.swift in Sources */, F0A110070000000000000001 /* GhosttyMouseSessionLedgerSurfaceIdentity.swift in Sources */, D78971000000000000000001 /* GhosttyNotificationScrollRuntimeBridge.swift in Sources */, + 123810000000000000000006 /* GhosttyNSView+CellMetrics.swift in Sources */, B88357000000000000000001 /* GhosttyNSView+ClipboardActions.swift in Sources */, B88357000000000000000003 /* GhosttyNSView+ClipboardInputSequencing.swift in Sources */, F0ACC0DE0000000000000001 /* GhosttyNSView+ForkConversationContextMenu.swift in Sources */, From 7a367217308030cb68f12c0fba1122c0d63c1650 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 25 Sep 2026 04:50:02 -0700 Subject: [PATCH 13/16] test: preserve logical markers through hidden pane reflow --- .../TerminalPaneMetricInvalidationTests.swift | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/cmuxTests/TerminalPaneMetricInvalidationTests.swift b/cmuxTests/TerminalPaneMetricInvalidationTests.swift index 602a10605f5d..4ebf6141513f 100644 --- a/cmuxTests/TerminalPaneMetricInvalidationTests.swift +++ b/cmuxTests/TerminalPaneMetricInvalidationTests.swift @@ -126,9 +126,13 @@ struct TerminalPaneMetricInvalidationTests { #expect(span.row < sample.rows) #expect(span.column + cellWidth <= sample.columns) } - let rows = rendered.rows - #expect(rows.allSatisfy { $0.count <= sample.columns }) - let compact = rows.joined().filter { !$0.isWhitespace } + #expect(rendered.rows.allSatisfy { $0.count <= sample.columns }) + // The native render-grid export is the physical reflow oracle above, + // but it can omit soft-wrapped text while a hidden pane is settling. + // Ghostty's direct reader preserves those logical lines and their + // markers, which is the behavior this regression path needs to prove. + let text = try readSurfaceText(fixture) + let compact = text.filter { !$0.isWhitespace } for index in 1...4 { let expected = "R\(index)" + String(repeating: "=", count: 74) + "END\(index)" #expect(compact.components(separatedBy: expected).count == 2) @@ -136,4 +140,31 @@ struct TerminalPaneMetricInvalidationTests { #expect(compact.components(separatedBy: "END\(index)").count == 2) } } + + private func readSurfaceText(_ fixture: TerminalPaneMetricsFixture) throws -> String { + let runtime = try #require(fixture.surface.surface) + let selection = ghostty_selection_s( + top_left: ghostty_point_s( + tag: GHOSTTY_POINT_SURFACE, + coord: GHOSTTY_POINT_COORD_TOP_LEFT, + x: 0, + y: 0 + ), + bottom_right: ghostty_point_s( + tag: GHOSTTY_POINT_SURFACE, + coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT, + x: 0, + y: 0 + ), + rectangle: false + ) + var text = ghostty_text_s() + guard ghostty_surface_read_text(runtime, selection, &text) else { return "" } + defer { ghostty_surface_free_text(runtime, &text) } + guard let pointer = text.text, text.text_len > 0 else { return "" } + return String( + decoding: Data(bytes: pointer, count: Int(text.text_len)), + as: UTF8.self + ) + } } From 1aa71cce72b89787197a6e456c909f9eec388b8e Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 25 Sep 2026 05:30:40 -0700 Subject: [PATCH 14/16] test: preserve soft-wrapped terminal markers --- cmuxTests/TerminalPaneMetricInvalidationTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmuxTests/TerminalPaneMetricInvalidationTests.swift b/cmuxTests/TerminalPaneMetricInvalidationTests.swift index 4ebf6141513f..3793696393de 100644 --- a/cmuxTests/TerminalPaneMetricInvalidationTests.swift +++ b/cmuxTests/TerminalPaneMetricInvalidationTests.swift @@ -156,7 +156,7 @@ struct TerminalPaneMetricInvalidationTests { x: 0, y: 0 ), - rectangle: false + rectangle: true ) var text = ghostty_text_s() guard ghostty_surface_read_text(runtime, selection, &text) else { return "" } From db5d7a73f67e760be1c5f15bc28868d88553416d Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 25 Sep 2026 06:28:24 -0700 Subject: [PATCH 15/16] test: read reflow markers from the viewport --- cmuxTests/TerminalPaneMetricInvalidationTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmuxTests/TerminalPaneMetricInvalidationTests.swift b/cmuxTests/TerminalPaneMetricInvalidationTests.swift index 3793696393de..42953f1af10e 100644 --- a/cmuxTests/TerminalPaneMetricInvalidationTests.swift +++ b/cmuxTests/TerminalPaneMetricInvalidationTests.swift @@ -145,13 +145,13 @@ struct TerminalPaneMetricInvalidationTests { let runtime = try #require(fixture.surface.surface) let selection = ghostty_selection_s( top_left: ghostty_point_s( - tag: GHOSTTY_POINT_SURFACE, + tag: GHOSTTY_POINT_VIEWPORT, coord: GHOSTTY_POINT_COORD_TOP_LEFT, x: 0, y: 0 ), bottom_right: ghostty_point_s( - tag: GHOSTTY_POINT_SURFACE, + tag: GHOSTTY_POINT_VIEWPORT, coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT, x: 0, y: 0 From 3a713d669660686729c90336afa128fd6fdb4bdb Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Thu, 1 Oct 2026 18:09:00 -0700 Subject: [PATCH 16/16] Normalize Xcode project after main merge --- cmux.xcodeproj/project.pbxproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 6e7e4a50da46..4eb6a4ee2b44 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -2119,8 +2119,8 @@ 2070CCCF882C91E496C1FBC6 /* GhosttyNamedCtrlLetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0721B2F3007FE4325E6702 /* GhosttyNamedCtrlLetterTests.swift */; }; A11EAF000000000000000000 /* GhosttyNotificationDispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11EAF000000000000000001 /* GhosttyNotificationDispatcherTests.swift */; }; D78971000000000000000001 /* GhosttyNotificationScrollRuntimeBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78971000000000000000002 /* GhosttyNotificationScrollRuntimeBridge.swift */; }; - 123810000000000000000006 /* GhosttyNSView+CellMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000005 /* GhosttyNSView+CellMetrics.swift */; }; 09F6D65337EEB5988F365778 /* GhosttyNSView+Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C738D190FCB85202992C756 /* GhosttyNSView+Accessibility.swift */; }; + 123810000000000000000006 /* GhosttyNSView+CellMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123810000000000000000005 /* GhosttyNSView+CellMetrics.swift */; }; B88357000000000000000001 /* GhosttyNSView+ClipboardActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88357000000000000000002 /* GhosttyNSView+ClipboardActions.swift */; }; B88357000000000000000003 /* GhosttyNSView+ClipboardInputSequencing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88357000000000000000004 /* GhosttyNSView+ClipboardInputSequencing.swift */; }; 3436831626068CC556129EB3 /* GhosttyNSView+CodexActionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FA91EF89C03A74979E6EF7 /* GhosttyNSView+CodexActionCommand.swift */; }; @@ -6437,8 +6437,8 @@ 5B0721B2F3007FE4325E6702 /* GhosttyNamedCtrlLetterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNamedCtrlLetterTests.swift"; sourceTree = ""; }; A11EAF000000000000000001 /* GhosttyNotificationDispatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyNotificationDispatcherTests.swift; sourceTree = ""; }; D78971000000000000000002 /* GhosttyNotificationScrollRuntimeBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyNotificationScrollRuntimeBridge.swift; sourceTree = ""; }; - 123810000000000000000005 /* GhosttyNSView+CellMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+CellMetrics.swift"; sourceTree = ""; }; 2C738D190FCB85202992C756 /* GhosttyNSView+Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+Accessibility.swift"; sourceTree = ""; }; + 123810000000000000000005 /* GhosttyNSView+CellMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+CellMetrics.swift"; sourceTree = ""; }; B88357000000000000000002 /* GhosttyNSView+ClipboardActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+ClipboardActions.swift"; sourceTree = ""; }; B88357000000000000000004 /* GhosttyNSView+ClipboardInputSequencing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+ClipboardInputSequencing.swift"; sourceTree = ""; }; A9FA91EF89C03A74979E6EF7 /* GhosttyNSView+CodexActionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyNSView+CodexActionCommand.swift"; sourceTree = ""; }; @@ -14973,8 +14973,8 @@ F0A1100B0000000000000001 /* GhosttyMouseSessionLedgerSession.swift in Sources */, F0A110070000000000000001 /* GhosttyMouseSessionLedgerSurfaceIdentity.swift in Sources */, D78971000000000000000001 /* GhosttyNotificationScrollRuntimeBridge.swift in Sources */, - 123810000000000000000006 /* GhosttyNSView+CellMetrics.swift in Sources */, 09F6D65337EEB5988F365778 /* GhosttyNSView+Accessibility.swift in Sources */, + 123810000000000000000006 /* GhosttyNSView+CellMetrics.swift in Sources */, B88357000000000000000001 /* GhosttyNSView+ClipboardActions.swift in Sources */, B88357000000000000000003 /* GhosttyNSView+ClipboardInputSequencing.swift in Sources */, 3436831626068CC556129EB3 /* GhosttyNSView+CodexActionCommand.swift in Sources */,