mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
Merge pull request #169 from mitchellh/mrn/goto-tab-macos
macOS: implement cmd+[0-9] to goto tab
This commit is contained in:
@ -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;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
|
@ -26,6 +26,9 @@ struct ContentView: View {
|
||||
NSApplication.shared.reply(toApplicationShouldTerminate: true)
|
||||
}
|
||||
case .ready:
|
||||
let center = NotificationCenter.default
|
||||
let gotoTab = center.publisher(for: Ghostty.Notification.ghosttyGotoTab)
|
||||
|
||||
let confirmQuitting = Binding<Bool>(get: {
|
||||
self.appDelegate.confirmQuit && (self.window?.isKeyWindow ?? false)
|
||||
}, set: {
|
||||
@ -35,6 +38,7 @@ struct ContentView: View {
|
||||
Ghostty.TerminalSplit(onClose: Self.closeWindow)
|
||||
.ghosttyApp(ghostty.app!)
|
||||
.background(WindowAccessor(window: $window))
|
||||
.onReceive(gotoTab) { onGotoTab(notification: $0) }
|
||||
.confirmationDialog(
|
||||
"Quit Ghostty?",
|
||||
isPresented: confirmQuitting) {
|
||||
@ -57,4 +61,27 @@ struct ContentView: View {
|
||||
guard let currentWindow = NSApp.keyWindow else { return }
|
||||
currentWindow.close()
|
||||
}
|
||||
|
||||
private func onGotoTab(notification: SwiftUI.Notification) {
|
||||
// Notification center indiscriminately sends to every subscriber (makes sense)
|
||||
// but we only want to process this once. In order to process it once lets only
|
||||
// handle it if we're the focused window.
|
||||
guard let window = self.window else { return }
|
||||
guard window.isKeyWindow else { return }
|
||||
|
||||
// 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 windowController = window.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)
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -20,7 +20,9 @@ struct GhosttyApp: App {
|
||||
WindowGroup {
|
||||
ContentView(ghostty: ghostty)
|
||||
}
|
||||
|
||||
.backport.defaultSize(width: 800, height: 600)
|
||||
|
||||
.commands {
|
||||
CommandGroup(after: .newItem) {
|
||||
Button("New Tab", action: Self.newTab).keyboardShortcut("t", modifiers: [.command])
|
||||
|
@ -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