diff --git a/include/ghostty.h b/include/ghostty.h index 04233287f..42f7e1e91 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -142,6 +142,7 @@ typedef enum { typedef enum { GHOSTTY_TAB_PREVIOUS = -1, GHOSTTY_TAB_NEXT = -2, + GHOSTTY_TAB_LAST_ACTIVE = -3, } ghostty_tab_e; typedef enum { diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 8b6b064a9..1d278a272 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -36,6 +36,7 @@ class AppDelegate: NSObject, @IBOutlet private var menuPaste: NSMenuItem? @IBOutlet private var menuSelectAll: NSMenuItem? + @IBOutlet private var menuLastActiveTab: NSMenuItem? @IBOutlet private var menuToggleFullScreen: NSMenuItem? @IBOutlet private var menuZoomSplit: NSMenuItem? @IBOutlet private var menuPreviousSplit: NSMenuItem? diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index bbfd59eae..9515aa9ac 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -22,6 +22,7 @@ + @@ -41,8 +42,8 @@ - + @@ -233,7 +234,17 @@ - + + + +CQ + + + + + + + diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 81b86a215..a883d0c52 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -550,6 +550,17 @@ class TerminalController: NSWindowController, NSWindowDelegate, splitMoveFocus(direction: .right) } + @IBAction func selectLastActiveTab(_ sender: Any) { + guard let surface = focusedSurface else { return } + NotificationCenter.default.post( + name: Ghostty.Notification.ghosttyGotoTab, + object: surface, + userInfo: [ + Ghostty.Notification.GotoTabKey: GHOSTTY_TAB_LAST_ACTIVE.rawValue, + ] + ) + } + @IBAction func equalizeSplits(_ sender: Any) { guard let surface = focusedSurface?.surface else { return } ghostty.splitEqualize(surface: surface) @@ -710,6 +721,8 @@ class TerminalController: NSWindowController, NSWindowDelegate, } else { finalIndex = selectedIndex + 1 } + } else if (tabIndex == GHOSTTY_TAB_LAST_ACTIVE.rawValue) { + finalIndex = ghostty.lastActiveTabIndex } else { return } diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index d259637ae..0b3e57a72 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -108,6 +108,13 @@ class TerminalWindow: NSWindow { resetZoomTabButton.contentTintColor = .secondaryLabelColor resetZoomToolbarButton.contentTintColor = .tertiaryLabelColor tab.attributedTitle = attributedTitle + + // if resigned key and not selected tab, then we were the last active tab + if let tabGroup, + tabGroup.selectedWindow != self, + let selfIndex = tabGroup.windows.firstIndex(of: self) { + (windowController as? TerminalController)?.ghostty.lastActiveTabIndex = selfIndex + } } override func layoutIfNeeded() { diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 2e991ecba..7be3618ec 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -24,6 +24,9 @@ extension Ghostty { /// Optional delegate weak var delegate: GhosttyAppDelegate? + // TODO: this needs to be per-tab-group + var lastActiveTabIndex = 0 + /// The readiness value of the state. @Published var readiness: Readiness = .loading diff --git a/src/Surface.zig b/src/Surface.zig index bb90841f8..6094ffb49 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3424,6 +3424,20 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool } else log.warn("runtime doesn't implement gotoPreviousTab", .{}); }, + .last_active_tab => { + log.warn("last_active_tab is deprecated, use gotoLastActiveTab", .{}); + if (@hasDecl(apprt.Surface, "hasTabs")) { + if (!self.rt_surface.hasTabs()) { + log.debug("surface has no tabs, ignoring last_active_tab binding", .{}); + return false; + } + } + + if (@hasDecl(apprt.Surface, "gotoLastActiveTab")) { + self.rt_surface.gotoLastActiveTab(); + } else log.warn("runtime doesn't implement gotoLastActiveTab", .{}); + }, + .next_tab => { if (@hasDecl(apprt.Surface, "hasTabs")) { if (!self.rt_surface.hasTabs()) { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 113d9379a..815ddd455 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -134,6 +134,7 @@ pub const App = struct { const GotoTab = enum(i32) { previous = -1, next = -2, + last_active = -3, _, }; @@ -995,6 +996,15 @@ pub const Surface = struct { func(self.userdata, .previous); } + pub fn gotoLastActiveTab(self: *Surface) void { + const func = self.app.opts.goto_tab orelse { + log.info("runtime embedder does not goto_tab", .{}); + return; + }; + + func(self.userdata, .last_active); + } + pub fn gotoNextTab(self: *Surface) void { const func = self.app.opts.goto_tab orelse { log.info("runtime embedder does not goto_tab", .{}); diff --git a/src/config/Config.zig b/src/config/Config.zig index 1d6b08aba..56ab74379 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1565,6 +1565,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 = .{ .translated = .tab }, .mods = .{ .ctrl = true } }, + .{ .last_active_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 5640843d9..059b8633e 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -215,6 +215,9 @@ pub const Action = union(enum) { /// Go to the next tab. next_tab: void, + /// Go to the last active tab. + last_active_tab: void, + /// Go to the tab with the specific number, 1-indexed. goto_tab: usize,