macOS: Focus Terminal App Intent (#8961)
Closes #8791 Discussion: #8657 ### Summary This adds the FocusTerminalIntent App Intent and related function (focusSurface), allows external tools (such as Shortcuts/Siri) to programmatically move focus to a specific terminal window or tab. ### Verification This functionality has been tested across following scenarios, confirming correct focus behavior for: - Split Window - Tab Group - Quick Terminal ### Note It is not supported to move focus to a split that is hidden by a zoomed split. The same applies to the CloseTerminalIntent. ### AI Disclosure This pull request was made with assistance from Claude Code. I reviewed all AI-generated code and wrote the final output manually.pull/8938/head
commit
0cc3728803
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
|
||||
super.surfaceTreeDidChange(from: from, to: to)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue