From dff45003e1bd6493a98f088c4767f544bc193f4e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 19 Feb 2023 15:18:01 -0800 Subject: [PATCH] macos: hook up clipboards --- include/ghostty.h | 6 ++++ macos/Sources/GhosttyApp.swift | 41 ++++++++++++++++++++++--- macos/Sources/TerminalSurfaceView.swift | 4 +-- src/App.zig | 10 ++++++ src/apprt/embedded.zig | 25 ++++++++++----- 5 files changed, 73 insertions(+), 13 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index fa07d657d..1764ef445 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -28,11 +28,15 @@ extern "C" { // for all of these types is available in the Zig source. typedef void (*ghostty_runtime_wakeup_cb)(void *); typedef void (*ghostty_runtime_set_title_cb)(void *, const char *); +typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *); +typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *); typedef struct { void *userdata; ghostty_runtime_wakeup_cb wakeup_cb; ghostty_runtime_set_title_cb set_title_cb; + ghostty_runtime_read_clipboard_cb read_clipboard_cb; + ghostty_runtime_write_clipboard_cb write_clipboard_cb; } ghostty_runtime_config_s; typedef struct { @@ -221,9 +225,11 @@ void ghostty_config_finalize(ghostty_config_t); ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s *, ghostty_config_t); void ghostty_app_free(ghostty_app_t); int ghostty_app_tick(ghostty_app_t); +void *ghostty_app_userdata(ghostty_app_t); ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*); void ghostty_surface_free(ghostty_surface_t); +ghostty_app_t ghostty_surface_app(ghostty_surface_t); void ghostty_surface_refresh(ghostty_surface_t); void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); void ghostty_surface_set_focus(ghostty_surface_t, bool); diff --git a/macos/Sources/GhosttyApp.swift b/macos/Sources/GhosttyApp.swift index a68978c2b..7d38157cb 100644 --- a/macos/Sources/GhosttyApp.swift +++ b/macos/Sources/GhosttyApp.swift @@ -55,6 +55,9 @@ class GhosttyState: ObservableObject { /// The ghostty app instance. We only have one of these for the entire app, although I guess /// in theory you can have multiple... I don't know why you would... var app: ghostty_app_t? = nil + + /// Cached clipboard string for `read_clipboard` callback. + private var cached_clipboard_string: String? = nil init() { // Initialize ghostty global state. This happens once per process. @@ -84,7 +87,9 @@ class GhosttyState: ObservableObject { var runtime_cfg = ghostty_runtime_config_s( userdata: Unmanaged.passUnretained(self).toOpaque(), wakeup_cb: { userdata in GhosttyState.wakeup(userdata) }, - set_title_cb: { userdata, title in GhosttyState.setTitle(userdata, title: title) }) + set_title_cb: { userdata, title in GhosttyState.setTitle(userdata, title: title) }, + read_clipboard_cb: { userdata in GhosttyState.readClipboard(userdata) }, + write_clipboard_cb: { userdata, str in GhosttyState.writeClipboard(userdata, string: str) }) // Create the ghostty app. guard let app = ghostty_app_new(&runtime_cfg, cfg) else { @@ -97,11 +102,35 @@ class GhosttyState: ObservableObject { self.readiness = .ready } + deinit { + ghostty_app_free(app) + ghostty_config_free(config) + } + func appTick() { guard let app = self.app else { return } ghostty_app_tick(app) } + // MARK: Ghostty Callbacks + + 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 } + + // 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 + } + + static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?) { + guard let valueStr = String(cString: string!, encoding: .utf8) else { return } + let pb = NSPasteboard.general + pb.declareTypes([.string], owner: nil) + pb.setString(valueStr, forType: .string) + } + static func wakeup(_ userdata: UnsafeMutableRawPointer?) { let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() @@ -120,8 +149,12 @@ class GhosttyState: ObservableObject { } } - deinit { - ghostty_app_free(app) - ghostty_config_free(config) + /// Returns the GhosttyState from the given userdata value. + static func appState(fromSurface userdata: UnsafeMutableRawPointer?) -> GhosttyState? { + let surfaceView = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() + guard let surface = surfaceView.surface else { return nil } + guard let app = ghostty_surface_app(surface) else { return nil } + guard let app_ud = ghostty_app_userdata(app) else { return nil } + return Unmanaged.fromOpaque(app_ud).takeUnretainedValue() } } diff --git a/macos/Sources/TerminalSurfaceView.swift b/macos/Sources/TerminalSurfaceView.swift index 103c88ceb..9acf38756 100644 --- a/macos/Sources/TerminalSurfaceView.swift +++ b/macos/Sources/TerminalSurfaceView.swift @@ -76,8 +76,8 @@ class TerminalSurfaceView_Real: NSView, NSTextInputClient, ObservableObject { } } - private var surface: ghostty_surface_t? = nil - private var error: Error? = nil + var surface: ghostty_surface_t? = nil + var error: Error? = nil private var markedText: NSMutableAttributedString; // We need to support being a first responder so that we can get input events diff --git a/src/App.zig b/src/App.zig index 29d3fc617..4efbda3b2 100644 --- a/src/App.zig +++ b/src/App.zig @@ -368,6 +368,11 @@ pub const CAPI = struct { }; } + /// Return the userdata associated with the app. + export fn ghostty_app_userdata(v: *App) ?*anyopaque { + return v.runtime.opts.userdata; + } + export fn ghostty_app_free(ptr: ?*App) void { if (ptr) |v| { v.destroy(); @@ -400,6 +405,11 @@ pub const CAPI = struct { if (ptr) |v| v.app.closeWindow(v); } + /// Returns the app associated with a surface. + export fn ghostty_surface_app(win: *Window) *App { + return win.app; + } + /// Tell the surface that it needs to schedule a render export fn ghostty_surface_refresh(win: *Window) void { win.window.refresh(); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 8a1f345b0..663860977 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -23,15 +23,27 @@ pub const App = struct { /// /// C type: ghostty_runtime_config_s pub const Options = extern struct { + /// These are just aliases to make the function signatures below + /// more obvious what values will be sent. + const AppUD = ?*anyopaque; + const SurfaceUD = ?*anyopaque; + /// Userdata that is passed to all the callbacks. - userdata: ?*anyopaque = null, + userdata: AppUD = null, /// Callback called to wakeup the event loop. This should trigger /// a full tick of the app loop. - wakeup: *const fn (?*anyopaque) callconv(.C) void, + wakeup: *const fn (AppUD) callconv(.C) void, /// Called to set the title of the window. - set_title: *const fn (?*anyopaque, [*]const u8) callconv(.C) void, + set_title: *const fn (SurfaceUD, [*]const u8) callconv(.C) void, + + /// Read the clipboard value. The return value must be preserved + /// by the host until the next call. + read_clipboard: *const fn (SurfaceUD) callconv(.C) [*:0]const u8, + + /// Write the clipboard value. + write_clipboard: *const fn (SurfaceUD, [*:0]const u8) callconv(.C) void, }; opts: Options, @@ -114,13 +126,12 @@ pub const Window = struct { } pub fn getClipboardString(self: *const Window) ![:0]const u8 { - _ = self; - return ""; + const ptr = self.core_win.app.runtime.opts.read_clipboard(self.opts.userdata); + return std.mem.sliceTo(ptr, 0); } pub fn setClipboardString(self: *const Window, val: [:0]const u8) !void { - _ = self; - _ = val; + self.core_win.app.runtime.opts.write_clipboard(self.opts.userdata, val.ptr); } pub fn setShouldClose(self: *Window) void {