diff --git a/include/ghostty.h b/include/ghostty.h index 37c844472..88da70b8b 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -680,6 +680,12 @@ typedef struct { uintptr_t len; } ghostty_action_open_url_s; +// apprt.action.CloseTabMode +typedef enum { + GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS, + GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER, +} ghostty_action_close_tab_mode_e; + // apprt.surface.Message.ChildExited typedef struct { uint32_t exit_code; @@ -709,7 +715,6 @@ 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, @@ -787,6 +792,7 @@ typedef union { ghostty_action_reload_config_s reload_config; ghostty_action_config_change_s config_change; ghostty_action_open_url_s open_url; + ghostty_action_close_tab_mode_e close_tab_mode; ghostty_surface_message_childexited_s child_exited; ghostty_terminal_osc_command_progressreport_s progress_report; } ghostty_action_u; diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index e64ed30ee..eaccb43cf 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -455,10 +455,7 @@ extension Ghostty { newSplit(app, target: target, direction: action.action.new_split) case GHOSTTY_ACTION_CLOSE_TAB: - closeTab(app, target: target) - - case GHOSTTY_ACTION_CLOSE_OTHER_TABS: - closeOtherTabs(app, target: target) + closeTab(app, target: target, mode: action.action.close_tab_mode) case GHOSTTY_ACTION_CLOSE_WINDOW: closeWindow(app, target: target) @@ -781,7 +778,7 @@ extension Ghostty { } } - private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s) { + private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s, mode: ghostty_action_close_tab_mode_e) { switch (target.tag) { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("close tabs does nothing with an app target") @@ -791,31 +788,24 @@ extension Ghostty { guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - NotificationCenter.default.post( - name: .ghosttyCloseTab, - object: surfaceView - ) + switch (mode) { + case GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS: + NotificationCenter.default.post( + name: .ghosttyCloseTab, + object: surfaceView + ) + return + case GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER: + NotificationCenter.default.post( + name: .ghosttyCloseOtherTabs, + object: surfaceView + ) + return - default: - assertionFailure() - } - } - - 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() + } default: diff --git a/src/Surface.zig b/src/Surface.zig index 686d214cd..e72322e31 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4701,10 +4701,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), - .close_tab => return try self.rt_app.performAction( + .close_tab => |v| return try self.rt_app.performAction( .{ .surface = self }, .close_tab, - {}, + switch (v) { + .this => .this, + .other => .other, + }, ), inline .previous_tab, @@ -4840,12 +4843,6 @@ 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| { diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 1a7a2a345..a41a4627f 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -83,12 +83,9 @@ pub const Action = union(Key) { /// the tab should be opened in a new window. new_tab, - /// 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, + /// Closes the tab belonging to the currently focused split, or all other + /// tabs, depending on the mode. + close_tab: CloseTabMode, /// Create a new split. The value determines the location of the split /// relative to the target. @@ -304,7 +301,6 @@ pub const Action = union(Key) { new_window, new_tab, close_tab, - close_other_tabs, new_split, close_all_windows, toggle_maximize, @@ -706,3 +702,11 @@ pub const OpenUrl = struct { }; } }; + +/// sync with ghostty_action_close_tab_mode_e in ghostty.h +pub const CloseTabMode = enum(c_int) { + /// Close the current tab. + this, + /// Close all other tabs. + other, +}; diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 8cf61c4ba..984eda15e 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -542,7 +542,7 @@ pub const Application = extern struct { value: apprt.Action.Value(action), ) !bool { switch (action) { - .close_tab => return Action.closeTab(target), + .close_tab => return Action.closeTab(target, value), .close_window => return Action.closeWindow(target), .config_change => try Action.configChange( @@ -625,7 +625,6 @@ pub const Application = extern struct { // Unimplemented .secure_input, .close_all_windows, - .close_other_tabs, .float_window, .toggle_visibility, .cell_size, @@ -873,7 +872,8 @@ pub const Application = extern struct { self.syncActionAccelerator("win.close", .{ .close_window = {} }); self.syncActionAccelerator("win.new-window", .{ .new_window = {} }); self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} }); - self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} }); + self.syncActionAccelerator("win.close-tab::this", .{ .close_tab = .this }); + self.syncActionAccelerator("tab.close::this", .{ .close_tab = .this }); self.syncActionAccelerator("win.split-right", .{ .new_split = .right }); self.syncActionAccelerator("win.split-down", .{ .new_split = .down }); self.syncActionAccelerator("win.split-left", .{ .new_split = .left }); @@ -1577,12 +1577,16 @@ pub const Application = extern struct { /// All apprt action handlers const Action = struct { - pub fn closeTab(target: apprt.Target) bool { + pub fn closeTab(target: apprt.Target, value: apprt.Action.Value(.close_tab)) bool { switch (target) { .app => return false, .surface => |core| { const surface = core.rt_surface.surface; - return surface.as(gtk.Widget).activateAction("tab.close", null) != 0; + return surface.as(gtk.Widget).activateAction( + "tab.close", + glib.ext.VariantType.stringFor([:0]const u8), + @as([*:0]const u8, @tagName(value)), + ) != 0; }, } } diff --git a/src/apprt/gtk-ng/class/command_palette.zig b/src/apprt/gtk-ng/class/command_palette.zig index c2a4ec215..8b7bb328c 100644 --- a/src/apprt/gtk-ng/class/command_palette.zig +++ b/src/apprt/gtk-ng/class/command_palette.zig @@ -156,7 +156,6 @@ pub const CommandPalette = extern struct { // for GTK. switch (command.action) { .close_all_windows, - .close_other_tabs, .toggle_secure_input, .check_for_updates, .redo, diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig index 5f1cf50de..0899d06ee 100644 --- a/src/apprt/gtk-ng/class/tab.zig +++ b/src/apprt/gtk-ng/class/tab.zig @@ -199,8 +199,11 @@ pub const Tab = extern struct { } fn initActionMap(self: *Self) void { + const s_param_type = glib.ext.VariantType.newFor([:0]const u8); + defer s_param_type.free(); + const actions = [_]ext.actions.Action(Self){ - .init("close", actionClose, null), + .init("close", actionClose, s_param_type), .init("ring-bell", actionRingBell, null), }; @@ -314,18 +317,48 @@ pub const Tab = extern struct { fn actionClose( _: *gio.SimpleAction, - _: ?*glib.Variant, + param_: ?*glib.Variant, self: *Self, ) callconv(.c) void { + const param = param_ orelse { + log.warn("tab.close-tab called without a parameter", .{}); + return; + }; + + var str: ?[*:0]const u8 = null; + param.get("&s", &str); + const tab_view = ext.getAncestor( adw.TabView, self.as(gtk.Widget), ) orelse return; + const page = tab_view.getPage(self.as(gtk.Widget)); + const mode = std.meta.stringToEnum( + apprt.action.CloseTabMode, + std.mem.span( + str orelse { + log.warn("invalid mode provided to tab.close-tab", .{}); + return; + }, + ), + ) orelse { + // Need to be defensive here since actions can be triggered externally. + log.warn("invalid mode provided to tab.close-tab: {s}", .{str.?}); + return; + }; + // Delegate to our parent to handle this, since this will emit // a close-page signal that the parent can intercept. - tab_view.closePage(page); + switch (mode) { + .this => { + tab_view.closePage(page); + }, + .other => { + log.warn("close-tab:other is not implemented", .{}); + }, + } } fn actionRingBell( diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 117fac540..862455fc8 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -320,10 +320,13 @@ pub const Window = extern struct { /// Setup our action map. fn initActionMap(self: *Self) void { + const s_variant_type = glib.ext.VariantType.newFor([:0]const u8); + defer s_variant_type.free(); + const actions = [_]ext.actions.Action(Self){ .init("about", actionAbout, null), .init("close", actionClose, null), - .init("close-tab", actionCloseTab, null), + .init("close-tab", actionCloseTab, s_variant_type), .init("new-tab", actionNewTab, null), .init("new-window", actionNewWindow, null), .init("ring-bell", actionRingBell, null), @@ -1679,10 +1682,31 @@ pub const Window = extern struct { fn actionCloseTab( _: *gio.SimpleAction, - _: ?*glib.Variant, + param_: ?*glib.Variant, self: *Window, ) callconv(.c) void { - self.performBindingAction(.close_tab); + const param = param_ orelse { + log.warn("win.close-tab called without a parameter", .{}); + return; + }; + + var str: ?[*:0]const u8 = null; + param.get("&s", &str); + + const mode = std.meta.stringToEnum( + input.Binding.Action.CloseTabMode, + std.mem.span( + str orelse { + log.warn("invalid mode provided to win.close-tab", .{}); + return; + }, + ), + ) orelse { + log.warn("invalid mode provided to win.close-tab: {s}", .{str.?}); + return; + }; + + self.performBindingAction(.{ .close_tab = mode }); } fn actionNewWindow( diff --git a/src/apprt/gtk-ng/ui/1.2/surface.blp b/src/apprt/gtk-ng/ui/1.2/surface.blp index 6c027e735..9989e9c10 100644 --- a/src/apprt/gtk-ng/ui/1.2/surface.blp +++ b/src/apprt/gtk-ng/ui/1.2/surface.blp @@ -228,7 +228,8 @@ menu context_menu_model { item { label: _("Close Tab"); - action: "win.close-tab"; + action: "tab.close"; + target: "this"; } } diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index a63da6792..8c0a7bedb 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -226,6 +226,7 @@ menu main_menu { item { label: _("Close Tab"); action: "win.close-tab"; + target: "this"; } } diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 70c03e098..ee5f3eb96 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -491,7 +491,7 @@ pub fn performAction( .toggle_maximize => self.toggleMaximize(target), .toggle_fullscreen => self.toggleFullscreen(target, value), .new_tab => try self.newTab(target), - .close_tab => return try self.closeTab(target), + .close_tab => return try self.closeTab(target, value), .goto_tab => return self.gotoTab(target, value), .move_tab => self.moveTab(target, value), .new_split => try self.newSplit(target, value), @@ -528,7 +528,6 @@ pub fn performAction( // Unimplemented .close_all_windows, - .close_other_tabs, .float_window, .toggle_visibility, .cell_size, @@ -586,7 +585,7 @@ fn newTab(_: *App, target: apprt.Target) !void { } } -fn closeTab(_: *App, target: apprt.Target) !bool { +fn closeTab(_: *App, target: apprt.Target, value: apprt.Action.Value(.close_tab)) !bool { switch (target) { .app => return false, .surface => |v| { @@ -598,8 +597,16 @@ fn closeTab(_: *App, target: apprt.Target) !bool { return false; }; - tab.closeWithConfirmation(); - return true; + switch (value) { + .this => { + tab.closeWithConfirmation(); + return true; + }, + .other => { + log.warn("close-tab:other is not implemented", .{}); + return false; + }, + } }, } } @@ -1146,7 +1153,7 @@ fn syncActionAccelerators(self: *App) !void { try self.syncActionAccelerator("win.close", .{ .close_window = {} }); try self.syncActionAccelerator("win.new-window", .{ .new_window = {} }); try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} }); - try self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} }); + try self.syncActionAccelerator("win.close-tab", .{ .close_tab = .this }); try self.syncActionAccelerator("win.split-right", .{ .new_split = .right }); try self.syncActionAccelerator("win.split-down", .{ .new_split = .down }); try self.syncActionAccelerator("win.split-left", .{ .new_split = .left }); diff --git a/src/apprt/gtk/CommandPalette.zig b/src/apprt/gtk/CommandPalette.zig index e0ff8c177..076459dbd 100644 --- a/src/apprt/gtk/CommandPalette.zig +++ b/src/apprt/gtk/CommandPalette.zig @@ -108,7 +108,6 @@ 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, diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index d3408e867..8c02396a6 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -1076,7 +1076,7 @@ fn gtkActionCloseTab( _: ?*glib.Variant, self: *Window, ) callconv(.c) void { - self.performBindingAction(.{ .close_tab = {} }); + self.performBindingAction(.{ .close_tab = .this }); } fn gtkActionSplitRight( diff --git a/src/config/Config.zig b/src/config/Config.zig index 637cfee49..b82824a93 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -5598,7 +5598,7 @@ pub const Keybinds = struct { try self.set.put( alloc, .{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } }, - .{ .close_tab = {} }, + .{ .close_tab = .this }, ); try self.set.putFlags( alloc, @@ -5904,7 +5904,7 @@ pub const Keybinds = struct { try self.set.put( alloc, .{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true, .alt = true } }, - .{ .close_tab = {} }, + .{ .close_tab = .this }, ); try self.set.put( alloc, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 6db0decc2..6d1050859 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -552,17 +552,17 @@ pub const Action = union(enum) { /// of the `confirm-close-surface` configuration setting. close_surface, - /// Close the current tab and all splits therein. + /// 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. + /// + /// If the mode is not specified, defaults to closing the current tab. + /// + /// close-tab:other is only available on macOS. /// /// This might trigger a close confirmation popup, depending on the value /// 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_tab: CloseTabMode, /// Close the current window and all tabs and splits therein. /// @@ -864,6 +864,13 @@ pub const Action = union(enum) { hide, }; + pub const CloseTabMode = enum { + this, + other, + + pub const default: CloseTabMode = .this; + }; + fn parseEnum(comptime T: type, value: []const u8) !T { return std.meta.stringToEnum(T, value) orelse return Error.InvalidFormat; } @@ -1058,7 +1065,6 @@ pub const Action = union(enum) { .write_selection_file, .close_surface, .close_tab, - .close_other_tabs, .close_window, .toggle_maximize, .toggle_fullscreen, diff --git a/src/input/command.zig b/src/input/command.zig index 68652cce3..f6c29040a 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -375,12 +375,6 @@ 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", @@ -399,11 +393,27 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Close the current terminal.", }}, - .close_tab => comptime &.{.{ - .action = .close_tab, - .title = "Close Tab", - .description = "Close the current tab.", - }}, + .close_tab => comptime if (builtin.target.os.tag.isDarwin()) + &.{ + .{ + .action = .{ .close_tab = .this }, + .title = "Close Tab", + .description = "Close the current tab.", + }, + .{ + .action = .{ .close_tab = .other }, + .title = "Close Other Tabs", + .description = "Close all tabs in this window except the current one.", + }, + } + else + &.{ + .{ + .action = .{ .close_tab = .this }, + .title = "Close Tab", + .description = "Close the current tab.", + }, + }, .close_window => comptime &.{.{ .action = .close_window,