diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index cd9e56186..e53f6d468 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -75,6 +75,7 @@ "Features/App Intents/CommandPaletteIntent.swift", "Features/App Intents/Entities/CommandEntity.swift", "Features/App Intents/Entities/TerminalEntity.swift", + "Features/App Intents/FocusTerminalIntent.swift", "Features/App Intents/GetTerminalDetailsIntent.swift", "Features/App Intents/GhosttyIntentError.swift", "Features/App Intents/InputIntent.swift", diff --git a/macos/Sources/Features/App Intents/FocusTerminalIntent.swift b/macos/Sources/Features/App Intents/FocusTerminalIntent.swift new file mode 100644 index 000000000..4e813e842 --- /dev/null +++ b/macos/Sources/Features/App Intents/FocusTerminalIntent.swift @@ -0,0 +1,35 @@ +import AppKit +import AppIntents +import GhosttyKit + +struct FocusTerminalIntent: AppIntent { + static var title: LocalizedStringResource = "Focus Terminal" + static var description = IntentDescription("Move focus to an existing terminal.") + + @Parameter( + title: "Terminal", + description: "The terminal to focus.", + ) + var terminal: TerminalEntity + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = .background + + @MainActor + func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + + guard let surfaceView = terminal.surfaceView else { + throw GhosttyIntentError.surfaceNotFound + } + + guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { + return .result() + } + + controller.focusSurface(surfaceView) + return .result() + } +} diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 65186c5d7..fcc8c6505 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -247,6 +247,22 @@ class QuickTerminalController: BaseTerminalController { // MARK: Base Controller Overrides + override func focusSurface(_ view: Ghostty.SurfaceView) { + if visible { + // If we're visible, we just focus the surface as normal. + super.focusSurface(view) + return + } + // Check if target surface belongs to this quick terminal + guard surfaceTree.contains(view) else { return } + // Set the target surface as focused + DispatchQueue.main.async { + Ghostty.moveFocus(to: view) + } + // Animation completion handler will handle window/app activation + animateIn() + } + override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { super.surfaceTreeDidChange(from: from, to: to) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 2de967daf..a34be4125 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -233,6 +233,21 @@ class BaseTerminalController: NSWindowController, return newView } + /// Move focus to a surface view. + func focusSurface(_ view: Ghostty.SurfaceView) { + // Check if target surface is in our tree + guard surfaceTree.contains(view) else { return } + + // Move focus to the target surface and activate the window/app + DispatchQueue.main.async { + Ghostty.moveFocus(to: view) + view.window?.makeKeyAndOrderFront(nil) + if !NSApp.isActive { + NSApp.activate(ignoringOtherApps: true) + } + } + } + /// Called when the surfaceTree variable changed. /// /// Subclasses should call super first.