diff --git a/include/ghostty.h b/include/ghostty.h index ea62ca74b..329ff6171 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -32,6 +32,8 @@ typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *); 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 *); +typedef void (*ghostty_runtime_focus_next_split_cb)(void *); +typedef void (*ghostty_runtime_focus_previous_split_cb)(void *); typedef struct { void *userdata; @@ -41,6 +43,8 @@ typedef struct { ghostty_runtime_write_clipboard_cb write_clipboard_cb; ghostty_runtime_new_split_cb new_split_cb; ghostty_runtime_close_surface_cb close_surface_cb; + ghostty_runtime_focus_next_split_cb focus_next_split_cb; + ghostty_runtime_focus_previous_split_cb focus_previous_split_cb; } ghostty_runtime_config_s; typedef struct { diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 59f68785b..736ca915d 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -58,7 +58,9 @@ extension Ghostty { read_clipboard_cb: { userdata in AppState.readClipboard(userdata) }, write_clipboard_cb: { userdata, str in AppState.writeClipboard(userdata, string: str) }, new_split_cb: { userdata, direction in AppState.newSplit(userdata, direction: ghostty_split_direction_e(UInt32(direction))) }, - close_surface_cb: { userdata in AppState.closeSurface(userdata) } + close_surface_cb: { userdata in AppState.closeSurface(userdata) }, + focus_next_split_cb: { userdata in AppState.focusSplit(userdata, direction: .next) }, + focus_previous_split_cb: { userdata in AppState.focusSplit(userdata, direction: .previous) } ) // Create the ghostty app. @@ -106,6 +108,17 @@ extension Ghostty { NotificationCenter.default.post(name: Notification.ghosttyCloseSurface, object: surface) } + static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: SplitFocusDirection) { + guard let surface = self.surfaceUserdata(from: userdata) else { return } + NotificationCenter.default.post( + name: Notification.ghosttyFocusSplit, + object: surface, + userInfo: [ + Notification.SplitDirectionKey: direction, + ] + ) + } + 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/Ghostty.SplitView.swift b/macos/Sources/Ghostty/Ghostty.SplitView.swift index 8dbbfd9bc..7e417e5c0 100644 --- a/macos/Sources/Ghostty/Ghostty.SplitView.swift +++ b/macos/Sources/Ghostty/Ghostty.SplitView.swift @@ -123,11 +123,14 @@ extension Ghostty { @Binding var requestClose: Bool var body: some View { - let pub = NotificationCenter.default.publisher(for: Notification.ghosttyNewSplit, object: leaf.surface) - let pubClose = NotificationCenter.default.publisher(for: Notification.ghosttyCloseSurface, object: leaf.surface) + let center = NotificationCenter.default + let pub = center.publisher(for: Notification.ghosttyNewSplit, object: leaf.surface) + let pubClose = center.publisher(for: Notification.ghosttyCloseSurface, object: leaf.surface) + let pubFocus = center.publisher(for: Notification.ghosttyFocusSplit, object: leaf.surface) SurfaceWrapper(surfaceView: leaf.surface) .onReceive(pub) { onNewSplit(notification: $0) } .onReceive(pubClose) { _ in requestClose = true } + .onReceive(pubFocus) { onMoveFocus(notification: $0) } } private func onNewSplit(notification: SwiftUI.Notification) { @@ -162,6 +165,13 @@ extension Ghostty { Self.fixFocus(container.bottomRight, previous: node) } + private func onMoveFocus(notification: SwiftUI.Notification) { + // Determine our desired direction + guard let directionAny = notification.userInfo?[Notification.SplitDirectionKey] else { return } + guard let direction = directionAny as? SplitFocusDirection else { return } + print("MOVE FOCUS: \(direction)") + } + /// There is a bug I can't figure out where when changing the split state, the terminal view /// will lose focus. There has to be some nice SwiftUI-native way to fix this but I can't /// figure it out so we're going to do this hacky thing to bring focus back to the terminal diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index b673549e5..603dec3e2 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -1,4 +1,28 @@ +import SwiftUI + struct Ghostty { // All the notifications that will be emitted will be put here. struct Notification {} } + +// MARK: Surface Notifications + +extension Ghostty { + /// An enum that is used for the directions that a split focus event can change. + enum SplitFocusDirection { + case previous, next + } +} + +extension Ghostty.Notification { + /// Posted when a new split is requested. The sending object will be the surface that had focus. The + /// userdata has one key "direction" with the direction to split to. + static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit") + + /// Close the calling surface. + static let ghosttyCloseSurface = Notification.Name("com.mitchellh.ghostty.closeSurface") + + /// Focus previous/next split. Has a SplitFocusDirection in the userinfo. + static let ghosttyFocusSplit = Notification.Name("com.mitchellh.ghostty.focusSplit") + static let SplitDirectionKey = ghosttyFocusSplit.rawValue +} diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 2f76fce11..659de9da7 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -509,18 +509,6 @@ extension Ghostty { 0x4E: GHOSTTY_KEY_KP_SUBTRACT, ]; } - -} - -// MARK: Surface Notifications - -extension Ghostty.Notification { - /// Posted when a new split is requested. The sending object will be the surface that had focus. The - /// userdata has one key "direction" with the direction to split to. - static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit") - - /// Close the calling surface. - static let ghosttyCloseSurface = Notification.Name("com.mitchellh.ghostty.closeSurface") } // MARK: Surface Environment Keys diff --git a/src/Surface.zig b/src/Surface.zig index f2452b85b..e9c007a96 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -947,6 +947,18 @@ pub fn keyCallback( } else log.warn("runtime doesn't implement newSplit", .{}); }, + .next_split => { + if (@hasDecl(apprt.Surface, "gotoNextSplit")) { + self.rt_surface.gotoNextSplit(); + } else log.warn("runtime doesn't implement gotoNextSplit", .{}); + }, + + .previous_split => { + if (@hasDecl(apprt.Surface, "gotoPreviousSplit")) { + self.rt_surface.gotoPreviousSplit(); + } else log.warn("runtime doesn't implement gotoPreviousSplit", .{}); + }, + .close_surface => { if (@hasDecl(apprt.Surface, "closeSurface")) { try self.rt_surface.closeSurface(); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 7ed69a514..bff1bb6cb 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -51,6 +51,10 @@ pub const App = struct { /// Close the current surface given by this function. close_surface: ?*const fn (SurfaceUD) callconv(.C) void = null, + + /// Focus the previous/next split (if any). + focus_next_split: ?*const fn (SurfaceUD) callconv(.C) void = null, + focus_previous_split: ?*const fn (SurfaceUD) callconv(.C) void = null, }; core_app: *CoreApp, @@ -166,7 +170,25 @@ pub const Surface = struct { pub fn closeSurface(self: *const Surface) !void { const func = self.app.opts.close_surface orelse { - log.info("runtime embedder does not closing a surface", .{}); + log.info("runtime embedder does not support closing a surface", .{}); + return; + }; + + func(self.opts.userdata); + } + + pub fn gotoNextSplit(self: *const Surface) void { + const func = self.app.opts.focus_next_split orelse { + log.info("runtime embedder does not support focus next split", .{}); + return; + }; + + func(self.opts.userdata); + } + + pub fn gotoPreviousSplit(self: *const Surface) void { + const func = self.app.opts.focus_previous_split orelse { + log.info("runtime embedder does not support focus previous split", .{}); return; }; diff --git a/src/config.zig b/src/config.zig index 2b6b3766f..193bd69a5 100644 --- a/src/config.zig +++ b/src/config.zig @@ -311,6 +311,16 @@ pub const Config = struct { .{ .key = .d, .mods = .{ .super = true, .shift = true } }, .{ .new_split = .down }, ); + try result.keybind.set.put( + alloc, + .{ .key = .left_bracket, .mods = .{ .super = true } }, + .{ .previous_split = {} }, + ); + try result.keybind.set.put( + alloc, + .{ .key = .right_bracket, .mods = .{ .super = true } }, + .{ .next_split = {} }, + ); { // Cmd+N for goto tab N const start = @enumToInt(inputpkg.Key.one); diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 3eeb941a6..549e7a615 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -184,6 +184,12 @@ pub const Action = union(enum) { /// in the direction given. new_split: SplitDirection, + /// Go to the previous split. + previous_split: void, + + /// Go to the next split. + next_split: void, + /// Close the current "surface", whether that is a window, tab, split, /// etc. This only closes ONE surface. close_surface: void,