Add close tabs on the right action
parent
894e8d91ba
commit
625d7274bf
|
|
@ -714,6 +714,7 @@ typedef struct {
|
|||
typedef enum {
|
||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
|
||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER,
|
||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT,
|
||||
} ghostty_action_close_tab_mode_e;
|
||||
|
||||
// apprt.surface.Message.ChildExited
|
||||
|
|
|
|||
|
|
@ -104,6 +104,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
selector: #selector(onCloseOtherTabs),
|
||||
name: .ghosttyCloseOtherTabs,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onCloseTabsOnTheRight),
|
||||
name: .ghosttyCloseTabsOnTheRight,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onResetWindowSize),
|
||||
|
|
@ -627,6 +632,48 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
}
|
||||
}
|
||||
|
||||
private func closeTabsOnTheRightImmediately() {
|
||||
guard let window = window else { return }
|
||||
guard let tabGroup = window.tabGroup else { return }
|
||||
guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return }
|
||||
|
||||
let tabsToClose = tabGroup.windows.enumerated().filter { $0.offset > currentIndex }
|
||||
guard !tabsToClose.isEmpty else { return }
|
||||
|
||||
if let undoManager {
|
||||
undoManager.beginUndoGrouping()
|
||||
}
|
||||
defer {
|
||||
undoManager?.endUndoGrouping()
|
||||
}
|
||||
|
||||
for (_, candidate) in tabsToClose {
|
||||
if let controller = candidate.windowController as? TerminalController {
|
||||
controller.closeTabImmediately(registerRedo: false)
|
||||
}
|
||||
}
|
||||
|
||||
if let undoManager {
|
||||
undoManager.setActionName("Close Tabs on the Right")
|
||||
|
||||
undoManager.registerUndo(
|
||||
withTarget: self,
|
||||
expiresAfter: undoExpiration
|
||||
) { target in
|
||||
DispatchQueue.main.async {
|
||||
target.window?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
undoManager.registerUndo(
|
||||
withTarget: target,
|
||||
expiresAfter: target.undoExpiration
|
||||
) { target in
|
||||
target.closeTabsOnTheRightImmediately()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the current window (including any other tabs) immediately and without
|
||||
/// confirmation. This will setup proper undo state so the action can be undone.
|
||||
private func closeWindowImmediately() {
|
||||
|
|
@ -1078,24 +1125,24 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
|
||||
// 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."
|
||||
|
|
@ -1104,6 +1151,35 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
}
|
||||
}
|
||||
|
||||
@IBAction func closeTabsOnTheRight(_ sender: Any?) {
|
||||
guard let window = window else { return }
|
||||
guard let tabGroup = window.tabGroup else { return }
|
||||
guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return }
|
||||
|
||||
let tabsToClose = tabGroup.windows.enumerated().filter { $0.offset > currentIndex }
|
||||
guard !tabsToClose.isEmpty else { return }
|
||||
|
||||
let needsConfirm = tabsToClose.contains { (_, candidate) in
|
||||
guard let controller = candidate.windowController as? TerminalController else {
|
||||
return false
|
||||
}
|
||||
|
||||
return controller.surfaceTree.contains(where: { $0.needsConfirmQuit })
|
||||
}
|
||||
|
||||
if !needsConfirm {
|
||||
self.closeTabsOnTheRightImmediately()
|
||||
return
|
||||
}
|
||||
|
||||
confirmClose(
|
||||
messageText: "Close Tabs on the Right?",
|
||||
informativeText: "At least one tab to the right still has a running process. If you close the tab the process will be killed."
|
||||
) {
|
||||
self.closeTabsOnTheRightImmediately()
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func returnToDefaultSize(_ sender: Any?) {
|
||||
guard let window, let defaultSize else { return }
|
||||
defaultSize.apply(to: window)
|
||||
|
|
@ -1305,6 +1381,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
closeOtherTabs(self)
|
||||
}
|
||||
|
||||
@objc private func onCloseTabsOnTheRight(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard surfaceTree.contains(target) else { return }
|
||||
closeTabsOnTheRight(self)
|
||||
}
|
||||
|
||||
@objc private func onCloseWindow(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard surfaceTree.contains(target) else { return }
|
||||
|
|
@ -1367,6 +1449,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
extension TerminalController {
|
||||
override func validateMenuItem(_ item: NSMenuItem) -> Bool {
|
||||
switch item.action {
|
||||
case #selector(closeTabsOnTheRight):
|
||||
guard let window, let tabGroup = window.tabGroup else { return false }
|
||||
guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return false }
|
||||
return tabGroup.windows.enumerated().contains { $0.offset > currentIndex }
|
||||
|
||||
case #selector(returnToDefaultSize):
|
||||
guard let window else { return false }
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ class TerminalWindow: NSWindow {
|
|||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private(set) var derivedConfig: DerivedConfig = .init()
|
||||
|
||||
private var tabMenuObserver: NSObjectProtocol? = nil
|
||||
|
||||
/// Whether this window supports the update accessory. If this is false, then views within this
|
||||
/// window should determine how to show update notifications.
|
||||
|
|
@ -53,6 +55,15 @@ class TerminalWindow: NSWindow {
|
|||
override func awakeFromNib() {
|
||||
// Notify that this terminal window has loaded
|
||||
NotificationCenter.default.post(name: Self.terminalDidAwake, object: self)
|
||||
|
||||
tabMenuObserver = NotificationCenter.default.addObserver(
|
||||
forName: Notification.Name(rawValue: "NSMenuWillOpenNotification"),
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] note in
|
||||
guard let self, let menu = note.object as? NSMenu else { return }
|
||||
self.configureTabContextMenuIfNeeded(menu)
|
||||
}
|
||||
|
||||
// This is required so that window restoration properly creates our tabs
|
||||
// again. I'm not sure why this is required. If you don't do this, then
|
||||
|
|
@ -202,6 +213,8 @@ class TerminalWindow: NSWindow {
|
|||
/// added.
|
||||
static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar")
|
||||
|
||||
private static let closeTabsOnRightMenuItemIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.closeTabsOnTheRightMenuItem")
|
||||
|
||||
func findTitlebarView() -> NSView? {
|
||||
// Find our tab bar. If it doesn't exist we don't do anything.
|
||||
//
|
||||
|
|
@ -277,6 +290,47 @@ class TerminalWindow: NSWindow {
|
|||
}
|
||||
}
|
||||
|
||||
private func configureTabContextMenuIfNeeded(_ menu: NSMenu) {
|
||||
guard isTabContextMenu(menu) else { return }
|
||||
if let existing = menu.items.first(where: { $0.identifier == Self.closeTabsOnRightMenuItemIdentifier }) {
|
||||
menu.removeItem(existing)
|
||||
}
|
||||
guard let terminalController else { return }
|
||||
|
||||
let title = NSLocalizedString("Close Tabs on the Right", comment: "Tab context menu option")
|
||||
let item = NSMenuItem(title: title, action: #selector(TerminalController.closeTabsOnTheRight(_:)), keyEquivalent: "")
|
||||
item.identifier = Self.closeTabsOnRightMenuItemIdentifier
|
||||
item.target = terminalController
|
||||
item.isEnabled = true
|
||||
|
||||
let closeOtherIndex = menu.items.firstIndex(where: { menuItem in
|
||||
guard let action = menuItem.action else { return false }
|
||||
let name = NSStringFromSelector(action).lowercased()
|
||||
return name.contains("close") && name.contains("other") && name.contains("tab")
|
||||
})
|
||||
|
||||
let closeThisIndex = menu.items.firstIndex(where: { menuItem in
|
||||
guard let action = menuItem.action else { return false }
|
||||
let name = NSStringFromSelector(action).lowercased()
|
||||
return name.contains("close") && name.contains("tab")
|
||||
})
|
||||
|
||||
if let idx = closeOtherIndex {
|
||||
menu.insertItem(item, at: idx + 1)
|
||||
} else if let idx = closeThisIndex {
|
||||
menu.insertItem(item, at: idx + 1)
|
||||
} else {
|
||||
menu.addItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
private func isTabContextMenu(_ menu: NSMenu) -> Bool {
|
||||
guard NSApp.keyWindow === self else { return false }
|
||||
let selectorNames = menu.items.compactMap { $0.action }.map { NSStringFromSelector($0).lowercased() }
|
||||
return selectorNames.contains { $0.contains("close") && $0.contains("tab") }
|
||||
}
|
||||
|
||||
|
||||
// MARK: Tab Key Equivalents
|
||||
|
||||
var keyEquivalent: String? = nil {
|
||||
|
|
@ -517,6 +571,12 @@ class TerminalWindow: NSWindow {
|
|||
standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
standardWindowButton(.zoomButton)?.isHidden = true
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let observer = tabMenuObserver {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Config
|
||||
|
||||
|
|
|
|||
|
|
@ -861,6 +861,13 @@ extension Ghostty {
|
|||
)
|
||||
return
|
||||
|
||||
case GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT:
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyCloseTabsOnTheRight,
|
||||
object: surfaceView
|
||||
)
|
||||
return
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -380,6 +380,9 @@ extension Notification.Name {
|
|||
/// Close other tabs
|
||||
static let ghosttyCloseOtherTabs = Notification.Name("com.mitchellh.ghostty.closeOtherTabs")
|
||||
|
||||
/// Close tabs to the right of the focused tab
|
||||
static let ghosttyCloseTabsOnTheRight = Notification.Name("com.mitchellh.ghostty.closeTabsOnTheRight")
|
||||
|
||||
/// Close window
|
||||
static let ghosttyCloseWindow = Notification.Name("com.mitchellh.ghostty.closeWindow")
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ pub fn addPaths(
|
|||
framework: []const u8,
|
||||
system_include: []const u8,
|
||||
library: []const u8,
|
||||
cxx_include: []const u8,
|
||||
}) = .{};
|
||||
};
|
||||
|
||||
|
|
@ -82,11 +83,36 @@ pub fn addPaths(
|
|||
});
|
||||
};
|
||||
|
||||
const cxx_include_path = cxx: {
|
||||
const preferred = try std.fs.path.join(b.allocator, &.{
|
||||
libc.sys_include_dir.?,
|
||||
"c++",
|
||||
"v1",
|
||||
});
|
||||
if (std.fs.accessAbsolute(preferred, .{})) |_| {
|
||||
break :cxx preferred;
|
||||
} else |_| {}
|
||||
|
||||
const sdk_root = std.fs.path.dirname(libc.sys_include_dir.?).?;
|
||||
const fallback = try std.fs.path.join(b.allocator, &.{
|
||||
sdk_root,
|
||||
"include",
|
||||
"c++",
|
||||
"v1",
|
||||
});
|
||||
if (std.fs.accessAbsolute(fallback, .{})) |_| {
|
||||
break :cxx fallback;
|
||||
} else |_| {}
|
||||
|
||||
break :cxx preferred;
|
||||
};
|
||||
|
||||
gop.value_ptr.* = .{
|
||||
.libc = path,
|
||||
.framework = framework_path,
|
||||
.system_include = libc.sys_include_dir.?,
|
||||
.library = library_path,
|
||||
.cxx_include = cxx_include_path,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -107,5 +133,6 @@ pub fn addPaths(
|
|||
// https://github.com/ziglang/zig/issues/24024
|
||||
step.root_module.addSystemFrameworkPath(.{ .cwd_relative = value.framework });
|
||||
step.root_module.addSystemIncludePath(.{ .cwd_relative = value.system_include });
|
||||
step.root_module.addSystemIncludePath(.{ .cwd_relative = value.cxx_include });
|
||||
step.root_module.addLibraryPath(.{ .cwd_relative = value.library });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5299,6 +5299,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
switch (v) {
|
||||
.this => .this,
|
||||
.other => .other,
|
||||
.right => .right,
|
||||
},
|
||||
),
|
||||
|
||||
|
|
|
|||
|
|
@ -767,6 +767,8 @@ pub const CloseTabMode = enum(c_int) {
|
|||
this,
|
||||
/// Close all other tabs.
|
||||
other,
|
||||
/// Close all tabs to the right of the current tab.
|
||||
right,
|
||||
};
|
||||
|
||||
pub const CommandFinished = struct {
|
||||
|
|
|
|||
|
|
@ -347,6 +347,7 @@ pub const Tab = extern struct {
|
|||
switch (mode) {
|
||||
.this => tab_view.closePage(page),
|
||||
.other => tab_view.closeOtherPages(page),
|
||||
.right => tab_view.closePagesAfter(page),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ template $GhosttyWindow: Adw.ApplicationWindow {
|
|||
page-attached => $page_attached();
|
||||
page-detached => $page_detached();
|
||||
create-window => $tab_create_window();
|
||||
menu-model: tab_context_menu;
|
||||
shortcuts: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -192,6 +193,35 @@ menu split_menu {
|
|||
}
|
||||
}
|
||||
|
||||
menu tab_context_menu {
|
||||
section {
|
||||
item {
|
||||
label: _("New Tab");
|
||||
action: "win.new-tab";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("Close Tab");
|
||||
action: "tab.close";
|
||||
target: "this";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Close Other Tabs");
|
||||
action: "tab.close";
|
||||
target: "other";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Close Tabs on the Right");
|
||||
action: "tab.close";
|
||||
target: "right";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
menu main_menu {
|
||||
section {
|
||||
item {
|
||||
|
|
|
|||
|
|
@ -600,9 +600,8 @@ pub const Action = union(enum) {
|
|||
/// of the `confirm-close-surface` configuration setting.
|
||||
close_surface,
|
||||
|
||||
/// Close the current tab and all splits therein _or_ close all tabs and
|
||||
/// splits thein of tabs _other_ than the current tab, depending on the
|
||||
/// mode.
|
||||
/// Close the current tab and all splits therein, close all other tabs, or
|
||||
/// close every tab to the right of the current one depending on the mode.
|
||||
///
|
||||
/// If the mode is not specified, defaults to closing the current tab.
|
||||
///
|
||||
|
|
@ -1005,6 +1004,7 @@ pub const Action = union(enum) {
|
|||
pub const CloseTabMode = enum {
|
||||
this,
|
||||
other,
|
||||
right,
|
||||
|
||||
pub const default: CloseTabMode = .this;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -538,6 +538,11 @@ fn actionCommands(action: Action.Key) []const Command {
|
|||
.title = "Close Other Tabs",
|
||||
.description = "Close all tabs in this window except the current one.",
|
||||
},
|
||||
.{
|
||||
.action = .{ .close_tab = .right },
|
||||
.title = "Close Tabs on the Right",
|
||||
.description = "Close every tab to the right of the current one.",
|
||||
},
|
||||
},
|
||||
|
||||
.close_window => comptime &.{.{
|
||||
|
|
|
|||
Loading…
Reference in New Issue