Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(macos/gtk): keybind for accessing the last active tab #1869

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/ghostty.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions macos/Sources/App/macOS/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
19 changes: 15 additions & 4 deletions macos/Sources/App/macOS/MainMenu.xib
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22689"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
Expand All @@ -22,6 +22,7 @@
<outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
<outlet property="menuEqualizeSplits" destination="3gH-VD-vL9" id="SiZ-ce-FOF"/>
<outlet property="menuIncreaseFontSize" destination="CIH-ey-Z6x" id="hkc-9C-80E"/>
<outlet property="menuLastActiveTab" destination="ucq-9T-8lf" id="KbP-Y4-YBa"/>
<outlet property="menuMoveSplitDividerDown" destination="Zj7-2W-fdF" id="997-LL-nlN"/>
<outlet property="menuMoveSplitDividerLeft" destination="wSR-ny-j1a" id="HCZ-CI-2ob"/>
<outlet property="menuMoveSplitDividerRight" destination="CcX-ql-QU4" id="rIn-PK-fVM"/>
Expand All @@ -41,8 +42,8 @@
<outlet property="menuSelectSplitLeft" destination="cTK-oy-KuV" id="Jpr-5q-dqz"/>
<outlet property="menuSelectSplitRight" destination="upj-mc-L7X" id="nLY-o1-lky"/>
<outlet property="menuServices" destination="aQe-vS-j8Q" id="uWQ-Wo-T1L"/>
<outlet property="menuSplitRight" destination="VUR-Ld-nLx" id="RxO-Zw-ovb"/>
<outlet property="menuSplitDown" destination="UDZ-4y-6xL" id="fgZ-Wb-8OR"/>
<outlet property="menuSplitRight" destination="VUR-Ld-nLx" id="RxO-Zw-ovb"/>
<outlet property="menuTerminalInspector" destination="QwP-M5-fvh" id="wJi-Dh-S9f"/>
<outlet property="menuToggleFullScreen" destination="8kY-Pi-KaY" id="yQg-6V-OO6"/>
<outlet property="menuZoomSplit" destination="oPd-mn-IEH" id="wTu-jK-egI"/>
Expand Down Expand Up @@ -233,7 +234,17 @@
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem isSeparatorItem="YES" id="Bws-Hg-Q2a"/>
<menuItem title="Show Next Tab" id="ucq-9T-8lf">
<string key="keyEquivalent" base64-UTF8="YES">
CQ
</string>
<modifierMask key="keyEquivalentModifierMask" control="YES"/>
<connections>
<action selector="selectLastActiveTab:" target="-1" id="nJL-fj-u7w"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="CMt-XK-G4G"/>
<menuItem title="Toggle Full Screen" keyEquivalent="f" id="8kY-Pi-KaY">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
Expand Down
13 changes: 13 additions & 0 deletions macos/Sources/Features/Terminal/TerminalController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
7 changes: 7 additions & 0 deletions macos/Sources/Features/Terminal/TerminalWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
3 changes: 3 additions & 0 deletions macos/Sources/Ghostty/Ghostty.App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
10 changes: 10 additions & 0 deletions src/apprt/embedded.zig
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ pub const App = struct {
const GotoTab = enum(i32) {
previous = -1,
next = -2,
last_active = -3,
_,
};

Expand Down Expand Up @@ -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", .{});
Expand Down
12 changes: 12 additions & 0 deletions src/apprt/gtk/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
28 changes: 28 additions & 0 deletions src/apprt/gtk/Window.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -205,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);
Expand Down Expand Up @@ -248,6 +257,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();
}
Expand All @@ -265,6 +277,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();
}
Expand All @@ -278,6 +293,19 @@ 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();
self.last_active_tab_index = page_idx;
}
}

/// Toggle fullscreen for this window.
Expand Down
5 changes: 5 additions & 0 deletions src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
Expand Down
3 changes: 3 additions & 0 deletions src/input/Binding.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
Loading