fix: ensure terminal windows don't part from parent window when toggling visibility (#4789)

As of version 1.0.1 (macOS build) when running the toggle visibility
action a window with tabs is made into multiple windows.

This PR ensures terminal tabs are reconstructed and correctly placed
into its parent window.

# Demo


https://github.com/user-attachments/assets/44f14bca-15a1-4717-ba0a-44f0767feec3


FYI: I will create another PR to ensure the right tab is focused after
the main window is restored.

Solves #4329
This commit is contained in:
Mitchell Hashimoto
2025-01-10 14:52:33 -08:00
committed by GitHub
4 changed files with 45 additions and 27 deletions

View File

@ -72,6 +72,7 @@
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; }; A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; };
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; }; A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; };
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; }; A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; }; A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; };
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; }; A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
@ -167,6 +168,7 @@
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = "<group>"; }; A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = "<group>"; };
A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; }; A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; };
A5CBD0572C9F30860017A1AE /* Cursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = "<group>"; }; A5CBD0572C9F30860017A1AE /* Cursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = "<group>"; };
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QuickTerminal.xib; sourceTree = "<group>"; }; A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QuickTerminal.xib; sourceTree = "<group>"; };
@ -282,6 +284,7 @@
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
A5985CD62C320C4500C57AD3 /* String+Extension.swift */, A5985CD62C320C4500C57AD3 /* String+Extension.swift */,
A5CC36142C9CDA03004D6760 /* View+Extension.swift */, A5CC36142C9CDA03004D6760 /* View+Extension.swift */,
A5CA378D2D31D6C100931030 /* Weak.swift */,
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */, C1F26EE72B76CBFC00404083 /* VibrantLayer.h */,
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */, C1F26EE82B76CBFC00404083 /* VibrantLayer.m */,
A5CEAFDA29B8005900646FDA /* SplitView */, A5CEAFDA29B8005900646FDA /* SplitView */,
@ -647,6 +650,7 @@
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */, A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */,
A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */, A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */,
A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */, A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */,
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */,
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,

View File

@ -92,10 +92,8 @@ class AppDelegate: NSObject,
return ProcessInfo.processInfo.systemUptime - applicationLaunchTime return ProcessInfo.processInfo.systemUptime - applicationLaunchTime
} }
/// Tracks whether the application is currently visible. This can be gamed, i.e. if a user manually /// Tracks the windows that we hid for toggleVisibility.
/// brings each window one by one to the front. But at worst its off by one set of toggles and this private var hiddenWindows: [Weak<NSWindow>] = []
/// makes our logic very easy.
private var isVisible: Bool = true
/// The observer for the app appearance. /// The observer for the app appearance.
private var appearanceObserver: NSKeyValueObservation? = nil private var appearanceObserver: NSKeyValueObservation? = nil
@ -219,15 +217,20 @@ class AppDelegate: NSObject,
} }
func applicationDidBecomeActive(_ notification: Notification) { func applicationDidBecomeActive(_ notification: Notification) {
guard !applicationHasBecomeActive else { return } // If we're back then clear the hidden windows
applicationHasBecomeActive = true self.hiddenWindows = []
// Let's launch our first window. We only do this if we have no other windows. It // First launch stuff
// is possible to have other windows in a few scenarios: if (!applicationHasBecomeActive) {
// - if we're opening a URL since `application(_:openFile:)` is called before this. applicationHasBecomeActive = true
// - if we're restoring from persisted state
if terminalManager.windows.count == 0 && derivedConfig.initialWindow { // Let's launch our first window. We only do this if we have no other windows. It
terminalManager.newWindow() // is possible to have other windows in a few scenarios:
// - if we're opening a URL since `application(_:openFile:)` is called before this.
// - if we're restoring from persisted state
if terminalManager.windows.count == 0 && derivedConfig.initialWindow {
terminalManager.newWindow()
}
} }
} }
@ -706,21 +709,23 @@ class AppDelegate: NSObject,
/// Toggles visibility of all Ghosty Terminal windows. When hidden, activates Ghostty as the frontmost application /// Toggles visibility of all Ghosty Terminal windows. When hidden, activates Ghostty as the frontmost application
@IBAction func toggleVisibility(_ sender: Any) { @IBAction func toggleVisibility(_ sender: Any) {
// We only care about terminal windows. // If we have focus, then we hide all windows.
for window in NSApp.windows.filter({ $0.windowController is BaseTerminalController }) { if NSApp.isActive {
if isVisible { // We need to keep track of the windows that were visible because we only
window.orderOut(nil) // want to bring back these windows if we remove the toggle.
} else { self.hiddenWindows = NSApp.windows.filter { $0.isVisible }.map { Weak($0) }
window.makeKeyAndOrderFront(nil) NSApp.hide(nil)
} return
} }
// After bringing them all to front we make sure our app is active too. // If we're not active, we want to become active
if !isVisible { NSApp.activate(ignoringOtherApps: true)
NSApp.activate(ignoringOtherApps: true)
}
isVisible.toggle() // Bring all windows to the front. Note: we don't use NSApp.unhide because
// that will unhide ALL hidden windows. We want to only bring forward the
// ones that we hid.
self.hiddenWindows.forEach { $0.value?.orderFrontRegardless() }
self.hiddenWindows = []
} }
private struct DerivedConfig { private struct DerivedConfig {

View File

@ -0,0 +1,9 @@
/// A wrapper that holds a weak reference to an object. This lets us create native containers
/// of weak references.
class Weak<T: AnyObject> {
weak var value: T?
init(_ value: T) {
self.value = value
}
}

View File

@ -441,10 +441,10 @@ pub const Action = union(enum) {
toggle_quick_terminal: void, toggle_quick_terminal: void,
/// Show/hide all windows. If all windows become shown, we also ensure /// Show/hide all windows. If all windows become shown, we also ensure
/// Ghostty is focused. /// Ghostty becomes focused. When hiding all windows, focus is yielded
/// to the next application as determined by the OS.
/// ///
/// This currently only works on macOS. When hiding all windows, we do /// This currently only works on macOS.
/// not yield focus to the previous application.
toggle_visibility: void, toggle_visibility: void,
/// Quit ghostty. /// Quit ghostty.