mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-05 19:08:17 +00:00
Close other tabs feature on Mac. (#8363)
Supporting command line, file menu and keybindings. Default mac shortcut of `super + alt + o` (other) Not able to test on Linux so excluding `close_other_tabs` from `gtk` for now
This commit is contained in:
@@ -709,6 +709,7 @@ typedef enum {
|
||||
GHOSTTY_ACTION_NEW_WINDOW,
|
||||
GHOSTTY_ACTION_NEW_TAB,
|
||||
GHOSTTY_ACTION_CLOSE_TAB,
|
||||
GHOSTTY_ACTION_CLOSE_OTHER_TABS,
|
||||
GHOSTTY_ACTION_NEW_SPLIT,
|
||||
GHOSTTY_ACTION_CLOSE_ALL_WINDOWS,
|
||||
GHOSTTY_ACTION_TOGGLE_MAXIMIZE,
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24123.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24123.1"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
|
@@ -95,6 +95,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
selector: #selector(onCloseTab),
|
||||
name: .ghosttyCloseTab,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onCloseOtherTabs),
|
||||
name: .ghosttyCloseOtherTabs,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onResetWindowSize),
|
||||
@@ -559,7 +564,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
closeWindow(nil)
|
||||
}
|
||||
|
||||
private func closeTabImmediately() {
|
||||
private func closeTabImmediately(registerRedo: Bool = true) {
|
||||
guard let window = window else { return }
|
||||
guard let tabGroup = window.tabGroup,
|
||||
tabGroup.windows.count > 1 else {
|
||||
@@ -576,19 +581,69 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
expiresAfter: undoExpiration
|
||||
) { ghostty in
|
||||
let newController = TerminalController(ghostty, with: undoState)
|
||||
|
||||
// Register redo action
|
||||
undoManager.registerUndo(
|
||||
withTarget: newController,
|
||||
expiresAfter: newController.undoExpiration
|
||||
) { target in
|
||||
target.closeTabImmediately()
|
||||
|
||||
if registerRedo {
|
||||
undoManager.registerUndo(
|
||||
withTarget: newController,
|
||||
expiresAfter: newController.undoExpiration
|
||||
) { target in
|
||||
target.closeTabImmediately()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.close()
|
||||
}
|
||||
|
||||
private func closeOtherTabsImmediately() {
|
||||
guard let window = window else { return }
|
||||
guard let tabGroup = window.tabGroup else { return }
|
||||
guard tabGroup.windows.count > 1 else { return }
|
||||
|
||||
// Start an undo grouping
|
||||
if let undoManager {
|
||||
undoManager.beginUndoGrouping()
|
||||
}
|
||||
defer {
|
||||
undoManager?.endUndoGrouping()
|
||||
}
|
||||
|
||||
// Iterate through all tabs except the current one.
|
||||
for window in tabGroup.windows where window != self.window {
|
||||
// We ignore any non-terminal tabs. They don't currently exist and we can't
|
||||
// properly undo them anyways so I'd rather ignore them and get a bug report
|
||||
// later if and when we introduce non-terminal tabs.
|
||||
if let controller = window.windowController as? TerminalController {
|
||||
// We must not register a redo, because it messes with our own redo
|
||||
// that we register later.
|
||||
controller.closeTabImmediately(registerRedo: false)
|
||||
}
|
||||
}
|
||||
|
||||
if let undoManager {
|
||||
undoManager.setActionName("Close Other Tabs")
|
||||
|
||||
// We need to register an undo that refocuses this window. Otherwise, the
|
||||
// undo operation above for each tab will steal focus.
|
||||
undoManager.registerUndo(
|
||||
withTarget: self,
|
||||
expiresAfter: undoExpiration
|
||||
) { target in
|
||||
DispatchQueue.main.async {
|
||||
target.window?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
// Register redo action
|
||||
undoManager.registerUndo(
|
||||
withTarget: target,
|
||||
expiresAfter: target.undoExpiration
|
||||
) { target in
|
||||
target.closeOtherTabsImmediately()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the current window (including any other tabs) immediately and without
|
||||
/// confirmation. This will setup proper undo state so the action can be undone.
|
||||
@@ -1023,6 +1078,38 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func closeOtherTabs(_ sender: Any?) {
|
||||
guard let window = window else { return }
|
||||
guard let tabGroup = window.tabGroup else { return }
|
||||
|
||||
// If we only have one window then we have no other tabs to close
|
||||
guard tabGroup.windows.count > 1 else { return }
|
||||
|
||||
// Check if we have to confirm close.
|
||||
guard tabGroup.windows.contains(where: { window in
|
||||
// Ignore ourself
|
||||
if window == self.window { return false }
|
||||
|
||||
// Ignore non-terminals
|
||||
guard let controller = window.windowController as? TerminalController else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if any surfaces require confirmation
|
||||
return controller.surfaceTree.contains(where: { $0.needsConfirmQuit })
|
||||
}) else {
|
||||
self.closeOtherTabsImmediately()
|
||||
return
|
||||
}
|
||||
|
||||
confirmClose(
|
||||
messageText: "Close Other Tabs?",
|
||||
informativeText: "At least one other tab still has a running process. If you close the tab the process will be killed."
|
||||
) {
|
||||
self.closeOtherTabsImmediately()
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func returnToDefaultSize(_ sender: Any?) {
|
||||
guard let defaultSize else { return }
|
||||
window?.setFrame(defaultSize, display: true)
|
||||
@@ -1206,6 +1293,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
closeTab(self)
|
||||
}
|
||||
|
||||
@objc private func onCloseOtherTabs(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard surfaceTree.contains(target) else { return }
|
||||
closeOtherTabs(self)
|
||||
}
|
||||
|
||||
@objc private func onCloseWindow(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard surfaceTree.contains(target) else { return }
|
||||
|
@@ -457,6 +457,9 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_CLOSE_TAB:
|
||||
closeTab(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_CLOSE_OTHER_TABS:
|
||||
closeOtherTabs(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_CLOSE_WINDOW:
|
||||
closeWindow(app, target: target)
|
||||
|
||||
@@ -781,7 +784,7 @@ extension Ghostty {
|
||||
private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("close tab does nothing with an app target")
|
||||
Ghostty.logger.warning("close tabs does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
@@ -799,6 +802,27 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private static func closeOtherTabs(_ app: ghostty_app_t, target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("close other tabs does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyCloseOtherTabs,
|
||||
object: surfaceView
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func closeWindow(_ app: ghostty_app_t, target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
|
@@ -329,6 +329,9 @@ extension Notification.Name {
|
||||
/// Close tab
|
||||
static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab")
|
||||
|
||||
/// Close other tabs
|
||||
static let ghosttyCloseOtherTabs = Notification.Name("com.mitchellh.ghostty.closeOtherTabs")
|
||||
|
||||
/// Close window
|
||||
static let ghosttyCloseWindow = Notification.Name("com.mitchellh.ghostty.closeWindow")
|
||||
|
||||
|
@@ -4840,6 +4840,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
{},
|
||||
),
|
||||
|
||||
.close_other_tabs => return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.close_other_tabs,
|
||||
{},
|
||||
),
|
||||
|
||||
.select_all => {
|
||||
const sel = self.io.terminal.screen.selectAll();
|
||||
if (sel) |s| {
|
||||
|
@@ -86,6 +86,10 @@ pub const Action = union(Key) {
|
||||
/// Closes the tab belonging to the currently focused split.
|
||||
close_tab,
|
||||
|
||||
/// Closes all tabs in the current window other than the currently
|
||||
/// focused tab.
|
||||
close_other_tabs,
|
||||
|
||||
/// Create a new split. The value determines the location of the split
|
||||
/// relative to the target.
|
||||
new_split: SplitDirection,
|
||||
@@ -300,6 +304,7 @@ pub const Action = union(Key) {
|
||||
new_window,
|
||||
new_tab,
|
||||
close_tab,
|
||||
close_other_tabs,
|
||||
new_split,
|
||||
close_all_windows,
|
||||
toggle_maximize,
|
||||
|
@@ -625,6 +625,7 @@ pub const Application = extern struct {
|
||||
// Unimplemented
|
||||
.secure_input,
|
||||
.close_all_windows,
|
||||
.close_other_tabs,
|
||||
.float_window,
|
||||
.toggle_visibility,
|
||||
.cell_size,
|
||||
|
@@ -156,6 +156,7 @@ pub const CommandPalette = extern struct {
|
||||
// for GTK.
|
||||
switch (command.action) {
|
||||
.close_all_windows,
|
||||
.close_other_tabs,
|
||||
.toggle_secure_input,
|
||||
.check_for_updates,
|
||||
.redo,
|
||||
|
@@ -528,6 +528,7 @@ pub fn performAction(
|
||||
|
||||
// Unimplemented
|
||||
.close_all_windows,
|
||||
.close_other_tabs,
|
||||
.float_window,
|
||||
.toggle_visibility,
|
||||
.cell_size,
|
||||
|
@@ -108,6 +108,7 @@ pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !voi
|
||||
// or don't make sense for GTK
|
||||
switch (command.action) {
|
||||
.close_all_windows,
|
||||
.close_other_tabs,
|
||||
.toggle_secure_input,
|
||||
.check_for_updates,
|
||||
.redo,
|
||||
|
@@ -558,6 +558,12 @@ pub const Action = union(enum) {
|
||||
/// of the `confirm-close-surface` configuration setting.
|
||||
close_tab,
|
||||
|
||||
/// Close all tabs other than the currently focused one within the same
|
||||
/// window.
|
||||
///
|
||||
/// Only available on macOS currently.
|
||||
close_other_tabs,
|
||||
|
||||
/// Close the current window and all tabs and splits therein.
|
||||
///
|
||||
/// This might trigger a close confirmation popup, depending on the value
|
||||
@@ -1052,6 +1058,7 @@ pub const Action = union(enum) {
|
||||
.write_selection_file,
|
||||
.close_surface,
|
||||
.close_tab,
|
||||
.close_other_tabs,
|
||||
.close_window,
|
||||
.toggle_maximize,
|
||||
.toggle_fullscreen,
|
||||
|
@@ -375,6 +375,12 @@ fn actionCommands(action: Action.Key) []const Command {
|
||||
.description = "Show the on-screen keyboard if present.",
|
||||
}},
|
||||
|
||||
.close_other_tabs => comptime &.{.{
|
||||
.action = .close_other_tabs,
|
||||
.title = "Close Other Tabs",
|
||||
.description = "Close all tabs in this window except the current one.",
|
||||
}},
|
||||
|
||||
.open_config => comptime &.{.{
|
||||
.action = .open_config,
|
||||
.title = "Open Config",
|
||||
|
Reference in New Issue
Block a user