diff --git a/include/ghostty.h b/include/ghostty.h index 7b4afa1d5..1bdb4fc31 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -316,7 +316,7 @@ typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void *); typedef void (*ghostty_runtime_set_title_cb)(void *, const char *); typedef void (*ghostty_runtime_set_mouse_shape_cb)(void *, ghostty_mouse_shape_e); typedef void (*ghostty_runtime_set_mouse_visibility_cb)(void *, bool); -typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboard_e); +typedef void (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboard_e, void *); 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, ghostty_surface_config_s); typedef void (*ghostty_runtime_new_tab_cb)(void *, ghostty_surface_config_s); @@ -393,6 +393,7 @@ void ghostty_surface_request_close(ghostty_surface_t); void ghostty_surface_split(ghostty_surface_t, ghostty_split_direction_e); void ghostty_surface_split_focus(ghostty_surface_t, ghostty_split_focus_direction_e); bool ghostty_surface_binding_action(ghostty_surface_t, const char *, uintptr_t); +void ghostty_surface_complete_clipboard_request(ghostty_surface_t, const char *, uintptr_t, void *); // APIs I'd like to get rid of eventually but are still needed for now. // Don't use these unless you know what you're doing. diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index e0dab0b75..fe6ee42ae 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -72,9 +72,6 @@ extension Ghostty { return v; } - /// Cached clipboard string for `read_clipboard` callback. - private var cached_clipboard_string: String? = nil - init() { // Initialize ghostty global state. This happens once per process. guard ghostty_init() == GHOSTTY_SUCCESS else { @@ -100,7 +97,7 @@ extension Ghostty { set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) }, set_mouse_shape_cb: { userdata, shape in AppState.setMouseShape(userdata, shape: shape) }, set_mouse_visibility_cb: { userdata, visible in AppState.setMouseVisibility(userdata, visible: visible) }, - read_clipboard_cb: { userdata, loc in AppState.readClipboard(userdata, location: loc) }, + read_clipboard_cb: { userdata, loc, state in AppState.readClipboard(userdata, location: loc, state: state) }, write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) }, new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) }, new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) }, @@ -301,18 +298,24 @@ extension Ghostty { ) } - static func readClipboard(_ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e) -> UnsafePointer? { + static func readClipboard(_ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e, state: UnsafeMutableRawPointer?) { + // If we don't even have a surface, something went terrible wrong so we have + // to leak "state". + guard let surfaceView = self.surfaceUserdata(from: userdata) else { return } + guard let surface = surfaceView.surface else { return } + // We only support the standard clipboard - if (location != GHOSTTY_CLIPBOARD_STANDARD) { return nil } - - guard let surface = self.surfaceUserdata(from: userdata) else { return nil } - guard let appState = self.appState(fromView: surface) else { return nil } - guard let str = NSPasteboard.general.string(forType: .string) else { return nil } - - // Ghostty requires we cache the string because the pointer we return has to remain - // stable until the next call to readClipboard. - appState.cached_clipboard_string = str - return (str as NSString).utf8String + if (location != GHOSTTY_CLIPBOARD_STANDARD) { + return completeClipboardRequest(surface, data: "", state: state) + } + + // Get our string + let str = NSPasteboard.general.string(forType: .string) ?? "" + completeClipboardRequest(surface, data: str, state: state) + } + + static private func completeClipboardRequest(_ surface: ghostty_surface_t, data: String, state: UnsafeMutableRawPointer?) { + ghostty_surface_complete_clipboard_request(surface, data, UInt(data.count), state) } static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?, location: ghostty_clipboard_e) { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 4ee4b5770..bc1efc3ec 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -58,7 +58,7 @@ pub const App = struct { /// Read the clipboard value. The return value must be preserved /// by the host until the next call. If there is no valid clipboard /// value then this should return null. - read_clipboard: *const fn (SurfaceUD, c_int) callconv(.C) ?[*:0]const u8, + read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.C) void, /// Write the clipboard value. write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int) callconv(.C) void, @@ -342,15 +342,25 @@ pub const Surface = struct { }; } - pub fn getClipboardString( - self: *const Surface, + pub fn clipboardRequest( + self: *Surface, clipboard_type: apprt.Clipboard, - ) ![:0]const u8 { - const ptr = self.app.opts.read_clipboard( + state: apprt.ClipboardRequest, + ) !void { + // We need to allocate to get a pointer to store our clipboard request + // so that it is stable until the read_clipboard callback and call + // complete_clipboard_request. This sucks but clipboard requests aren't + // high throughput so it's probably fine. + const alloc = self.app.core_app.alloc; + const state_ptr = try alloc.create(apprt.ClipboardRequest); + errdefer alloc.destroy(state_ptr); + state_ptr.* = state; + + self.app.opts.read_clipboard( self.opts.userdata, @intCast(@intFromEnum(clipboard_type)), - ) orelse return ""; - return std.mem.sliceTo(ptr, 0); + state_ptr, + ); } pub fn setClipboardString( @@ -982,6 +992,26 @@ pub const CAPI = struct { return true; } + /// Complete a clipboard read request startd via the read callback. + /// This can only be called once for a given request. Once it is called + /// with a request the request pointer will be invalidated. + export fn ghostty_surface_complete_clipboard_request( + ptr: *Surface, + str_ptr: [*]const u8, + str_len: usize, + state: *apprt.ClipboardRequest, + ) void { + // The state is unusable after this + defer ptr.core_surface.app.alloc.destroy(state); + + if (str_len == 0) return; + const str = str_ptr[0..str_len]; + ptr.core_surface.completeClipboardRequest(state.*, str) catch |err| { + log.err("error completing clipboard request err={}", .{err}); + return; + }; + } + /// Sets the window background blur on macOS to the desired value. /// I do this in Zig as an extern function because I don't know how to /// call these functions in Swift.