Skip to content
Merged
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
61 changes: 61 additions & 0 deletions macos/Sources/Ghostty/Ghostty.App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,9 @@ extension Ghostty {
case GHOSTTY_ACTION_GOTO_SPLIT:
return gotoSplit(app, target: target, direction: action.action.goto_split)

case GHOSTTY_ACTION_GOTO_WINDOW:
return gotoWindow(app, target: target, direction: action.action.goto_window)

case GHOSTTY_ACTION_RESIZE_SPLIT:
resizeSplit(app, target: target, resize: action.action.resize_split)

Expand Down Expand Up @@ -1149,6 +1152,64 @@ extension Ghostty {
}
}

private static func gotoWindow(
_ app: ghostty_app_t,
target: ghostty_target_s,
direction: ghostty_action_goto_window_e
) -> Bool {
// Collect candidate windows: visible terminal windows that are either
// standalone or the currently selected tab in their tab group. This
// treats each native tab group as a single "window" for navigation
// purposes, since goto_tab handles per-tab navigation.
let candidates: [NSWindow] = NSApplication.shared.windows.filter { window in
guard window.windowController is BaseTerminalController else { return false }
guard window.isVisible, !window.isMiniaturized else { return false }
// For native tabs, only include the selected tab in each group
if let group = window.tabGroup, group.selectedWindow !== window {
return false
}
return true
}

// Need at least two windows to navigate between
guard candidates.count > 1 else { return false }

// Find starting index from the current key/main window
let startIndex = candidates.firstIndex(where: { $0.isKeyWindow })
?? candidates.firstIndex(where: { $0.isMainWindow })
?? 0

let step: Int
switch direction {
case GHOSTTY_GOTO_WINDOW_NEXT:
step = 1
case GHOSTTY_GOTO_WINDOW_PREVIOUS:
step = -1
default:
return false
}

// Iterate with wrap-around until we find a valid window or return to start
let count = candidates.count
var index = (startIndex + step + count) % count

while index != startIndex {
let candidate = candidates[index]
if candidate.isVisible, !candidate.isMiniaturized {
candidate.makeKeyAndOrderFront(nil)
// Also focus the terminal surface within the window
if let controller = candidate.windowController as? BaseTerminalController,
let surface = controller.focusedSurface {
Ghostty.moveFocus(to: surface)
}
return true
}
index = (index + step + count) % count
}

return false
}

private static func resizeSplit(
_ app: ghostty_app_t,
target: ghostty_target_s,
Expand Down