Expose surface separation in UI entry points

Wire the new detach helpers into the context menu and command palette, document the UI affordances, and wrap the detach/tab creation flow in a single undo grouping.
pull/9457/head
Sash Zats 2025-11-02 12:24:09 -05:00
parent e3bb73a482
commit 0d93afdd3e
2 changed files with 74 additions and 1 deletions

View File

@ -72,6 +72,16 @@ struct TerminalCommandPaletteView: View {
return options
}
if surfaceView.canSeparatePane {
options.append(CommandOption(
title: "Separate Pane into New Tab",
description: "Move the focused pane into its own tab"
) {
isPresented = false
surfaceView.separateIntoNewTab()
})
}
return options
}

View File

@ -1377,6 +1377,10 @@ extension Ghostty {
item.setImageIfDesired(systemSymbolName: "rectangle.bottomhalf.inset.filled")
item = menu.addItem(withTitle: "Split Up", action: #selector(splitUp(_:)), keyEquivalent: "")
item.setImageIfDesired(systemSymbolName: "rectangle.tophalf.inset.filled")
if canSeparatePane {
item = menu.addItem(withTitle: "Separate", action: #selector(separateTab(_:)), keyEquivalent: "")
item.setImageIfDesired(systemSymbolName: "menubar.arrow.up.rectangle")
}
menu.addItem(.separator())
item = menu.addItem(withTitle: "Reset Terminal", action: #selector(resetTerminal(_:)), keyEquivalent: "")
@ -1453,6 +1457,65 @@ extension Ghostty {
ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_UP)
}
/// Moves the currently focused pane into a new tab when possible.
@IBAction func separateTab(_ sender: Any) {
guard canSeparatePane,
let controller = window?.windowController as? TerminalController,
let parentWindow = controller.window,
let delegate = NSApplication.shared.delegate as? AppDelegate else { return }
if let fullscreenStyle = controller.fullscreenStyle,
fullscreenStyle.isFullscreen && !fullscreenStyle.supportsTabs {
let alert = NSAlert()
alert.messageText = "Cannot Create New Tab"
alert.informativeText = "New tabs are unsupported while in non-native fullscreen. Exit fullscreen and try again."
alert.addButton(withTitle: "OK")
alert.alertStyle = .warning
alert.beginSheetModal(for: parentWindow)
return
}
guard let detachResult = controller.detachSurface(self, recordUndo: false) else { return }
let undoManager = controller.undoManager
undoManager?.beginUndoGrouping()
controller.registerDetachUndo(detachResult)
guard let newController = TerminalController.newTab(
delegate.ghostty,
from: parentWindow,
withSurfaceTree: detachResult.detachedTree
) else {
controller.restoreDetachedSurface(detachResult)
undoManager?.removeAllActions(withTarget: controller)
undoManager?.endUndoGrouping()
return
}
newController.focusSurface(self)
if let undoManager {
undoManager.setActionName("Separate Terminal")
undoManager.endUndoGrouping()
}
}
/// Programmatically detaches this surface into a new tab.
///
/// This exists so callers outside the traditional AppKit action chain
/// (for example SwiftUI views or tooling consuming GhosttyKit) can trigger
/// the same behavior exposed by the `Separate Tab` menu item.
func separateIntoNewTab() {
separateTab(self)
}
/// Returns true when the view can be detached into a separate tab.
var canSeparatePane: Bool {
guard let controller = window?.windowController as? BaseTerminalController else { return false }
return controller.canDetachSurface(self)
}
@objc func resetTerminal(_ sender: Any) {
guard let surface = self.surface else { return }
let action = "reset"