macos: goto_split direction is performable (#9284)

Fixes #9283

There was a comment here noting this deficiency. GTK implements this
properly.
1.2.x
Mitchell Hashimoto 2025-10-19 20:29:36 -07:00
parent ed91bdadd6
commit bbbb52ed75
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 52 additions and 20 deletions

View File

@ -552,22 +552,11 @@ class BaseTerminalController: NSWindowController,
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<Ghostty.SurfaceView>.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
}

View File

@ -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<Ghostty.SurfaceView>.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(

View File

@ -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<ViewType>() -> SplitTree<ViewType>.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