From 323d362bc18003ef94a1b69ce27b9833573a2aa1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Jan 2026 09:33:32 -0800 Subject: [PATCH] macos: dragging last window out of quick terminal works --- .../Terminal/BaseTerminalController.swift | 24 +------ .../Terminal/TerminalController.swift | 64 ++++++++++++------- 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 71daafaeb..88d8e39d8 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -457,7 +457,7 @@ class BaseTerminalController: NSWindowController, ) } - private func replaceSurfaceTree( + func replaceSurfaceTree( _ newTree: SplitTree, moveFocusTo newView: Ghostty.SurfaceView? = nil, moveFocusFrom oldView: Ghostty.SurfaceView? = nil, @@ -930,7 +930,6 @@ class BaseTerminalController: NSWindowController, // Remove from source controller's tree and add it to our tree. // We do this first because if there is an error then we can // abort. - let sourceTreeWithoutNode = sourceController.surfaceTree.removing(sourceNode) let newTree: SplitTree do { newTree = try surfaceTree.inserting(view: source, at: destination, direction: direction) @@ -946,25 +945,8 @@ class BaseTerminalController: NSWindowController, undoManager?.endUndoGrouping() } - if sourceTreeWithoutNode.isEmpty { - // If our source tree is becoming empty, then we're closing this terminal. - // We need to handle this carefully to get undo to work properly. If the - // controller is a TerminalController this is easy because it has a way - // to do this. - if let c = sourceController as? TerminalController { - c.closeTabImmediately() - } else { - // Not a TerminalController so we always undo into a new window. - _ = TerminalController.newWindow( - sourceController.ghostty, - tree: sourceController.surfaceTree, - confirmUndo: false) - } - } else { - // The source isn't empty so we can do a simple remove which will handle - // the undo properly. - sourceController.removeSurfaceNode(sourceNode) - } + // Remove the node from the source. + sourceController.removeSurfaceNode(sourceNode) // Add in the surface to our tree replaceSurfaceTree( diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index abaedbe41..c7f9fe086 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -8,16 +8,16 @@ import GhosttyKit class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Controller { override var windowNibName: NSNib.Name? { let defaultValue = "Terminal" - + guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue } let config = appDelegate.ghostty.config - + // If we have no window decorations, there's no reason to do anything but // the default titlebar (because there will be no titlebar). if !config.windowDecorations { return defaultValue } - + let nib = switch config.macosTitlebarStyle { case "native": "Terminal" case "hidden": "TerminalHiddenTitlebar" @@ -34,33 +34,33 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr #endif default: defaultValue } - + return nib } - + /// This is set to true when we care about frame changes. This is a small optimization since /// this controller registers a listener for ALL frame change notifications and this lets us bail /// early if we don't care. private var tabListenForFrame: Bool = false - + /// This is the hash value of the last tabGroup.windows array. We use this to detect order /// changes in the list. private var tabWindowsHash: Int = 0 - + /// This is set to false by init if the window managed by this controller should not be restorable. /// For example, terminals executing custom scripts are not restorable. private var restorable: Bool = true - + /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig - - + + /// The notification cancellable for focused surface property changes. private var surfaceAppearanceCancellables: Set = [] - + /// This will be set to the initial frame of the window from the xib on load. private var initialFrame: NSRect? = nil - + init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withSurfaceTree tree: SplitTree? = nil, @@ -72,12 +72,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // as the script. We may want to revisit this behavior when we have scrollback // restoration. self.restorable = (base?.command ?? "") == "" - + // Setup our initial derived config based on the current app config self.derivedConfig = DerivedConfig(ghostty.config) - + super.init(ghostty, baseConfig: base, surfaceTree: tree) - + // Setup our notifications for behaviors let center = NotificationCenter.default center.addObserver( @@ -134,36 +134,56 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr object: nil ) } - + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported for this view") } - + deinit { // Remove all of our notificationcenter subscriptions let center = NotificationCenter.default center.removeObserver(self) } - + // MARK: Base Controller Overrides - + override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { super.surfaceTreeDidChange(from: from, to: to) - + // Whenever our surface tree changes in any way (new split, close split, etc.) // we want to invalidate our state. invalidateRestorableState() - + // Update our zoom state if let window = window as? TerminalWindow { window.surfaceIsZoomed = to.zoomed != nil } - + // If our surface tree is now nil then we close our window. if (to.isEmpty) { self.window?.close() } } + + override func replaceSurfaceTree( + _ newTree: SplitTree, + moveFocusTo newView: Ghostty.SurfaceView? = nil, + moveFocusFrom oldView: Ghostty.SurfaceView? = nil, + undoAction: String? = nil + ) { + // We have a special case if our tree is empty to close our tab immediately. + // This makes it so that undo is handled properly. + if newTree.isEmpty { + closeTabImmediately() + return + } + + super.replaceSurfaceTree( + newTree, + moveFocusTo: newView, + moveFocusFrom: oldView, + undoAction: undoAction) + } // MARK: Terminal Creation