mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-20 18:56:08 +03:00
Merge pull request #2327 from ghostty-org/quickterm
macOS: Quick Terminal focus improvements
This commit is contained in:
@ -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) {
|
||||
|
Reference in New Issue
Block a user