mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: hook up clipboards
This commit is contained in:
@ -28,11 +28,15 @@ extern "C" {
|
|||||||
// for all of these types is available in the Zig source.
|
// for all of these types is available in the Zig source.
|
||||||
typedef void (*ghostty_runtime_wakeup_cb)(void *);
|
typedef void (*ghostty_runtime_wakeup_cb)(void *);
|
||||||
typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
|
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 {
|
typedef struct {
|
||||||
void *userdata;
|
void *userdata;
|
||||||
ghostty_runtime_wakeup_cb wakeup_cb;
|
ghostty_runtime_wakeup_cb wakeup_cb;
|
||||||
ghostty_runtime_set_title_cb set_title_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;
|
} ghostty_runtime_config_s;
|
||||||
|
|
||||||
typedef struct {
|
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);
|
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s *, ghostty_config_t);
|
||||||
void ghostty_app_free(ghostty_app_t);
|
void ghostty_app_free(ghostty_app_t);
|
||||||
int ghostty_app_tick(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*);
|
ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*);
|
||||||
void ghostty_surface_free(ghostty_surface_t);
|
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_refresh(ghostty_surface_t);
|
||||||
void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
|
void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
|
||||||
void ghostty_surface_set_focus(ghostty_surface_t, bool);
|
void ghostty_surface_set_focus(ghostty_surface_t, bool);
|
||||||
|
@ -55,6 +55,9 @@ class GhosttyState: ObservableObject {
|
|||||||
/// The ghostty app instance. We only have one of these for the entire app, although I guess
|
/// 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...
|
/// in theory you can have multiple... I don't know why you would...
|
||||||
var app: ghostty_app_t? = nil
|
var app: ghostty_app_t? = nil
|
||||||
|
|
||||||
|
/// Cached clipboard string for `read_clipboard` callback.
|
||||||
|
private var cached_clipboard_string: String? = nil
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Initialize ghostty global state. This happens once per process.
|
// Initialize ghostty global state. This happens once per process.
|
||||||
@ -84,7 +87,9 @@ class GhosttyState: ObservableObject {
|
|||||||
var runtime_cfg = ghostty_runtime_config_s(
|
var runtime_cfg = ghostty_runtime_config_s(
|
||||||
userdata: Unmanaged.passUnretained(self).toOpaque(),
|
userdata: Unmanaged.passUnretained(self).toOpaque(),
|
||||||
wakeup_cb: { userdata in GhosttyState.wakeup(userdata) },
|
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.
|
// Create the ghostty app.
|
||||||
guard let app = ghostty_app_new(&runtime_cfg, cfg) else {
|
guard let app = ghostty_app_new(&runtime_cfg, cfg) else {
|
||||||
@ -97,11 +102,35 @@ class GhosttyState: ObservableObject {
|
|||||||
self.readiness = .ready
|
self.readiness = .ready
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
ghostty_app_free(app)
|
||||||
|
ghostty_config_free(config)
|
||||||
|
}
|
||||||
|
|
||||||
func appTick() {
|
func appTick() {
|
||||||
guard let app = self.app else { return }
|
guard let app = self.app else { return }
|
||||||
ghostty_app_tick(app)
|
ghostty_app_tick(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Ghostty Callbacks
|
||||||
|
|
||||||
|
static func readClipboard(_ userdata: UnsafeMutableRawPointer?) -> UnsafePointer<CChar>? {
|
||||||
|
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<CChar>?) {
|
||||||
|
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?) {
|
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {
|
||||||
let state = Unmanaged<GhosttyState>.fromOpaque(userdata!).takeUnretainedValue()
|
let state = Unmanaged<GhosttyState>.fromOpaque(userdata!).takeUnretainedValue()
|
||||||
|
|
||||||
@ -120,8 +149,12 @@ class GhosttyState: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
/// Returns the GhosttyState from the given userdata value.
|
||||||
ghostty_app_free(app)
|
static func appState(fromSurface userdata: UnsafeMutableRawPointer?) -> GhosttyState? {
|
||||||
ghostty_config_free(config)
|
let surfaceView = Unmanaged<TerminalSurfaceView_Real>.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<GhosttyState>.fromOpaque(app_ud).takeUnretainedValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,8 +76,8 @@ class TerminalSurfaceView_Real: NSView, NSTextInputClient, ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var surface: ghostty_surface_t? = nil
|
var surface: ghostty_surface_t? = nil
|
||||||
private var error: Error? = nil
|
var error: Error? = nil
|
||||||
private var markedText: NSMutableAttributedString;
|
private var markedText: NSMutableAttributedString;
|
||||||
|
|
||||||
// We need to support being a first responder so that we can get input events
|
// We need to support being a first responder so that we can get input events
|
||||||
|
10
src/App.zig
10
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 {
|
export fn ghostty_app_free(ptr: ?*App) void {
|
||||||
if (ptr) |v| {
|
if (ptr) |v| {
|
||||||
v.destroy();
|
v.destroy();
|
||||||
@ -400,6 +405,11 @@ pub const CAPI = struct {
|
|||||||
if (ptr) |v| v.app.closeWindow(v);
|
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
|
/// Tell the surface that it needs to schedule a render
|
||||||
export fn ghostty_surface_refresh(win: *Window) void {
|
export fn ghostty_surface_refresh(win: *Window) void {
|
||||||
win.window.refresh();
|
win.window.refresh();
|
||||||
|
@ -23,15 +23,27 @@ pub const App = struct {
|
|||||||
///
|
///
|
||||||
/// C type: ghostty_runtime_config_s
|
/// C type: ghostty_runtime_config_s
|
||||||
pub const Options = extern struct {
|
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 that is passed to all the callbacks.
|
||||||
userdata: ?*anyopaque = null,
|
userdata: AppUD = null,
|
||||||
|
|
||||||
/// Callback called to wakeup the event loop. This should trigger
|
/// Callback called to wakeup the event loop. This should trigger
|
||||||
/// a full tick of the app loop.
|
/// 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.
|
/// 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,
|
opts: Options,
|
||||||
@ -114,13 +126,12 @@ pub const Window = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getClipboardString(self: *const Window) ![:0]const u8 {
|
pub fn getClipboardString(self: *const Window) ![:0]const u8 {
|
||||||
_ = self;
|
const ptr = self.core_win.app.runtime.opts.read_clipboard(self.opts.userdata);
|
||||||
return "";
|
return std.mem.sliceTo(ptr, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setClipboardString(self: *const Window, val: [:0]const u8) !void {
|
pub fn setClipboardString(self: *const Window, val: [:0]const u8) !void {
|
||||||
_ = self;
|
self.core_win.app.runtime.opts.write_clipboard(self.opts.userdata, val.ptr);
|
||||||
_ = val;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setShouldClose(self: *Window) void {
|
pub fn setShouldClose(self: *Window) void {
|
||||||
|
Reference in New Issue
Block a user