Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions supacode/Features/Terminal/Models/WorktreeTerminalState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ final class WorktreeTerminalState {

let tabManager: TerminalTabManager
private let runtime: GhosttyRuntime
@ObservationIgnored private let splitPreserveZoomOnNavigation: () -> Bool
private let worktree: Worktree
@ObservationIgnored
@SharedReader private var repositorySettings: RepositorySettings
Expand Down Expand Up @@ -63,8 +64,14 @@ final class WorktreeTerminalState {
var onCommandPaletteToggle: (() -> Void)?
var onSetupScriptConsumed: (() -> Void)?

init(runtime: GhosttyRuntime, worktree: Worktree, runSetupScript: Bool = false) {
init(
runtime: GhosttyRuntime,
worktree: Worktree,
runSetupScript: Bool = false,
splitPreserveZoomOnNavigation: (() -> Bool)? = nil
) {
self.runtime = runtime
self.splitPreserveZoomOnNavigation = splitPreserveZoomOnNavigation ?? { runtime.splitPreserveZoomOnNavigation() }
self.worktree = worktree
self.pendingSetupScript = runSetupScript
self.tabManager = TerminalTabManager()
Expand Down Expand Up @@ -574,8 +581,13 @@ final class WorktreeTerminalState {
return false
}
if tree.zoomed != nil {
tree = tree.settingZoomed(nil)
trees[tabId] = tree
if splitPreserveZoomOnNavigation() {
let nextNode = tree.root?.node(view: nextSurface)
tree = tree.settingZoomed(nextNode)
} else {
tree = tree.settingZoomed(nil)
}
updateTree(tree, for: tabId)
}
focusSurface(nextSurface, in: tabId)
syncFocusIfNeeded()
Expand Down
11 changes: 11 additions & 0 deletions supacode/Infrastructure/Ghostty/GhosttyRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,17 @@ final class GhosttyRuntime {
return true
}

func splitPreserveZoomOnNavigation() -> Bool {
guard let config else { return false }
var value: CUnsignedInt = 0
let key = "split-preserve-zoom"
guard ghostty_config_get(config, &value, key, UInt(key.count)) else { return false }
// Ghostty's C API bitcasts packed structs into c_uint; the first field maps to bit 0.
// https://github.com/ghostty-org/ghostty/blob/6057f8d/src/config/c_get.zig#L74-L84
// https://github.com/ghostty-org/ghostty/blob/6057f8d/src/config/c_get.zig#L226-L240
return value & (1 << 0) != 0
Comment thread
sbertix marked this conversation as resolved.
}

func backgroundOpacity() -> Double {
guard let config else { return 1 }
var value: Double = 1
Expand Down
80 changes: 80 additions & 0 deletions supacodeTests/SplitTreeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,27 @@ struct SplitTreeTests {
#expect(tree.focusTargetAfterClosing(node) === second)
}

@Test func focusTargetNextWrapsAroundFromZoomedNode() throws {
let first = SplitTreeTestView()
let second = SplitTreeTestView()
let third = SplitTreeTestView()

let tree = try SplitTree(view: first)
.inserting(view: second, at: first, direction: .right)
.inserting(view: third, at: second, direction: .right)

let zoomedNode = tree.find(id: second.id)!
let zoomed = tree.settingZoomed(zoomedNode)

let next = zoomed.focusTarget(for: .next, from: zoomedNode)
#expect(next === third)

let nextNode = zoomed.find(id: third.id)!
let rezoomed = zoomed.settingZoomed(nextNode)
#expect(rezoomed.visibleLeaves().count == 1)
#expect(rezoomed.visibleLeaves().first === third)
}

@Test func visibleLeavesOnlyReturnZoomedPane() throws {
let first = SplitTreeTestView()
let second = SplitTreeTestView()
Expand All @@ -44,6 +65,65 @@ struct SplitTreeTests {
#expect(visibleLeaves.count == 1)
#expect(visibleLeaves.first === second)
}

@Test func gotoSplitPreservesZoomWhenConfigured() throws {
let fixture = makeWorktreeFixture(preserveZoomOnNavigation: true)
let first = fixture.first
let second = try #require(fixture.second)

#expect(fixture.state.performSplitAction(.toggleSplitZoom, for: first.id))
#expect(fixture.state.performSplitAction(.gotoSplit(direction: .next), for: first.id))

let visibleLeaves = fixture.state.splitTree(for: fixture.tabId).visibleLeaves()
#expect(visibleLeaves.count == 1)
#expect(visibleLeaves.first === second)
}

@Test func gotoSplitClearsZoomWhenNotConfigured() throws {
let fixture = makeWorktreeFixture(preserveZoomOnNavigation: false)
let first = fixture.first

#expect(fixture.state.performSplitAction(.toggleSplitZoom, for: first.id))
#expect(fixture.state.performSplitAction(.gotoSplit(direction: .next), for: first.id))

let visibleLeaves = fixture.state.splitTree(for: fixture.tabId).visibleLeaves()
#expect(visibleLeaves.count == 2)
}

private func makeWorktreeFixture(preserveZoomOnNavigation: Bool) -> WorktreeFixture {
let state = WorktreeTerminalState(
runtime: GhosttyRuntime(),
worktree: makeWorktree(),
splitPreserveZoomOnNavigation: { preserveZoomOnNavigation }
)
let tabId = state.createTab()!
let first = state.splitTree(for: tabId).root!.leftmostLeaf()
_ = state.performSplitAction(.newSplit(direction: .right), for: first.id)
let leaves = state.splitTree(for: tabId).leaves()
return WorktreeFixture(
state: state,
tabId: tabId,
first: first,
second: leaves.first { $0.id != first.id }
)
}

private func makeWorktree() -> Worktree {
Worktree(
id: "/tmp/repo/wt-1",
name: "wt-1",
detail: "detail",
workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt-1"),
repositoryRootURL: URL(fileURLWithPath: "/tmp/repo")
)
}
}

private struct WorktreeFixture {
let state: WorktreeTerminalState
let tabId: TerminalTabID
let first: GhosttySurfaceView
let second: GhosttySurfaceView?
}

private final class SplitTreeTestView: NSView, Identifiable {
Expand Down
Loading