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:
Thorsten Ball
2023-07-01 17:12:55 +02:00
committed by Mitchell Hashimoto
parent cb775b5d56
commit 6ff3f62e3a
5 changed files with 58 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

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