parameterize close_tab

- Add mode (`this`/`other`) parameter to `close_tab` keybind/apprt action.
- Keybinds will default to `this` if not specified, eliminating backward
  compatibility issues (`keybind=x=close_tab` === `keybind=x=close_tab:this`).
- Remove `close_other_tabs` keybind and apprt action.
This commit is contained in:
Jeffrey C. Ollie
2025-08-25 11:00:26 -05:00
parent 8aa0b4c92a
commit 52a25e9c69
16 changed files with 168 additions and 87 deletions

View File

@@ -680,6 +680,12 @@ typedef struct {
uintptr_t len; uintptr_t len;
} ghostty_action_open_url_s; } 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 // apprt.surface.Message.ChildExited
typedef struct { typedef struct {
uint32_t exit_code; uint32_t exit_code;
@@ -709,7 +715,6 @@ typedef enum {
GHOSTTY_ACTION_NEW_WINDOW, GHOSTTY_ACTION_NEW_WINDOW,
GHOSTTY_ACTION_NEW_TAB, GHOSTTY_ACTION_NEW_TAB,
GHOSTTY_ACTION_CLOSE_TAB, GHOSTTY_ACTION_CLOSE_TAB,
GHOSTTY_ACTION_CLOSE_OTHER_TABS,
GHOSTTY_ACTION_NEW_SPLIT, GHOSTTY_ACTION_NEW_SPLIT,
GHOSTTY_ACTION_CLOSE_ALL_WINDOWS, GHOSTTY_ACTION_CLOSE_ALL_WINDOWS,
GHOSTTY_ACTION_TOGGLE_MAXIMIZE, GHOSTTY_ACTION_TOGGLE_MAXIMIZE,
@@ -787,6 +792,7 @@ typedef union {
ghostty_action_reload_config_s reload_config; ghostty_action_reload_config_s reload_config;
ghostty_action_config_change_s config_change; ghostty_action_config_change_s config_change;
ghostty_action_open_url_s open_url; ghostty_action_open_url_s open_url;
ghostty_action_close_tab_mode_e close_tab_mode;
ghostty_surface_message_childexited_s child_exited; ghostty_surface_message_childexited_s child_exited;
ghostty_terminal_osc_command_progressreport_s progress_report; ghostty_terminal_osc_command_progressreport_s progress_report;
} ghostty_action_u; } ghostty_action_u;

View File

@@ -455,10 +455,7 @@ extension Ghostty {
newSplit(app, target: target, direction: action.action.new_split) newSplit(app, target: target, direction: action.action.new_split)
case GHOSTTY_ACTION_CLOSE_TAB: case GHOSTTY_ACTION_CLOSE_TAB:
closeTab(app, target: target) closeTab(app, target: target, mode: action.action.close_tab_mode)
case GHOSTTY_ACTION_CLOSE_OTHER_TABS:
closeOtherTabs(app, target: target)
case GHOSTTY_ACTION_CLOSE_WINDOW: case GHOSTTY_ACTION_CLOSE_WINDOW:
closeWindow(app, target: target) 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) { switch (target.tag) {
case GHOSTTY_TARGET_APP: case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("close tabs does nothing with an app target") 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 surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return } guard let surfaceView = self.surfaceView(from: surface) else { return }
switch (mode) {
case GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS:
NotificationCenter.default.post( NotificationCenter.default.post(
name: .ghosttyCloseTab, name: .ghosttyCloseTab,
object: surfaceView object: surfaceView
) )
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 return
case GHOSTTY_TARGET_SURFACE: case GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
NotificationCenter.default.post( NotificationCenter.default.post(
name: .ghosttyCloseOtherTabs, name: .ghosttyCloseOtherTabs,
object: surfaceView object: surfaceView
) )
return
default:
assertionFailure()
}
default: default:

View File

@@ -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 }, .{ .surface = self },
.close_tab, .close_tab,
{}, switch (v) {
.this => .this,
.other => .other,
},
), ),
inline .previous_tab, 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 => { .select_all => {
const sel = self.io.terminal.screen.selectAll(); const sel = self.io.terminal.screen.selectAll();
if (sel) |s| { if (sel) |s| {

View File

@@ -83,12 +83,9 @@ pub const Action = union(Key) {
/// the tab should be opened in a new window. /// the tab should be opened in a new window.
new_tab, new_tab,
/// Closes the tab belonging to the currently focused split. /// Closes the tab belonging to the currently focused split, or all other
close_tab, /// tabs, depending on the mode.
close_tab: CloseTabMode,
/// 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 /// Create a new split. The value determines the location of the split
/// relative to the target. /// relative to the target.
@@ -304,7 +301,6 @@ pub const Action = union(Key) {
new_window, new_window,
new_tab, new_tab,
close_tab, close_tab,
close_other_tabs,
new_split, new_split,
close_all_windows, close_all_windows,
toggle_maximize, 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,
};

View File

@@ -542,7 +542,7 @@ pub const Application = extern struct {
value: apprt.Action.Value(action), value: apprt.Action.Value(action),
) !bool { ) !bool {
switch (action) { switch (action) {
.close_tab => return Action.closeTab(target), .close_tab => return Action.closeTab(target, value),
.close_window => return Action.closeWindow(target), .close_window => return Action.closeWindow(target),
.config_change => try Action.configChange( .config_change => try Action.configChange(
@@ -625,7 +625,6 @@ pub const Application = extern struct {
// Unimplemented // Unimplemented
.secure_input, .secure_input,
.close_all_windows, .close_all_windows,
.close_other_tabs,
.float_window, .float_window,
.toggle_visibility, .toggle_visibility,
.cell_size, .cell_size,
@@ -873,7 +872,8 @@ pub const Application = extern struct {
self.syncActionAccelerator("win.close", .{ .close_window = {} }); self.syncActionAccelerator("win.close", .{ .close_window = {} });
self.syncActionAccelerator("win.new-window", .{ .new_window = {} }); self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} }); 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-right", .{ .new_split = .right });
self.syncActionAccelerator("win.split-down", .{ .new_split = .down }); self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
self.syncActionAccelerator("win.split-left", .{ .new_split = .left }); self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
@@ -1577,12 +1577,16 @@ pub const Application = extern struct {
/// All apprt action handlers /// All apprt action handlers
const Action = struct { 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) { switch (target) {
.app => return false, .app => return false,
.surface => |core| { .surface => |core| {
const surface = core.rt_surface.surface; 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;
}, },
} }
} }

View File

@@ -156,7 +156,6 @@ pub const CommandPalette = extern struct {
// for GTK. // for GTK.
switch (command.action) { switch (command.action) {
.close_all_windows, .close_all_windows,
.close_other_tabs,
.toggle_secure_input, .toggle_secure_input,
.check_for_updates, .check_for_updates,
.redo, .redo,

View File

@@ -199,8 +199,11 @@ pub const Tab = extern struct {
} }
fn initActionMap(self: *Self) void { 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){ const actions = [_]ext.actions.Action(Self){
.init("close", actionClose, null), .init("close", actionClose, s_param_type),
.init("ring-bell", actionRingBell, null), .init("ring-bell", actionRingBell, null),
}; };
@@ -314,18 +317,48 @@ pub const Tab = extern struct {
fn actionClose( fn actionClose(
_: *gio.SimpleAction, _: *gio.SimpleAction,
_: ?*glib.Variant, param_: ?*glib.Variant,
self: *Self, self: *Self,
) callconv(.c) void { ) 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( const tab_view = ext.getAncestor(
adw.TabView, adw.TabView,
self.as(gtk.Widget), self.as(gtk.Widget),
) orelse return; ) orelse return;
const page = tab_view.getPage(self.as(gtk.Widget)); 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 // Delegate to our parent to handle this, since this will emit
// a close-page signal that the parent can intercept. // a close-page signal that the parent can intercept.
switch (mode) {
.this => {
tab_view.closePage(page); tab_view.closePage(page);
},
.other => {
log.warn("close-tab:other is not implemented", .{});
},
}
} }
fn actionRingBell( fn actionRingBell(

View File

@@ -320,10 +320,13 @@ pub const Window = extern struct {
/// Setup our action map. /// Setup our action map.
fn initActionMap(self: *Self) void { 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){ const actions = [_]ext.actions.Action(Self){
.init("about", actionAbout, null), .init("about", actionAbout, null),
.init("close", actionClose, null), .init("close", actionClose, null),
.init("close-tab", actionCloseTab, null), .init("close-tab", actionCloseTab, s_variant_type),
.init("new-tab", actionNewTab, null), .init("new-tab", actionNewTab, null),
.init("new-window", actionNewWindow, null), .init("new-window", actionNewWindow, null),
.init("ring-bell", actionRingBell, null), .init("ring-bell", actionRingBell, null),
@@ -1679,10 +1682,31 @@ pub const Window = extern struct {
fn actionCloseTab( fn actionCloseTab(
_: *gio.SimpleAction, _: *gio.SimpleAction,
_: ?*glib.Variant, param_: ?*glib.Variant,
self: *Window, self: *Window,
) callconv(.c) void { ) 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( fn actionNewWindow(

View File

@@ -228,7 +228,8 @@ menu context_menu_model {
item { item {
label: _("Close Tab"); label: _("Close Tab");
action: "win.close-tab"; action: "tab.close";
target: "this";
} }
} }

View File

@@ -226,6 +226,7 @@ menu main_menu {
item { item {
label: _("Close Tab"); label: _("Close Tab");
action: "win.close-tab"; action: "win.close-tab";
target: "this";
} }
} }

View File

@@ -491,7 +491,7 @@ pub fn performAction(
.toggle_maximize => self.toggleMaximize(target), .toggle_maximize => self.toggleMaximize(target),
.toggle_fullscreen => self.toggleFullscreen(target, value), .toggle_fullscreen => self.toggleFullscreen(target, value),
.new_tab => try self.newTab(target), .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), .goto_tab => return self.gotoTab(target, value),
.move_tab => self.moveTab(target, value), .move_tab => self.moveTab(target, value),
.new_split => try self.newSplit(target, value), .new_split => try self.newSplit(target, value),
@@ -528,7 +528,6 @@ pub fn performAction(
// Unimplemented // Unimplemented
.close_all_windows, .close_all_windows,
.close_other_tabs,
.float_window, .float_window,
.toggle_visibility, .toggle_visibility,
.cell_size, .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) { switch (target) {
.app => return false, .app => return false,
.surface => |v| { .surface => |v| {
@@ -598,9 +597,17 @@ fn closeTab(_: *App, target: apprt.Target) !bool {
return false; return false;
}; };
switch (value) {
.this => {
tab.closeWithConfirmation(); tab.closeWithConfirmation();
return true; 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.close", .{ .close_window = {} });
try self.syncActionAccelerator("win.new-window", .{ .new_window = {} }); try self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} }); 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-right", .{ .new_split = .right });
try self.syncActionAccelerator("win.split-down", .{ .new_split = .down }); try self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
try self.syncActionAccelerator("win.split-left", .{ .new_split = .left }); try self.syncActionAccelerator("win.split-left", .{ .new_split = .left });

View File

@@ -108,7 +108,6 @@ pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !voi
// or don't make sense for GTK // or don't make sense for GTK
switch (command.action) { switch (command.action) {
.close_all_windows, .close_all_windows,
.close_other_tabs,
.toggle_secure_input, .toggle_secure_input,
.check_for_updates, .check_for_updates,
.redo, .redo,

View File

@@ -1076,7 +1076,7 @@ fn gtkActionCloseTab(
_: ?*glib.Variant, _: ?*glib.Variant,
self: *Window, self: *Window,
) callconv(.c) void { ) callconv(.c) void {
self.performBindingAction(.{ .close_tab = {} }); self.performBindingAction(.{ .close_tab = .this });
} }
fn gtkActionSplitRight( fn gtkActionSplitRight(

View File

@@ -5598,7 +5598,7 @@ pub const Keybinds = struct {
try self.set.put( try self.set.put(
alloc, alloc,
.{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } },
.{ .close_tab = {} }, .{ .close_tab = .this },
); );
try self.set.putFlags( try self.set.putFlags(
alloc, alloc,
@@ -5904,7 +5904,7 @@ pub const Keybinds = struct {
try self.set.put( try self.set.put(
alloc, alloc,
.{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true, .alt = true } }, .{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true, .alt = true } },
.{ .close_tab = {} }, .{ .close_tab = .this },
); );
try self.set.put( try self.set.put(
alloc, alloc,

View File

@@ -552,17 +552,17 @@ pub const Action = union(enum) {
/// of the `confirm-close-surface` configuration setting. /// of the `confirm-close-surface` configuration setting.
close_surface, 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 /// This might trigger a close confirmation popup, depending on the value
/// of the `confirm-close-surface` configuration setting. /// of the `confirm-close-surface` configuration setting.
close_tab, close_tab: CloseTabMode,
/// 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. /// Close the current window and all tabs and splits therein.
/// ///
@@ -864,6 +864,13 @@ pub const Action = union(enum) {
hide, hide,
}; };
pub const CloseTabMode = enum {
this,
other,
pub const default: CloseTabMode = .this;
};
fn parseEnum(comptime T: type, value: []const u8) !T { fn parseEnum(comptime T: type, value: []const u8) !T {
return std.meta.stringToEnum(T, value) orelse return Error.InvalidFormat; return std.meta.stringToEnum(T, value) orelse return Error.InvalidFormat;
} }
@@ -1058,7 +1065,6 @@ pub const Action = union(enum) {
.write_selection_file, .write_selection_file,
.close_surface, .close_surface,
.close_tab, .close_tab,
.close_other_tabs,
.close_window, .close_window,
.toggle_maximize, .toggle_maximize,
.toggle_fullscreen, .toggle_fullscreen,

View File

@@ -375,12 +375,6 @@ fn actionCommands(action: Action.Key) []const Command {
.description = "Show the on-screen keyboard if present.", .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 &.{.{ .open_config => comptime &.{.{
.action = .open_config, .action = .open_config,
.title = "Open Config", .title = "Open Config",
@@ -399,11 +393,27 @@ fn actionCommands(action: Action.Key) []const Command {
.description = "Close the current terminal.", .description = "Close the current terminal.",
}}, }},
.close_tab => comptime &.{.{ .close_tab => comptime if (builtin.target.os.tag.isDarwin())
.action = .close_tab, &.{
.{
.action = .{ .close_tab = .this },
.title = "Close Tab", .title = "Close Tab",
.description = "Close the current 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 &.{.{ .close_window => comptime &.{.{
.action = .close_window, .action = .close_window,