diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 3f4444fd9..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) } @@ -136,7 +162,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 +171,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,43 +196,29 @@ 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() + // 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: []) + } }) } - private func focusNextWindow() { - // We only want to consider windows that are visible - let windows = NSApp.windows.filter { $0.isVisible } - - // 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) {