native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Respect ghostty split-preserve-zoom config when cycling splits (#241)

Upstream Ghostty (v1.3.0+) has split-preserve-zoom = navigation to
keep zoom state when navigating between splits. Supacode's split
management bypasses BaseTerminalController so this config was never
read. Now GhosttyRuntime reads the setting and gotoSplit honors it:
zoom transfers to the next pane when enabled, unzooms when not.

authored by

C.J. Winslow and committed by
GitHub
bfcd2f05 bdba751f

+106 -3
+15 -3
supacode/Features/Terminal/Models/WorktreeTerminalState.swift
··· 21 21 22 22 let tabManager: TerminalTabManager 23 23 private let runtime: GhosttyRuntime 24 + @ObservationIgnored private let splitPreserveZoomOnNavigation: () -> Bool 24 25 private let worktree: Worktree 25 26 @ObservationIgnored 26 27 @SharedReader private var repositorySettings: RepositorySettings ··· 63 64 var onCommandPaletteToggle: (() -> Void)? 64 65 var onSetupScriptConsumed: (() -> Void)? 65 66 66 - init(runtime: GhosttyRuntime, worktree: Worktree, runSetupScript: Bool = false) { 67 + init( 68 + runtime: GhosttyRuntime, 69 + worktree: Worktree, 70 + runSetupScript: Bool = false, 71 + splitPreserveZoomOnNavigation: (() -> Bool)? = nil 72 + ) { 67 73 self.runtime = runtime 74 + self.splitPreserveZoomOnNavigation = splitPreserveZoomOnNavigation ?? { runtime.splitPreserveZoomOnNavigation() } 68 75 self.worktree = worktree 69 76 self.pendingSetupScript = runSetupScript 70 77 self.tabManager = TerminalTabManager() ··· 574 581 return false 575 582 } 576 583 if tree.zoomed != nil { 577 - tree = tree.settingZoomed(nil) 578 - trees[tabId] = tree 584 + if splitPreserveZoomOnNavigation() { 585 + let nextNode = tree.root?.node(view: nextSurface) 586 + tree = tree.settingZoomed(nextNode) 587 + } else { 588 + tree = tree.settingZoomed(nil) 589 + } 590 + updateTree(tree, for: tabId) 579 591 } 580 592 focusSurface(nextSurface, in: tabId) 581 593 syncFocusIfNeeded()
+11
supacode/Infrastructure/Ghostty/GhosttyRuntime.swift
··· 557 557 return true 558 558 } 559 559 560 + func splitPreserveZoomOnNavigation() -> Bool { 561 + guard let config else { return false } 562 + var value: CUnsignedInt = 0 563 + let key = "split-preserve-zoom" 564 + guard ghostty_config_get(config, &value, key, UInt(key.count)) else { return false } 565 + // Ghostty's C API bitcasts packed structs into c_uint; the first field maps to bit 0. 566 + // https://github.com/ghostty-org/ghostty/blob/6057f8d/src/config/c_get.zig#L74-L84 567 + // https://github.com/ghostty-org/ghostty/blob/6057f8d/src/config/c_get.zig#L226-L240 568 + return value & (1 << 0) != 0 569 + } 570 + 560 571 func backgroundOpacity() -> Double { 561 572 guard let config else { return 1 } 562 573 var value: Double = 1
+80
supacodeTests/SplitTreeTests.swift
··· 31 31 #expect(tree.focusTargetAfterClosing(node) === second) 32 32 } 33 33 34 + @Test func focusTargetNextWrapsAroundFromZoomedNode() throws { 35 + let first = SplitTreeTestView() 36 + let second = SplitTreeTestView() 37 + let third = SplitTreeTestView() 38 + 39 + let tree = try SplitTree(view: first) 40 + .inserting(view: second, at: first, direction: .right) 41 + .inserting(view: third, at: second, direction: .right) 42 + 43 + let zoomedNode = tree.find(id: second.id)! 44 + let zoomed = tree.settingZoomed(zoomedNode) 45 + 46 + let next = zoomed.focusTarget(for: .next, from: zoomedNode) 47 + #expect(next === third) 48 + 49 + let nextNode = zoomed.find(id: third.id)! 50 + let rezoomed = zoomed.settingZoomed(nextNode) 51 + #expect(rezoomed.visibleLeaves().count == 1) 52 + #expect(rezoomed.visibleLeaves().first === third) 53 + } 54 + 34 55 @Test func visibleLeavesOnlyReturnZoomedPane() throws { 35 56 let first = SplitTreeTestView() 36 57 let second = SplitTreeTestView() ··· 44 65 #expect(visibleLeaves.count == 1) 45 66 #expect(visibleLeaves.first === second) 46 67 } 68 + 69 + @Test func gotoSplitPreservesZoomWhenConfigured() throws { 70 + let fixture = makeWorktreeFixture(preserveZoomOnNavigation: true) 71 + let first = fixture.first 72 + let second = try #require(fixture.second) 73 + 74 + #expect(fixture.state.performSplitAction(.toggleSplitZoom, for: first.id)) 75 + #expect(fixture.state.performSplitAction(.gotoSplit(direction: .next), for: first.id)) 76 + 77 + let visibleLeaves = fixture.state.splitTree(for: fixture.tabId).visibleLeaves() 78 + #expect(visibleLeaves.count == 1) 79 + #expect(visibleLeaves.first === second) 80 + } 81 + 82 + @Test func gotoSplitClearsZoomWhenNotConfigured() throws { 83 + let fixture = makeWorktreeFixture(preserveZoomOnNavigation: false) 84 + let first = fixture.first 85 + 86 + #expect(fixture.state.performSplitAction(.toggleSplitZoom, for: first.id)) 87 + #expect(fixture.state.performSplitAction(.gotoSplit(direction: .next), for: first.id)) 88 + 89 + let visibleLeaves = fixture.state.splitTree(for: fixture.tabId).visibleLeaves() 90 + #expect(visibleLeaves.count == 2) 91 + } 92 + 93 + private func makeWorktreeFixture(preserveZoomOnNavigation: Bool) -> WorktreeFixture { 94 + let state = WorktreeTerminalState( 95 + runtime: GhosttyRuntime(), 96 + worktree: makeWorktree(), 97 + splitPreserveZoomOnNavigation: { preserveZoomOnNavigation } 98 + ) 99 + let tabId = state.createTab()! 100 + let first = state.splitTree(for: tabId).root!.leftmostLeaf() 101 + _ = state.performSplitAction(.newSplit(direction: .right), for: first.id) 102 + let leaves = state.splitTree(for: tabId).leaves() 103 + return WorktreeFixture( 104 + state: state, 105 + tabId: tabId, 106 + first: first, 107 + second: leaves.first { $0.id != first.id } 108 + ) 109 + } 110 + 111 + private func makeWorktree() -> Worktree { 112 + Worktree( 113 + id: "/tmp/repo/wt-1", 114 + name: "wt-1", 115 + detail: "detail", 116 + workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt-1"), 117 + repositoryRootURL: URL(fileURLWithPath: "/tmp/repo") 118 + ) 119 + } 120 + } 121 + 122 + private struct WorktreeFixture { 123 + let state: WorktreeTerminalState 124 + let tabId: TerminalTabID 125 + let first: GhosttySurfaceView 126 + let second: GhosttySurfaceView? 47 127 } 48 128 49 129 private final class SplitTreeTestView: NSView, Identifiable {