macos: dragging last window out of quick terminal works

This commit is contained in:
Mitchell Hashimoto
2026-01-07 09:33:32 -08:00
parent a265462aa6
commit 323d362bc1
2 changed files with 45 additions and 43 deletions

View File

@@ -457,7 +457,7 @@ class BaseTerminalController: NSWindowController,
)
}
private func replaceSurfaceTree(
func replaceSurfaceTree(
_ newTree: SplitTree<Ghostty.SurfaceView>,
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<Ghostty.SurfaceView>
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(

View File

@@ -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<AnyCancellable> = []
/// 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<Ghostty.SurfaceView>? = 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<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
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<Ghostty.SurfaceView>,
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