From bbbb52ed7534a580ad2b3a5ca9df282eaef1e820 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 19 Oct 2025 20:29:36 -0700 Subject: [PATCH] macos: goto_split direction is performable (#9284) Fixes #9283 There was a comment here noting this deficiency. GTK implements this properly. --- .../Terminal/BaseTerminalController.swift | 13 +------- macos/Sources/Ghostty/Ghostty.App.swift | 28 ++++++++++++----- macos/Sources/Ghostty/Package.swift | 31 +++++++++++++++++++ 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index a56d070b4..f2e8f8e45 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -551,23 +551,12 @@ class BaseTerminalController: NSWindowController, // Get the direction from the notification guard let directionAny = notification.userInfo?[Ghostty.Notification.SplitDirectionKey] else { return } guard let direction = directionAny as? Ghostty.SplitFocusDirection else { return } - - // Convert Ghostty.SplitFocusDirection to our SplitTree.FocusDirection - let focusDirection: SplitTree.FocusDirection - switch direction { - case .previous: focusDirection = .previous - case .next: focusDirection = .next - case .up: focusDirection = .spatial(.up) - case .down: focusDirection = .spatial(.down) - case .left: focusDirection = .spatial(.left) - case .right: focusDirection = .spatial(.right) - } // Find the node for the target surface guard let targetNode = surfaceTree.root?.node(view: target) else { return } // Find the next surface to focus - guard let nextSurface = surfaceTree.focusTarget(for: focusDirection, from: targetNode) else { + guard let nextSurface = surfaceTree.focusTarget(for: direction.toSplitTreeFocusDirection(), from: targetNode) else { return } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index bdc64e9e1..598e95a4c 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -1025,26 +1025,38 @@ extension Ghostty { guard let surfaceView = self.surfaceView(from: surface) else { return false } guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { return false } - // For now, we return false if the window has no splits and we return - // true if the window has ANY splits. This isn't strictly correct because - // we should only be returning true if we actually performed the action, - // but this handles the most common case of caring about goto_split performability - // which is the no-split case. + // If the window has no splits, the action is not performable guard controller.surfaceTree.isSplit else { return false } + // Convert the C API direction to our Swift type + guard let splitDirection = SplitFocusDirection.from(direction: direction) else { return false } + + // Find the current node in the tree + guard let targetNode = controller.surfaceTree.root?.node(view: surfaceView) else { return false } + + // Check if a split actually exists in the target direction before + // returning true. This ensures performable keybinds only consume + // the key event when we actually perform navigation. + let focusDirection: SplitTree.FocusDirection = splitDirection.toSplitTreeFocusDirection() + guard controller.surfaceTree.focusTarget(for: focusDirection, from: targetNode) != nil else { + return false + } + + // We have a valid target, post the notification to perform the navigation NotificationCenter.default.post( name: Notification.ghosttyFocusSplit, object: surfaceView, userInfo: [ - Notification.SplitDirectionKey: SplitFocusDirection.from(direction: direction) as Any, + Notification.SplitDirectionKey: splitDirection as Any, ] ) + return true + default: assertionFailure() + return false } - - return true } private static func resizeSplit( diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 85040d390..80d413f9c 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -223,7 +223,38 @@ extension Ghostty { } } } +} +#if canImport(AppKit) +// MARK: SplitFocusDirection Extensions + +extension Ghostty.SplitFocusDirection { + /// Convert to a SplitTree.FocusDirection for the given ViewType. + func toSplitTreeFocusDirection() -> SplitTree.FocusDirection { + switch self { + case .previous: + return .previous + + case .next: + return .next + + case .up: + return .spatial(.up) + + case .down: + return .spatial(.down) + + case .left: + return .spatial(.left) + + case .right: + return .spatial(.right) + } + } +} +#endif + +extension Ghostty { /// The type of a clipboard request enum ClipboardRequest { /// A direct paste of clipboard contents