mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macOS: implement cmd+[0-9] to goto tab
This is my attempt at fixing #63. It works! But: 1. The `NotificationCenter` subscription is triggered once for every open tab. That's obviously wrong. But I'm not sure and could use some pointers where else to put the subscription. That leads me to... 2. I'm _not_ knowledgable in Swift/AppKit/SwiftUI, so I might have put the wrong/right things in the wrong/right places. For example: wasn't sure what's to be handled in Swift and what's to be handled by the core in Zig. Would love some pointers :)
This commit is contained in:

committed by
Mitchell Hashimoto

parent
cb775b5d56
commit
6ff3f62e3a
@ -238,6 +238,7 @@ typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *);
|
||||
typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e);
|
||||
typedef void (*ghostty_runtime_close_surface_cb)(void *, bool);
|
||||
typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e);
|
||||
typedef void (*ghostty_runtime_goto_tab_cb)(void *, int32_t);
|
||||
|
||||
typedef struct {
|
||||
void *userdata;
|
||||
@ -249,6 +250,7 @@ typedef struct {
|
||||
ghostty_runtime_new_split_cb new_split_cb;
|
||||
ghostty_runtime_close_surface_cb close_surface_cb;
|
||||
ghostty_runtime_focus_split_cb focus_split_cb;
|
||||
ghostty_runtime_goto_tab_cb goto_tab_cb;
|
||||
} ghostty_runtime_config_s;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
|
@ -61,7 +61,8 @@ extension Ghostty {
|
||||
write_clipboard_cb: { userdata, str in AppState.writeClipboard(userdata, string: str) },
|
||||
new_split_cb: { userdata, direction in AppState.newSplit(userdata, direction: direction) },
|
||||
close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) },
|
||||
focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) }
|
||||
focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) },
|
||||
goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) }
|
||||
)
|
||||
|
||||
// Create the ghostty app.
|
||||
@ -157,6 +158,17 @@ extension Ghostty {
|
||||
)
|
||||
}
|
||||
|
||||
static func gotoTab(_ userdata: UnsafeMutableRawPointer?, n: Int32) {
|
||||
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.ghosttyGotoTab,
|
||||
object: surface,
|
||||
userInfo: [
|
||||
Notification.GotoTabKey: n,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
static func readClipboard(_ userdata: UnsafeMutableRawPointer?) -> UnsafePointer<CChar>? {
|
||||
guard let appState = self.appState(fromSurface: userdata) else { return nil }
|
||||
guard let str = NSPasteboard.general.string(forType: .string) else { return nil }
|
||||
|
@ -74,6 +74,10 @@ extension Ghostty.Notification {
|
||||
/// Focus previous/next split. Has a SplitFocusDirection in the userinfo.
|
||||
static let ghosttyFocusSplit = Notification.Name("com.mitchellh.ghostty.focusSplit")
|
||||
static let SplitDirectionKey = ghosttyFocusSplit.rawValue
|
||||
|
||||
/// Goto tab. Has tab index in the userinfo.
|
||||
static let ghosttyGotoTab = Notification.Name("com.mitchellh.ghostty.gotoTab")
|
||||
static let GotoTabKey = ghosttyGotoTab.rawValue
|
||||
}
|
||||
|
||||
// Make the input enum hashable.
|
||||
|
@ -17,10 +17,17 @@ struct GhosttyApp: App {
|
||||
@FocusedValue(\.ghosttySurfaceView) private var focusedSurface
|
||||
|
||||
var body: some Scene {
|
||||
let center = NotificationCenter.default
|
||||
let gotoTab = center.publisher(for: Ghostty.Notification.ghosttyGotoTab)
|
||||
|
||||
WindowGroup {
|
||||
ContentView(ghostty: ghostty)
|
||||
// TODO: This is wrong. This fires for every open tab.
|
||||
.onReceive(gotoTab) { onGotoTab(notification: $0) }
|
||||
}
|
||||
|
||||
.backport.defaultSize(width: 800, height: 600)
|
||||
|
||||
.commands {
|
||||
CommandGroup(after: .newItem) {
|
||||
Button("New Tab", action: Self.newTab).keyboardShortcut("t", modifiers: [.command])
|
||||
@ -100,6 +107,26 @@ struct GhosttyApp: App {
|
||||
guard let surface = surfaceView.surface else { return }
|
||||
ghostty.splitMoveFocus(surface: surface, direction: direction)
|
||||
}
|
||||
|
||||
private func onGotoTab(notification: SwiftUI.Notification) {
|
||||
// Get the tab index from the notification
|
||||
guard let tabIndexAny = notification.userInfo?[Ghostty.Notification.GotoTabKey] else { return }
|
||||
guard let tabIndex = tabIndexAny as? Int32 else { return }
|
||||
|
||||
guard let currentWindow = NSApp.keyWindow else { return }
|
||||
guard let windowController = currentWindow.windowController else { return }
|
||||
guard let tabGroup = windowController.window?.tabGroup else { return }
|
||||
|
||||
let tabbedWindows = tabGroup.windows
|
||||
|
||||
// Tabs are 0-indexed here, so we subtract one from the key the user hit.
|
||||
let adjustedIndex = Int(tabIndex - 1);
|
||||
guard adjustedIndex >= 0 && adjustedIndex < tabbedWindows.count else { return }
|
||||
|
||||
let targetWindow = tabbedWindows[adjustedIndex]
|
||||
targetWindow.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
|
@ -61,6 +61,9 @@ pub const App = struct {
|
||||
|
||||
/// Focus the previous/next split (if any).
|
||||
focus_split: ?*const fn (SurfaceUD, input.SplitFocusDirection) callconv(.C) void = null,
|
||||
|
||||
/// Goto tab
|
||||
goto_tab: ?*const fn (SurfaceUD, usize) callconv(.C) void = null,
|
||||
};
|
||||
|
||||
core_app: *CoreApp,
|
||||
@ -359,6 +362,15 @@ pub const Surface = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn gotoTab(self: *Surface, n: usize) void {
|
||||
const func = self.app.opts.goto_tab orelse {
|
||||
log.info("runtime embedder does not goto_tab", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
func(self.opts.userdata, n);
|
||||
}
|
||||
|
||||
/// The cursor position from the host directly is in screen coordinates but
|
||||
/// all our interface works in pixels.
|
||||
fn cursorPosToPixels(self: *const Surface, pos: apprt.CursorPos) !apprt.CursorPos {
|
||||
|
Reference in New Issue
Block a user