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
This commit is contained in:
Danny Lin
2024-06-07 20:27:50 -07:00
committed by Cameron Dart
parent 9f3f663633
commit 655743c75f
10 changed files with 72 additions and 4 deletions

View File

@ -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 {

View File

@ -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?

View File

@ -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">
@ -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"/>
@ -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"/>
@ -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>

View File

@ -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
}

View File

@ -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() {

View File

@ -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

View File

@ -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()) {

View File

@ -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", .{});

View File

@ -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 } },

View File

@ -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,