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/CommandPaletteIntent.swift",
|
||||||
"Features/App Intents/Entities/CommandEntity.swift",
|
"Features/App Intents/Entities/CommandEntity.swift",
|
||||||
"Features/App Intents/Entities/TerminalEntity.swift",
|
"Features/App Intents/Entities/TerminalEntity.swift",
|
||||||
|
"Features/App Intents/FocusTerminalIntent.swift",
|
||||||
"Features/App Intents/GetTerminalDetailsIntent.swift",
|
"Features/App Intents/GetTerminalDetailsIntent.swift",
|
||||||
"Features/App Intents/GhosttyIntentError.swift",
|
"Features/App Intents/GhosttyIntentError.swift",
|
||||||
"Features/App Intents/InputIntent.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
|
// 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>) {
|
override func surfaceTreeDidChange(from: SplitTree<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
|
||||||
super.surfaceTreeDidChange(from: from, to: to)
|
super.surfaceTreeDidChange(from: from, to: to)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,21 @@ class BaseTerminalController: NSWindowController,
|
||||||
return newView
|
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.
|
/// Called when the surfaceTree variable changed.
|
||||||
///
|
///
|
||||||
/// Subclasses should call super first.
|
/// Subclasses should call super first.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue