From 6ff3f62e3a87c7f2b88fa0b52fc9e1fd9fdbd560 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 1 Jul 2023 17:12:55 +0200 Subject: [PATCH 1/2] 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 :) --- include/ghostty.h | 2 ++ macos/Sources/Ghostty/AppState.swift | 14 +++++++++++++- macos/Sources/Ghostty/Package.swift | 4 ++++ macos/Sources/GhosttyApp.swift | 27 +++++++++++++++++++++++++++ src/apprt/embedded.zig | 12 ++++++++++++ 5 files changed, 58 insertions(+), 1 deletion(-) diff --git a/include/ghostty.h b/include/ghostty.h index 163044c1b..70428c89f 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -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; //------------------------------------------------------------------- diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 4a76e9a9d..6a8e3ec5c 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -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? { guard let appState = self.appState(fromSurface: userdata) else { return nil } guard let str = NSPasteboard.general.string(forType: .string) else { return nil } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 535fa1e67..e3c9c5a1f 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -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. diff --git a/macos/Sources/GhosttyApp.swift b/macos/Sources/GhosttyApp.swift index c5a1e7fa1..b9b948f86 100644 --- a/macos/Sources/GhosttyApp.swift +++ b/macos/Sources/GhosttyApp.swift @@ -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 { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 43886df57..7289b2401 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -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 { From eebcbb69558ec9a0e7957dd675e0384683df9337 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 1 Jul 2023 09:19:38 -0700 Subject: [PATCH 2/2] macos: attach gotoTab listener to contentView and filter on focused win --- macos/Sources/ContentView.swift | 27 +++++++++++++++++++++++++++ macos/Sources/GhosttyApp.swift | 25 ------------------------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/macos/Sources/ContentView.swift b/macos/Sources/ContentView.swift index eb938cc2e..d53951d47 100644 --- a/macos/Sources/ContentView.swift +++ b/macos/Sources/ContentView.swift @@ -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(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) + } } diff --git a/macos/Sources/GhosttyApp.swift b/macos/Sources/GhosttyApp.swift index b9b948f86..85845dd02 100644 --- a/macos/Sources/GhosttyApp.swift +++ b/macos/Sources/GhosttyApp.swift @@ -17,13 +17,8 @@ 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) @@ -107,26 +102,6 @@ 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 {