From 9d33545a55e4a2ed3346408ef3acf50e80964e29 Mon Sep 17 00:00:00 2001 From: himura467 Date: Sun, 28 Sep 2025 19:20:00 +0900 Subject: [PATCH 1/9] feat: focus terminal in basic cases --- .../App Intents/FocusTerminalIntent.swift | 35 +++++++++++++++++++ .../Terminal/BaseTerminalController.swift | 10 ++++++ 2 files changed, 45 insertions(+) create mode 100644 macos/Sources/Features/App Intents/FocusTerminalIntent.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/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 2de967daf..abf7d4a61 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -233,6 +233,16 @@ class BaseTerminalController: NSWindowController, return newView } + func focusSurface(_ view: Ghostty.SurfaceView) { + guard surfaceTree.contains(view) else { return } + + DispatchQueue.main.async { + Ghostty.moveFocus(to: view, from: self.focusedSurface) + view.window?.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + } + } + /// Called when the surfaceTree variable changed. /// /// Subclasses should call super first. From 8151f4bbf5ef90fc404a096c0bc33f910bc1311f Mon Sep 17 00:00:00 2001 From: himura467 Date: Mon, 29 Sep 2025 00:00:41 +0900 Subject: [PATCH 2/9] feat: focusSurface for quick terminal --- .../QuickTerminal/QuickTerminalController.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 65186c5d7..c7080d7de 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -247,6 +247,18 @@ 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 + } + animateIn() + DispatchQueue.main.asyncAfter(deadline: .now() + derivedConfig.quickTerminalAnimationDuration) { + super.focusSurface(view) + } + } + override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { super.surfaceTreeDidChange(from: from, to: to) From 337ecdd0b36ae6da34adf6c222cec23ec79158f1 Mon Sep 17 00:00:00 2001 From: himura467 Date: Mon, 29 Sep 2025 00:02:43 +0900 Subject: [PATCH 3/9] refactor(focusSurface): check app status in advance --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index abf7d4a61..ec9ddf83b 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -239,7 +239,9 @@ class BaseTerminalController: NSWindowController, DispatchQueue.main.async { Ghostty.moveFocus(to: view, from: self.focusedSurface) view.window?.makeKeyAndOrderFront(nil) - NSApp.activate(ignoringOtherApps: true) + if !NSApp.isActive { + NSApp.activate(ignoringOtherApps: true) + } } } From 7ab0a7814b82d5867aff2caabcb2de890aa2ff34 Mon Sep 17 00:00:00 2001 From: himura467 Date: Mon, 29 Sep 2025 00:40:47 +0900 Subject: [PATCH 4/9] docs(BaseTerminalController) --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index ec9ddf83b..c1c350f9d 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -233,9 +233,12 @@ 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, from: self.focusedSurface) view.window?.makeKeyAndOrderFront(nil) From a6dd7bbeee5b6d879732aa31e25f07c47a9694cd Mon Sep 17 00:00:00 2001 From: himura467 Date: Mon, 29 Sep 2025 02:02:42 +0900 Subject: [PATCH 5/9] refactor: improve asynchronous delay by delegating window/app activation process to animateIn --- .../QuickTerminal/QuickTerminalController.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index c7080d7de..17650b5c6 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -253,10 +253,14 @@ class QuickTerminalController: BaseTerminalController { super.focusSurface(view) return } - animateIn() - DispatchQueue.main.asyncAfter(deadline: .now() + derivedConfig.quickTerminalAnimationDuration) { - super.focusSurface(view) + // Check if target surface belongs to this quick terminal + guard surfaceTree.contains(view) else { return } + // Set the target surface as focused before animation + DispatchQueue.main.async { + Ghostty.moveFocus(to: view, from: self.focusedSurface) } + // Animation completion handler will handle window/app activation + animateIn() } override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { From bc3d0b7cbc2ff4343243eaed55656eb1a1555ea5 Mon Sep 17 00:00:00 2001 From: himura467 Date: Tue, 30 Sep 2025 05:21:56 +0900 Subject: [PATCH 6/9] fix: the renderer's cursor remains in an unfocused state (block_hollow) --- .../Sources/Features/Terminal/BaseTerminalController.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index c1c350f9d..4298dd1e6 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -240,7 +240,12 @@ class BaseTerminalController: NSWindowController, // Move focus to the target surface and activate the window/app DispatchQueue.main.async { - Ghostty.moveFocus(to: view, from: self.focusedSurface) + // We suppress the spurious unfocus signal by passing nil for `from` + // when the surface is already the logically focused one. + Ghostty.moveFocus( + to: view, + from: (self.focusedSurface == view) ? nil : self.focusedSurface + ) view.window?.makeKeyAndOrderFront(nil) if !NSApp.isActive { NSApp.activate(ignoringOtherApps: true) From b3d0b6a965575da7efecea02f6478136630dee6e Mon Sep 17 00:00:00 2001 From: himura467 Date: Tue, 30 Sep 2025 05:58:21 +0900 Subject: [PATCH 7/9] refactor: no need to set from for moveFocus probably --- .../Features/QuickTerminal/QuickTerminalController.swift | 2 +- .../Sources/Features/Terminal/BaseTerminalController.swift | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 17650b5c6..eaefbf55b 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -257,7 +257,7 @@ class QuickTerminalController: BaseTerminalController { guard surfaceTree.contains(view) else { return } // Set the target surface as focused before animation DispatchQueue.main.async { - Ghostty.moveFocus(to: view, from: self.focusedSurface) + Ghostty.moveFocus(to: view) } // Animation completion handler will handle window/app activation animateIn() diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 4298dd1e6..a34be4125 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -240,12 +240,7 @@ class BaseTerminalController: NSWindowController, // Move focus to the target surface and activate the window/app DispatchQueue.main.async { - // We suppress the spurious unfocus signal by passing nil for `from` - // when the surface is already the logically focused one. - Ghostty.moveFocus( - to: view, - from: (self.focusedSurface == view) ? nil : self.focusedSurface - ) + Ghostty.moveFocus(to: view) view.window?.makeKeyAndOrderFront(nil) if !NSApp.isActive { NSApp.activate(ignoringOtherApps: true) From 373be614828b146d468cf46e8f3958e76f93934d Mon Sep 17 00:00:00 2001 From: himura467 Date: Tue, 30 Sep 2025 06:36:29 +0900 Subject: [PATCH 8/9] docs --- .../Features/QuickTerminal/QuickTerminalController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index eaefbf55b..fcc8c6505 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -255,7 +255,7 @@ class QuickTerminalController: BaseTerminalController { } // Check if target surface belongs to this quick terminal guard surfaceTree.contains(view) else { return } - // Set the target surface as focused before animation + // Set the target surface as focused DispatchQueue.main.async { Ghostty.moveFocus(to: view) } From c58a8b27b6032b4eb20555ddae244c684f71888e Mon Sep 17 00:00:00 2001 From: himura467 Date: Tue, 30 Sep 2025 07:14:09 +0900 Subject: [PATCH 9/9] chore: update iOS membership exceptions --- macos/Ghostty.xcodeproj/project.pbxproj | 1 + 1 file changed, 1 insertion(+) 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",