diff --git a/include/ghostty.h b/include/ghostty.h index b56b8827e..b413dec41 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -143,6 +143,7 @@ typedef enum { typedef enum { GHOSTTY_TAB_PREVIOUS = -1, GHOSTTY_TAB_NEXT = -2, + GHOSTTY_TAB_LAST = -3, } ghostty_tab_e; typedef enum { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 0d5aa5a9f..5f14eed88 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -725,6 +725,8 @@ class TerminalController: NSWindowController, NSWindowDelegate, } else { finalIndex = selectedIndex + 1 } + } else if (tabIndex == GHOSTTY_TAB_LAST.rawValue) { + finalIndex = tabbedWindows.count - 1 } else { return } diff --git a/src/Surface.zig b/src/Surface.zig index e2972b858..a0f668e6e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3559,9 +3559,9 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool } } - if (@hasDecl(apprt.Surface, "gotoPreviousTab")) { - self.rt_surface.gotoPreviousTab(); - } else log.warn("runtime doesn't implement gotoPreviousTab", .{}); + if (@hasDecl(apprt.Surface, "gotoTab")) { + self.rt_surface.gotoTab(.previous); + } else log.warn("runtime doesn't implement gotoTab", .{}); }, .next_tab => { @@ -3572,14 +3572,27 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool } } - if (@hasDecl(apprt.Surface, "gotoNextTab")) { - self.rt_surface.gotoNextTab(); - } else log.warn("runtime doesn't implement gotoNextTab", .{}); + if (@hasDecl(apprt.Surface, "gotoTab")) { + self.rt_surface.gotoTab(.next); + } else log.warn("runtime doesn't implement gotoTab", .{}); + }, + + .last_tab => { + if (@hasDecl(apprt.Surface, "hasTabs")) { + if (!self.rt_surface.hasTabs()) { + log.debug("surface has no tabs, ignoring last_tab binding", .{}); + return false; + } + } + + if (@hasDecl(apprt.Surface, "gotoTab")) { + self.rt_surface.gotoTab(.last); + } else log.warn("runtime doesn't implement gotoTab", .{}); }, .goto_tab => |n| { if (@hasDecl(apprt.Surface, "gotoTab")) { - self.rt_surface.gotoTab(n); + self.rt_surface.gotoTab(@enumFromInt(n)); } else log.warn("runtime doesn't implement gotoTab", .{}); }, diff --git a/src/apprt.zig b/src/apprt.zig index 767fc57e6..491f1b8b5 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -28,6 +28,7 @@ pub const ClipboardRequestType = structs.ClipboardRequestType; pub const ColorScheme = structs.ColorScheme; pub const CursorPos = structs.CursorPos; pub const DesktopNotification = structs.DesktopNotification; +pub const GotoTab = structs.GotoTab; pub const IMEPos = structs.IMEPos; pub const Selection = structs.Selection; pub const SplitDirection = structs.SplitDirection; diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 0115c0ca5..9127bb5bd 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -108,7 +108,7 @@ pub const App = struct { toggle_split_zoom: ?*const fn (SurfaceUD) callconv(.C) void = null, /// Goto tab - goto_tab: ?*const fn (SurfaceUD, GotoTab) callconv(.C) void = null, + goto_tab: ?*const fn (SurfaceUD, apprt.GotoTab) callconv(.C) void = null, /// Toggle fullscreen for current window. toggle_fullscreen: ?*const fn (SurfaceUD, configpkg.NonNativeFullscreen) callconv(.C) void = null, @@ -135,13 +135,6 @@ pub const App = struct { mouse_over_link: ?*const fn (SurfaceUD, ?[*]const u8, usize) void = null, }; - /// Special values for the goto_tab callback. - const GotoTab = enum(i32) { - previous = -1, - next = -2, - _, - }; - core_app: *CoreApp, config: *const Config, opts: Options, @@ -994,36 +987,13 @@ pub const Surface = struct { }; } - pub fn gotoTab(self: *Surface, n: usize) void { + pub fn gotoTab(self: *Surface, tab: apprt.GotoTab) void { const func = self.app.opts.goto_tab orelse { log.info("runtime embedder does not goto_tab", .{}); return; }; - const idx = std.math.cast(i32, n) orelse { - log.warn("cannot cast tab index to i32 n={}", .{n}); - return; - }; - - func(self.userdata, @enumFromInt(idx)); - } - - pub fn gotoPreviousTab(self: *Surface) void { - const func = self.app.opts.goto_tab orelse { - log.info("runtime embedder does not goto_tab", .{}); - return; - }; - - func(self.userdata, .previous); - } - - pub fn gotoNextTab(self: *Surface) void { - const func = self.app.opts.goto_tab orelse { - log.info("runtime embedder does not goto_tab", .{}); - return; - }; - - func(self.userdata, .next); + func(self.userdata, tab); } pub fn toggleFullscreen(self: *Surface, nonNativeFullscreen: configpkg.NonNativeFullscreen) void { diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 7ae690ce5..fd7b62cdc 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -795,31 +795,7 @@ pub fn hasTabs(self: *const Surface) bool { return window.hasTabs(); } -pub fn gotoPreviousTab(self: *Surface) void { - const window = self.container.window() orelse { - log.info( - "gotoPreviousTab invalid for container={s}", - .{@tagName(self.container)}, - ); - return; - }; - - window.gotoPreviousTab(self); -} - -pub fn gotoNextTab(self: *Surface) void { - const window = self.container.window() orelse { - log.info( - "gotoNextTab invalid for container={s}", - .{@tagName(self.container)}, - ); - return; - }; - - window.gotoNextTab(self); -} - -pub fn gotoTab(self: *Surface, n: usize) void { +pub fn gotoTab(self: *Surface, tab: apprt.GotoTab) void { const window = self.container.window() orelse { log.info( "gotoTab invalid for container={s}", @@ -828,7 +804,12 @@ pub fn gotoTab(self: *Surface, n: usize) void { return; }; - window.gotoTab(n); + switch (tab) { + .previous => window.gotoPreviousTab(self), + .next => window.gotoNextTab(self), + .last => window.gotoLastTab(), + else => window.gotoTab(@intCast(@intFromEnum(tab))), + } } pub fn setShouldClose(self: *Surface) void { diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 1615b986f..9df7a04ed 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -288,6 +288,13 @@ pub fn gotoNextTab(self: *Window, surface: *Surface) void { self.focusCurrentTab(); } +/// Go to the next tab for a surface. +pub fn gotoLastTab(self: *Window) void { + const max = c.gtk_notebook_get_n_pages(self.notebook) -| 1; + c.gtk_notebook_set_current_page(self.notebook, max); + self.focusCurrentTab(); +} + /// Go to the specific tab index. pub fn gotoTab(self: *Window, n: usize) void { if (n == 0) return; diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index 1982cc497..1e14b1b7c 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -62,6 +62,16 @@ pub const DesktopNotification = struct { body: []const u8, }; +/// The tab to jump to. This is non-exhaustive so that integer values represent +/// the index (zero-based) of the tab to jump to. Negative values are special +/// values. +pub const GotoTab = enum(c_int) { + previous = -1, + next = -2, + last = -3, + _, +}; + // This is made extern (c_int) to make interop easier with our embedded // runtime. The small size cost doesn't make a difference in our union. pub const SplitDirection = enum(c_int) { diff --git a/src/config/Config.zig b/src/config/Config.zig index 16466eeaa..6a9e9b3b3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1894,6 +1894,11 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { .{ .key = .{ .translated = .right_bracket }, .mods = .{ .super = true, .shift = true } }, .{ .next_tab = {} }, ); + try result.keybind.set.put( + alloc, + .{ .key = .{ .physical = inputpkg.Key.zero }, .mods = .{ .super = true } }, + .{ .last_tab = {} }, + ); try result.keybind.set.put( alloc, .{ .key = .{ .translated = .d }, .mods = .{ .super = true } }, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 2e30741f0..b129a9c56 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -240,6 +240,9 @@ pub const Action = union(enum) { /// Go to the next tab. next_tab: void, + /// Go to the last tab (the one with the highest index) + last_tab: void, + /// Go to the tab with the specific number, 1-indexed. goto_tab: usize,