From c8a40a7791ff121425b1696891174627563a7370 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 29 Sep 2024 14:04:48 -0700 Subject: [PATCH 1/3] macos: quick terminal close focuses next window on same screen/space Previously, we'd find the next Ghostty window anywhere. Now we find the one on the same screen/space to avoid moving the focus to a different screen. --- .../QuickTerminalController.swift | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 3f4444fd9..c7e833499 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -177,13 +177,24 @@ class QuickTerminalController: BaseTerminalController { position.setInitial(in: window.animator(), on: screen) }, completionHandler: { guard wasKey else { return } - self.focusNextWindow() + self.focusNextWindow(on: screen) }) } - private func focusNextWindow() { - // We only want to consider windows that are visible - let windows = NSApp.windows.filter { $0.isVisible } + private func focusNextWindow(on screen: NSScreen) { + let windows = NSApp.windows.filter { + // Visible, otherwise we'll make an invisible window visible. + guard $0.isVisible else { return false } + + // Same screen, just a preference... + guard $0.screen == screen else { return false } + + // Same space (virtual screen). Otherwise we'll force an animation to + // another space which is very jarring. + guard $0.isOnActiveSpace else { return false } + + return true + } // If we have no windows there is nothing to focus. guard !windows.isEmpty else { return } From c70e0b2634b942e6eff666d9b13fac57893a280c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 29 Sep 2024 15:06:29 -0700 Subject: [PATCH 2/3] macos: use orderOut which handles all of our focus logic for us --- .../QuickTerminalController.swift | 71 +++++-------------- 1 file changed, 19 insertions(+), 52 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index c7e833499..bb15cc753 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -136,7 +136,7 @@ class QuickTerminalController: BaseTerminalController { position.setInitial(in: window, on: screen) // Move it to the visible position since animation requires this - window.makeKeyAndOrderFront(nil) + window.makeKeyAndOrderFront(self) // Run the animation that moves our window into the proper place and makes // it visible. @@ -145,20 +145,24 @@ class QuickTerminalController: BaseTerminalController { context.timingFunction = .init(name: .easeIn) position.setFinal(in: window.animator(), on: screen) }, completionHandler: { - // If we canceled our animation in we do nothing - guard self.visible else { return } + // There is a very minor delay here so waiting at least an event loop tick + // keeps us safe from the view not being on the window. + DispatchQueue.main.async { + // If we canceled our animation in we do nothing + guard self.visible else { return } - // If our focused view is somehow not connected to this window then the - // function calls below do nothing. I don't think this is possible but - // we should guard against it because it is a Cocoa assertion. - guard let focusedView = self.focusedSurface, - focusedView.window == window else { return } + // If our focused view is somehow not connected to this window then the + // function calls below do nothing. I don't think this is possible but + // we should guard against it because it is a Cocoa assertion. + guard let focusedView = self.focusedSurface, + focusedView.window == window else { return } - // The window must become top-level - window.makeKeyAndOrderFront(self) + // The window must become top-level + window.makeKeyAndOrderFront(self) - // The view must gain our keyboard focus - window.makeFirstResponder(focusedView) + // The view must gain our keyboard focus + window.makeFirstResponder(focusedView) + } }) } @@ -166,54 +170,17 @@ class QuickTerminalController: BaseTerminalController { // We always animate out to whatever screen the window is actually on. guard let screen = window.screen ?? NSScreen.main else { return } - // Keep track of if we were the key window. If we were the key window then we - // want to move focus to the next window so that focus is preserved somewhere - // in the app. - let wasKey = window.isKeyWindow - NSAnimationContext.runAnimationGroup({ context in context.duration = 0.2 context.timingFunction = .init(name: .easeIn) position.setInitial(in: window.animator(), on: screen) }, completionHandler: { - guard wasKey else { return } - self.focusNextWindow(on: screen) + // This causes the window to be removed from the screen list and macOS + // handles what should be focused next. + window.orderOut(self) }) } - private func focusNextWindow(on screen: NSScreen) { - let windows = NSApp.windows.filter { - // Visible, otherwise we'll make an invisible window visible. - guard $0.isVisible else { return false } - - // Same screen, just a preference... - guard $0.screen == screen else { return false } - - // Same space (virtual screen). Otherwise we'll force an animation to - // another space which is very jarring. - guard $0.isOnActiveSpace else { return false } - - return true - } - - // If we have no windows there is nothing to focus. - guard !windows.isEmpty else { return } - - // Find the current key window (the window that is currently focused) - if let keyWindow = NSApp.keyWindow, - let currentIndex = windows.firstIndex(of: keyWindow) { - // Calculate the index of the next window (cycle through the list) - let nextIndex = (currentIndex + 1) % windows.count - let nextWindow = windows[nextIndex] - - // Make the next window key and bring it to the front - nextWindow.makeKeyAndOrderFront(nil) - } else { - // If there's no key window, focus the first available window - windows.first?.makeKeyAndOrderFront(nil) - } - } - // MARK: First Responder @IBAction override func closeWindow(_ sender: Any) { From 19012cb6f53c5d8792bb6193e8cc7a88eea88c26 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 29 Sep 2024 15:32:53 -0700 Subject: [PATCH 3/3] macos: quick terminal restores focus to previous application --- .../QuickTerminalController.swift | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index bb15cc753..ecf0114de 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -13,6 +13,11 @@ class QuickTerminalController: BaseTerminalController { /// The current state of the quick terminal private(set) var visible: Bool = false + /// The previously running application when the terminal is shown. This is NEVER Ghostty. + /// If this is set then when the quick terminal is animated out then we will restore this + /// application to the front. + private var previousApp: NSRunningApplication? = nil + init(_ ghostty: Ghostty.App, position: QuickTerminalPosition = .top, baseConfig base: Ghostty.SurfaceConfiguration? = nil, @@ -58,10 +63,23 @@ class QuickTerminalController: BaseTerminalController { override func windowDidResignKey(_ notification: Notification) { super.windowDidResignKey(notification) + // If we're not visible then we don't want to run any of the logic below + // because things like resetting our previous app assume we're visible. + // windowDidResignKey will also get called after animateOut so this + // ensures we don't run logic twice. + guard visible else { return } + // We don't animate out if there is a modal sheet being shown currently. // This lets us show alerts without causing the window to disappear. guard window?.attachedSheet == nil else { return } + // If our app is still active, then it means that we're switching + // to another window within our app, so we remove the previous app + // so we don't restore it. + if NSApp.isActive { + self.previousApp = nil + } + animateOut() } @@ -103,6 +121,14 @@ class QuickTerminalController: BaseTerminalController { // If our application is not active, then we grab focus. The quick terminal // always grabs focus on animation in. if !NSApp.isActive { + // If we have a previously focused application and it isn't us, then + // we want to store it so we can restore state later. + if let previousApp = NSWorkspace.shared.frontmostApplication, + previousApp.bundleIdentifier != Bundle.main.bundleIdentifier + { + self.previousApp = previousApp + } + NSApp.activate(ignoringOtherApps: true) } @@ -178,6 +204,18 @@ class QuickTerminalController: BaseTerminalController { // This causes the window to be removed from the screen list and macOS // handles what should be focused next. window.orderOut(self) + + // If we have a previously active application, restore focus to it. + if let previousApp = self.previousApp { + // Make sure we unset the state no matter what + self.previousApp = nil + + // If the app is terminated to nothing + guard !previousApp.isTerminated else { return } + + // Ignore the result, it doesn't change our behavior. + _ = previousApp.activate(options: []) + } }) }