From 655743c75f716ef8ca93cc7d473086e9610770e2 Mon Sep 17 00:00:00 2001 From: Danny Lin Date: Fri, 7 Jun 2024 20:27:50 -0700 Subject: [PATCH 1/3] HACK: ctrl-tab to swap to last active tab KVO on tabGroup.selectedWindow works but I can't figure out how to detect when the tabGroup changes, so the observer gets attached to the wrong tabGroup TODO: lastActiveTabIndex needs to be per-tab-group, not global, but I can't figure out how to store per-tab-group or per-associated-window state --- include/ghostty.h | 1 + macos/Sources/App/macOS/AppDelegate.swift | 1 + macos/Sources/App/macOS/MainMenu.xib | 19 +++++++++++++++---- .../Terminal/TerminalController.swift | 13 +++++++++++++ .../Features/Terminal/TerminalWindow.swift | 7 +++++++ macos/Sources/Ghostty/Ghostty.App.swift | 3 +++ src/Surface.zig | 14 ++++++++++++++ src/apprt/embedded.zig | 10 ++++++++++ src/config/Config.zig | 5 +++++ src/input/Binding.zig | 3 +++ 10 files changed, 72 insertions(+), 4 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 04233287f7..42f7e1e914 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 8b6b064a97..1d278a2729 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 bbfd59eaea..9515aa9ac3 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 81b86a215d..a883d0c52f 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 d259637aed..0b3e57a72b 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 2e991ecba7..7be3618ec1 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 bb90841f82..6094ffb490 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 113d9379a7..815ddd455f 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 1d6b08aba9..56ab743790 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 5640843d9c..059b8633ee 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, From 27f5a6d08e37cdd72d1cc0078336a13ccf6d2ba2 Mon Sep 17 00:00:00 2001 From: Cameron Dart <8763518+SkamDart@users.noreply.github.com> Date: Fri, 21 Jun 2024 03:56:50 +0000 Subject: [PATCH 2/3] gtk: last_active_tab --- src/apprt/gtk/Surface.zig | 12 ++++++++++++ src/apprt/gtk/Window.zig | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 1ee433db93..2d1c92be58 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -695,6 +695,18 @@ pub fn gotoTab(self: *Surface, n: usize) void { window.gotoTab(n); } +pub fn gotoLastActiveTab(self: *Surface) void { + const window = self.container.window() orelse { + log.info( + "gotoLastActiveTab invalid for container={s}", + .{@tagName(self.container)}, + ); + return; + }; + + window.gotoLastActiveTab(); +} + pub fn setShouldClose(self: *Surface) void { _ = self; } diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 0cb73c4076..d1b1b9f8ff 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -30,6 +30,8 @@ window: *c.GtkWindow, /// The notebook (tab grouping) for this window. notebook: *c.GtkNotebook, +last_active_tab_index: c_int = 0, + pub fn create(alloc: Allocator, app: *App) !*Window { // Allocate a fixed pointer for our window. We try to minimize // allocations but windows and other GUI requirements are so minimal @@ -248,6 +250,9 @@ pub fn gotoPreviousTab(self: *Window, surface: *Surface) void { // Do nothing if we have one tab if (next_idx == page_idx) return; + // Cache the last active tab index for our last-active-tab hotkey. + self.last_active_tab_index = page_idx; + c.gtk_notebook_set_current_page(self.notebook, next_idx); self.focusCurrentTab(); } @@ -265,6 +270,9 @@ pub fn gotoNextTab(self: *Window, surface: *Surface) void { const next_idx = if (page_idx < max) page_idx + 1 else 0; if (next_idx == page_idx) return; + // Cache the last active tab index for our last-active-tab hotkey. + self.last_active_tab_index = page_idx; + c.gtk_notebook_set_current_page(self.notebook, next_idx); self.focusCurrentTab(); } @@ -278,6 +286,20 @@ pub fn gotoTab(self: *Window, n: usize) void { c.gtk_notebook_set_current_page(self.notebook, page_idx); self.focusCurrentTab(); } + + // Cache the last active tab index for our last-active-tab hotkey. + self.last_active_tab_index = page_idx; +} + +// Goto the more recently selected tab. +pub fn gotoLastActiveTab(self: *Window) void { + if (self.last_active_tab_index >= 0) { + const page_idx = c.gtk_notebook_get_current_page(self.notebook); + c.gtk_notebook_set_current_page(self.notebook, self.last_active_tab_index); + self.focusCurrentTab(); + log.debug("going to last active tab", .{}); + self.last_active_tab_index = page_idx; + } } /// Toggle fullscreen for this window. From 0d5067a5caf17cb375667376a4c368d444e6bf71 Mon Sep 17 00:00:00 2001 From: Cameron Dart <8763518+SkamDart@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:42:27 -0700 Subject: [PATCH 3/3] gtk: noop if last active tab is closed --- src/apprt/gtk/Window.zig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index d1b1b9f8ff..ebd2efc782 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -207,6 +207,13 @@ pub fn closeTab(self: *Window, tab: *Tab) void { // Find page and tab which we're closing const page_idx = getNotebookPageIndex(page); + // If the page we are closing is the last active tab, + // then make the last active tab keybind a noop until + // a new tab is selected. + if (page_idx == self.last_active_tab_index) { + self.last_active_tab_index = -1; + } + // Remove the page. This will destroy the GTK widgets in the page which // will trigger Tab cleanup. c.gtk_notebook_remove_page(self.notebook, page_idx); @@ -297,7 +304,6 @@ pub fn gotoLastActiveTab(self: *Window) void { const page_idx = c.gtk_notebook_get_current_page(self.notebook); c.gtk_notebook_set_current_page(self.notebook, self.last_active_tab_index); self.focusCurrentTab(); - log.debug("going to last active tab", .{}); self.last_active_tab_index = page_idx; } }