diff --git a/include/ghostty.h b/include/ghostty.h index a6fc51d9b..9a627e11d 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -234,6 +234,7 @@ typedef enum { GHOSTTY_BINDING_COPY_TO_CLIPBOARD, GHOSTTY_BINDING_PASTE_FROM_CLIPBOARD, GHOSTTY_BINDING_NEW_TAB, + GHOSTTY_BINDING_NEW_WINDOW, } ghostty_binding_action_e; // Fully defined types. This MUST be kept in sync with equivalent Zig @@ -253,6 +254,7 @@ typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboa typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty_clipboard_e); typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e); typedef void (*ghostty_runtime_new_tab_cb)(void *, ghostty_surface_config_s); +typedef void (*ghostty_runtime_new_window_cb)(void *, ghostty_surface_config_s); 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); @@ -268,6 +270,7 @@ typedef struct { ghostty_runtime_write_clipboard_cb write_clipboard_cb; ghostty_runtime_new_split_cb new_split_cb; ghostty_runtime_new_tab_cb new_tab_cb; + ghostty_runtime_new_window_cb new_window_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; diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/AppDelegate.swift index fa468221d..b13bd0aa9 100644 --- a/macos/Sources/AppDelegate.swift +++ b/macos/Sources/AppDelegate.swift @@ -77,7 +77,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } @IBAction func newWindow(_ sender: Any?) { - windowManager.addNewWindow() + windowManager.newWindow() } @IBAction func newTab(_ sender: Any?) { diff --git a/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift b/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift index 987dc86a2..4280877c0 100644 --- a/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift +++ b/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift @@ -46,21 +46,32 @@ class PrimaryWindowManager { init(ghostty: Ghostty.AppState) { self.ghostty = ghostty - // Register self as observer for the NewTab notification that - // is triggered via callback from Zig code. - NotificationCenter.default.addObserver( + // Register self as observer for the NewTab/NewWindow notifications that + // are triggered via callback from Zig code. + let center = NotificationCenter.default; + center.addObserver( self, selector: #selector(onNewTab), name: Ghostty.Notification.ghosttyNewTab, object: nil) + center.addObserver( + self, + selector: #selector(onNewWindow), + name: Ghostty.Notification.ghosttyNewWindow, + object: nil) } deinit { - // Clean up the observer. - NotificationCenter.default.removeObserver( + // Clean up the observers. + let center = NotificationCenter.default; + center.removeObserver( self, name: Ghostty.Notification.ghosttyNewTab, object: nil) + center.removeObserver( + self, + name: Ghostty.Notification.ghosttyNewWindow, + object: nil) } /// Add the initial window for the application. This should only be called once from the AppDelegate. @@ -73,12 +84,33 @@ class PrimaryWindowManager { } } - func addNewWindow() { - guard let controller = createWindowController() else { return } + func newWindow() { + if let window = mainWindow as? PrimaryWindow { + // If we already have a window, we go through Zig core code, which calls back into Swift. + self.triggerNewWindow(withParent: window) + } else { + self.addNewWindow() + } + } + + func triggerNewWindow(withParent window: PrimaryWindow) { + guard let surface = window.focusedSurfaceWrapper.surface else { return } + ghostty.newWindow(surface: surface) + } + + func addNewWindow(withBaseConfig config: ghostty_surface_config_s? = nil) { + guard let controller = createWindowController(withBaseConfig: config) else { return } guard let newWindow = addManagedWindow(windowController: controller)?.window else { return } newWindow.makeKeyAndOrderFront(nil) } + @objc private func onNewWindow(notification: SwiftUI.Notification) { + let configAny = notification.userInfo?[Ghostty.Notification.NewWindowKey] + let config = configAny as? ghostty_surface_config_s + + self.addNewWindow(withBaseConfig: config) + } + // triggerNewTab tells the Zig core code to create a new tab, which then calls // back into Swift code. func triggerNewTab(for window: PrimaryWindow) { diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 1a0a17f4b..51ff9db0a 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -62,6 +62,7 @@ extension Ghostty { write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) }, new_split_cb: { userdata, direction in AppState.newSplit(userdata, direction: direction) }, new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) }, + new_window_cb: { userdata, surfaceConfig in AppState.newWindow(userdata, config: surfaceConfig) }, close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) }, focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) }, goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) }, @@ -142,6 +143,10 @@ extension Ghostty { ghostty_surface_binding_action(surface, GHOSTTY_BINDING_NEW_TAB, nil) } + func newWindow(surface: ghostty_surface_t) { + ghostty_surface_binding_action(surface, GHOSTTY_BINDING_NEW_WINDOW, nil) + } + func split(surface: ghostty_surface_t, direction: ghostty_split_direction_e) { ghostty_surface_split(surface, direction) } @@ -266,13 +271,24 @@ extension Ghostty { static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { guard let surface = self.surfaceUserdata(from: userdata) else { return } - var userInfo: [AnyHashable : Any] = [:]; - userInfo[Notification.NewTabKey] = config; - NotificationCenter.default.post( name: Notification.ghosttyNewTab, object: surface, - userInfo: userInfo + userInfo: [ + Notification.NewTabKey: config + ] + ) + } + + static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { + guard let surface = self.surfaceUserdata(from: userdata) else { return } + + NotificationCenter.default.post( + name: Notification.ghosttyNewWindow, + object: surface, + userInfo: [ + Notification.NewWindowKey: config + ] ) } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 006433c72..b0ca3d560 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -79,9 +79,13 @@ extension Ghostty.Notification { static let ghosttyGotoTab = Notification.Name("com.mitchellh.ghostty.gotoTab") static let GotoTabKey = ghosttyGotoTab.rawValue - /// New tab. Has base surface config requestesd in userinfo. + /// New tab. Has base surface config requested in userinfo. static let ghosttyNewTab = Notification.Name("com.mitchellh.ghostty.newTab") static let NewTabKey = ghosttyNewTab.rawValue + + /// New window. Has base surface config requested in userinfo. + static let ghosttyNewWindow = Notification.Name("com.mitchellh.ghostty.newWindow") + static let NewWindowKey = ghosttyNewWindow.rawValue /// Toggle fullscreen of current window static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen") diff --git a/src/Surface.zig b/src/Surface.zig index 582c6cd71..173edea18 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2119,11 +2119,15 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !void } else log.warn("dev mode was not compiled into this binary", .{}), .new_window => { - _ = self.app_mailbox.push(.{ - .new_window = .{ - .parent = self, - }, - }, .{ .instant = {} }); + if (@hasDecl(apprt.Surface, "newWindow")) { + try self.rt_surface.newWindow(); + } else { + _ = self.app_mailbox.push(.{ + .new_window = .{ + .parent = self, + }, + }, .{ .instant = {} }); + } }, .new_tab => { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 777b7870e..adae5b09c 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -62,6 +62,9 @@ pub const App = struct { /// New tab with options. new_tab: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null, + /// New window with options. + new_window: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null, + /// Close the current surface given by this function. close_surface: ?*const fn (SurfaceUD, bool) callconv(.C) void = null, @@ -615,14 +618,29 @@ pub const Surface = struct { return; }; + const options = self.newSurfaceOptions(); + func(self.opts.userdata, options); + } + + pub fn newWindow(self: *const Surface) !void { + const func = self.app.opts.new_window orelse { + log.info("runtime embedder does not support new_window", .{}); + return; + }; + + const options = self.newSurfaceOptions(); + func(self.opts.userdata, options); + } + + fn newSurfaceOptions(self: *const Surface) apprt.Surface.Options { const font_size: u16 = font_size: { if (!self.app.config.@"window-inherit-font-size") break :font_size 0; break :font_size self.core_surface.font_size.points; }; - func(self.opts.userdata, .{ + return .{ .font_size = font_size, - }); + }; } /// The cursor position from the host directly is in screen coordinates but @@ -855,6 +873,7 @@ pub const CAPI = struct { .copy_to_clipboard => .{ .copy_to_clipboard = {} }, .paste_from_clipboard => .{ .paste_from_clipboard = {} }, .new_tab => .{ .new_tab = {} }, + .new_window => .{ .new_window = {} }, }; ptr.core_surface.performBindingAction(action) catch |err| { diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 400e9695e..8e5e9ec1e 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -277,6 +277,7 @@ pub const Key = enum(c_int) { copy_to_clipboard, paste_from_clipboard, new_tab, + new_window, }; /// Trigger is the associated key state that can trigger an action.