mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge pull request #490 from mitchellh/clipboard-request
Async clipboard read
This commit is contained in:
@ -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.
|
||||
|
@ -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<CChar>? {
|
||||
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<CChar>?, location: ghostty_clipboard_e) {
|
||||
|
249
src/Surface.zig
249
src/Surface.zig
@ -537,7 +537,14 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
|
||||
.cell_size => |size| try self.setCellSize(size),
|
||||
|
||||
.clipboard_read => |kind| try self.clipboardRead(kind),
|
||||
.clipboard_read => |kind| {
|
||||
if (!self.config.clipboard_read) {
|
||||
log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
try self.startClipboardRequest(.standard, .{ .osc_52 = kind });
|
||||
},
|
||||
|
||||
.clipboard_write => |req| switch (req) {
|
||||
.small => |v| try self.clipboardWrite(v.data[0..v.len], .standard),
|
||||
@ -663,89 +670,6 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
||||
return .{ .x = x, .y = y };
|
||||
}
|
||||
|
||||
/// Paste from the clipboard
|
||||
fn clipboardPaste(
|
||||
self: *Surface,
|
||||
loc: apprt.Clipboard,
|
||||
lock: bool,
|
||||
) !void {
|
||||
const data = self.rt_surface.getClipboardString(loc) catch |err| {
|
||||
log.warn("error reading clipboard: {}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
if (data.len > 0) {
|
||||
const bracketed = bracketed: {
|
||||
if (lock) self.renderer_state.mutex.lock();
|
||||
defer if (lock) self.renderer_state.mutex.unlock();
|
||||
|
||||
// With the lock held, we must scroll to the bottom.
|
||||
// We always scroll to the bottom for these inputs.
|
||||
self.scrollToBottom() catch |err| {
|
||||
log.warn("error scrolling to bottom err={}", .{err});
|
||||
};
|
||||
|
||||
break :bracketed self.io.terminal.modes.get(.bracketed_paste);
|
||||
};
|
||||
|
||||
if (bracketed) {
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
.write_stable = "\x1B[200~",
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
_ = self.io_thread.mailbox.push(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
data,
|
||||
), .{ .forever = {} });
|
||||
|
||||
if (bracketed) {
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
.write_stable = "\x1B[201~",
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
try self.io_thread.wakeup.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// This is similar to clipboardPaste but is used specifically for OSC 52
|
||||
fn clipboardRead(self: *const Surface, kind: u8) !void {
|
||||
if (!self.config.clipboard_read) {
|
||||
log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
const data = self.rt_surface.getClipboardString(.standard) catch |err| {
|
||||
log.warn("error reading clipboard: {}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
// Even if the clipboard data is empty we reply, since presumably
|
||||
// the client app is expecting a reply. We first allocate our buffer.
|
||||
// This must hold the base64 encoded data PLUS the OSC code surrounding it.
|
||||
const enc = std.base64.standard.Encoder;
|
||||
const size = enc.calcSize(data.len);
|
||||
var buf = try self.alloc.alloc(u8, size + 9); // const for OSC
|
||||
defer self.alloc.free(buf);
|
||||
|
||||
// Wrap our data with the OSC code
|
||||
const prefix = try std.fmt.bufPrint(buf, "\x1b]52;{c};", .{kind});
|
||||
assert(prefix.len == 7);
|
||||
buf[buf.len - 2] = '\x1b';
|
||||
buf[buf.len - 1] = '\\';
|
||||
|
||||
// Do the base64 encoding
|
||||
const encoded = enc.encode(buf[prefix.len..], data);
|
||||
assert(encoded.len == size);
|
||||
|
||||
_ = self.io_thread.mailbox.push(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
buf,
|
||||
), .{ .forever = {} });
|
||||
self.io_thread.wakeup.notify() catch {};
|
||||
}
|
||||
|
||||
fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) !void {
|
||||
if (!self.config.clipboard_write) {
|
||||
log.info("application attempted to write clipboard, but 'clipboard-write' setting is off", .{});
|
||||
@ -1541,41 +1465,45 @@ pub fn mouseButtonCallback(
|
||||
}
|
||||
}
|
||||
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// Report mouse events if enabled
|
||||
if (self.io.terminal.flags.mouse_event != .none) report: {
|
||||
// Shift overrides mouse "grabbing" in the window, taken from Kitty.
|
||||
if (mods.shift) break :report;
|
||||
{
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
if (self.io.terminal.flags.mouse_event != .none) report: {
|
||||
// Shift overrides mouse "grabbing" in the window, taken from Kitty.
|
||||
if (mods.shift) break :report;
|
||||
|
||||
// In any other mouse button scenario without shift pressed we
|
||||
// clear the selection since the underlying application can handle
|
||||
// that in any way (i.e. "scrolling").
|
||||
self.setSelection(null);
|
||||
// In any other mouse button scenario without shift pressed we
|
||||
// clear the selection since the underlying application can handle
|
||||
// that in any way (i.e. "scrolling").
|
||||
self.setSelection(null);
|
||||
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
|
||||
const report_action: MouseReportAction = switch (action) {
|
||||
.press => .press,
|
||||
.release => .release,
|
||||
};
|
||||
const report_action: MouseReportAction = switch (action) {
|
||||
.press => .press,
|
||||
.release => .release,
|
||||
};
|
||||
|
||||
try self.mouseReport(
|
||||
button,
|
||||
report_action,
|
||||
self.mouse.mods,
|
||||
pos,
|
||||
);
|
||||
try self.mouseReport(
|
||||
button,
|
||||
report_action,
|
||||
self.mouse.mods,
|
||||
pos,
|
||||
);
|
||||
|
||||
// If we're doing mouse reporting, we do not support any other
|
||||
// selection or highlighting.
|
||||
return;
|
||||
// If we're doing mouse reporting, we do not support any other
|
||||
// selection or highlighting.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// For left button clicks we always record some information for
|
||||
// selection/highlighting purposes.
|
||||
if (button == .left and action == .press) {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
|
||||
// If we move our cursor too much between clicks then we reset
|
||||
@ -1655,7 +1583,8 @@ pub fn mouseButtonCallback(
|
||||
.clipboard => .standard,
|
||||
.false => unreachable,
|
||||
};
|
||||
try self.clipboardPaste(clipboard, false);
|
||||
|
||||
try self.startClipboardRequest(clipboard, .{ .paste = {} });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2022,7 +1951,10 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !void
|
||||
}
|
||||
},
|
||||
|
||||
.paste_from_clipboard => try self.clipboardPaste(.standard, true),
|
||||
.paste_from_clipboard => try self.startClipboardRequest(
|
||||
.standard,
|
||||
.{ .paste = {} },
|
||||
),
|
||||
|
||||
.increase_font_size => |delta| {
|
||||
log.debug("increase font size={}", .{delta});
|
||||
@ -2202,6 +2134,103 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !void
|
||||
}
|
||||
}
|
||||
|
||||
/// Call this to complete a clipboard request sent to apprt. This should
|
||||
/// only be called once for each request. The data is immediately copied so
|
||||
/// it is safe to free the data after this call.
|
||||
pub fn completeClipboardRequest(
|
||||
self: *Surface,
|
||||
req: apprt.ClipboardRequest,
|
||||
data: []const u8,
|
||||
) !void {
|
||||
switch (req) {
|
||||
.paste => try self.completeClipboardPaste(data),
|
||||
.osc_52 => |kind| try self.completeClipboardReadOSC52(data, kind),
|
||||
}
|
||||
}
|
||||
|
||||
/// This starts a clipboard request, with some basic validation. For example,
|
||||
/// an OSC 52 request is not actually requested if OSC 52 is disabled.
|
||||
fn startClipboardRequest(
|
||||
self: *Surface,
|
||||
loc: apprt.Clipboard,
|
||||
req: apprt.ClipboardRequest,
|
||||
) !void {
|
||||
switch (req) {
|
||||
.paste => {}, // always allowed
|
||||
.osc_52 => if (!self.config.clipboard_read) {
|
||||
log.info(
|
||||
"application attempted to read clipboard, but 'clipboard-read' setting is off",
|
||||
.{},
|
||||
);
|
||||
return;
|
||||
},
|
||||
}
|
||||
|
||||
try self.rt_surface.clipboardRequest(loc, req);
|
||||
}
|
||||
|
||||
fn completeClipboardPaste(self: *Surface, data: []const u8) !void {
|
||||
if (data.len == 0) return;
|
||||
|
||||
const bracketed = bracketed: {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// With the lock held, we must scroll to the bottom.
|
||||
// We always scroll to the bottom for these inputs.
|
||||
self.scrollToBottom() catch |err| {
|
||||
log.warn("error scrolling to bottom err={}", .{err});
|
||||
};
|
||||
|
||||
break :bracketed self.io.terminal.modes.get(.bracketed_paste);
|
||||
};
|
||||
|
||||
if (bracketed) {
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
.write_stable = "\x1B[200~",
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
_ = self.io_thread.mailbox.push(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
data,
|
||||
), .{ .forever = {} });
|
||||
|
||||
if (bracketed) {
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
.write_stable = "\x1B[201~",
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
try self.io_thread.wakeup.notify();
|
||||
}
|
||||
|
||||
fn completeClipboardReadOSC52(self: *Surface, data: []const u8, kind: u8) !void {
|
||||
// Even if the clipboard data is empty we reply, since presumably
|
||||
// the client app is expecting a reply. We first allocate our buffer.
|
||||
// This must hold the base64 encoded data PLUS the OSC code surrounding it.
|
||||
const enc = std.base64.standard.Encoder;
|
||||
const size = enc.calcSize(data.len);
|
||||
var buf = try self.alloc.alloc(u8, size + 9); // const for OSC
|
||||
defer self.alloc.free(buf);
|
||||
|
||||
// Wrap our data with the OSC code
|
||||
const prefix = try std.fmt.bufPrint(buf, "\x1b]52;{c};", .{kind});
|
||||
assert(prefix.len == 7);
|
||||
buf[buf.len - 2] = '\x1b';
|
||||
buf[buf.len - 1] = '\\';
|
||||
|
||||
// Do the base64 encoding
|
||||
const encoded = enc.encode(buf[prefix.len..], data);
|
||||
assert(encoded.len == size);
|
||||
|
||||
_ = self.io_thread.mailbox.push(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
buf,
|
||||
), .{ .forever = {} });
|
||||
self.io_thread.wakeup.notify() catch {};
|
||||
}
|
||||
|
||||
const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf");
|
||||
const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf");
|
||||
const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf");
|
||||
|
@ -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.
|
||||
|
@ -547,25 +547,27 @@ pub const Surface = struct {
|
||||
self.window.setInputModeCursor(if (visible) .normal else .hidden);
|
||||
}
|
||||
|
||||
/// Read the clipboard. The windowing system is responsible for allocating
|
||||
/// a buffer as necessary. This should be a stable pointer until the next
|
||||
/// time getClipboardString is called.
|
||||
pub fn getClipboardString(
|
||||
self: *const Surface,
|
||||
/// Start an async clipboard request.
|
||||
pub fn clipboardRequest(
|
||||
self: *Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) ![:0]const u8 {
|
||||
_ = self;
|
||||
return switch (clipboard_type) {
|
||||
.standard => glfw.getClipboardString() orelse glfw.mustGetErrorCode(),
|
||||
state: apprt.ClipboardRequest,
|
||||
) !void {
|
||||
// GLFW can read clipboards immediately so just do that.
|
||||
const str: []const u8 = switch (clipboard_type) {
|
||||
.standard => glfw.getClipboardString() orelse return glfw.mustGetErrorCode(),
|
||||
.selection => selection: {
|
||||
// Not supported except on Linux
|
||||
if (comptime builtin.os.tag != .linux) return "";
|
||||
if (comptime builtin.os.tag != .linux) break :selection "";
|
||||
|
||||
const raw = glfwNative.getX11SelectionString() orelse
|
||||
return glfw.mustGetErrorCode();
|
||||
break :selection std.mem.span(raw);
|
||||
},
|
||||
};
|
||||
|
||||
// Complete our request
|
||||
try self.core_surface.completeClipboardRequest(state, str);
|
||||
}
|
||||
|
||||
/// Set the clipboard.
|
||||
|
@ -75,7 +75,6 @@ font_size: ?font.face.DesiredSize = null,
|
||||
/// Cached metrics about the surface from GTK callbacks.
|
||||
size: apprt.SurfaceSize,
|
||||
cursor_pos: apprt.CursorPos,
|
||||
clipboard: c.GValue,
|
||||
|
||||
/// Key input states. See gtkKeyPressed for detailed descriptions.
|
||||
in_keypress: bool = false,
|
||||
@ -151,7 +150,6 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
.font_size = opts.font_size,
|
||||
.size = .{ .width = 800, .height = 600 },
|
||||
.cursor_pos = .{ .x = 0, .y = 0 },
|
||||
.clipboard = std.mem.zeroes(c.GValue),
|
||||
.im_context = im_context,
|
||||
};
|
||||
errdefer self.* = undefined;
|
||||
@ -228,7 +226,6 @@ pub fn deinit(self: *Surface) void {
|
||||
|
||||
// Free all our GTK stuff
|
||||
c.g_object_unref(self.im_context);
|
||||
c.g_value_unset(&self.clipboard);
|
||||
|
||||
if (self.cursor) |cursor| c.g_object_unref(cursor);
|
||||
}
|
||||
@ -419,35 +416,26 @@ pub fn setMouseVisibility(self: *Surface, visible: bool) void {
|
||||
c.gtk_widget_set_cursor(@ptrCast(self.gl_area), self.app.cursor_none);
|
||||
}
|
||||
|
||||
pub fn getClipboardString(
|
||||
pub fn clipboardRequest(
|
||||
self: *Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) ![:0]const u8 {
|
||||
state: apprt.ClipboardRequest,
|
||||
) !void {
|
||||
// We allocate for userdata for the clipboard request. Not ideal but
|
||||
// clipboard requests aren't common so probably not a big deal.
|
||||
const alloc = self.app.core_app.alloc;
|
||||
const ud_ptr = try alloc.create(ClipboardRequest);
|
||||
errdefer alloc.destroy(ud_ptr);
|
||||
ud_ptr.* = .{ .self = self, .state = state };
|
||||
|
||||
// Start our async request
|
||||
const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type);
|
||||
const content = c.gdk_clipboard_get_content(clipboard) orelse {
|
||||
// On my machine, this NEVER works, so we fallback to glfw's
|
||||
// implementation... I believe this never works because we need to
|
||||
// use the async mechanism with GTK but that doesn't play nice
|
||||
// with what our core expects.
|
||||
log.debug("no GTK clipboard contents, falling back to glfw", .{});
|
||||
return switch (clipboard_type) {
|
||||
.standard => glfw.getClipboardString() orelse glfw.mustGetErrorCode(),
|
||||
.selection => value: {
|
||||
const raw = glfw_native.getX11SelectionString() orelse
|
||||
return glfw.mustGetErrorCode();
|
||||
break :value std.mem.span(raw);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
c.g_value_unset(&self.clipboard);
|
||||
_ = c.g_value_init(&self.clipboard, c.G_TYPE_STRING);
|
||||
if (c.gdk_content_provider_get_value(content, &self.clipboard, null) == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const ptr = c.g_value_get_string(&self.clipboard);
|
||||
return std.mem.sliceTo(ptr, 0);
|
||||
c.gdk_clipboard_read_text_async(
|
||||
clipboard,
|
||||
null,
|
||||
>kClipboardRead,
|
||||
ud_ptr,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setClipboardString(
|
||||
@ -459,6 +447,40 @@ pub fn setClipboardString(
|
||||
c.gdk_clipboard_set_text(clipboard, val.ptr);
|
||||
}
|
||||
|
||||
const ClipboardRequest = struct {
|
||||
self: *Surface,
|
||||
state: apprt.ClipboardRequest,
|
||||
};
|
||||
|
||||
fn gtkClipboardRead(
|
||||
source: ?*c.GObject,
|
||||
res: ?*c.GAsyncResult,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const req: *ClipboardRequest = @ptrCast(@alignCast(ud orelse return));
|
||||
const self = req.self;
|
||||
const alloc = self.app.core_app.alloc;
|
||||
defer alloc.destroy(req);
|
||||
|
||||
var gerr: ?*c.GError = null;
|
||||
const cstr = c.gdk_clipboard_read_text_finish(
|
||||
@ptrCast(source orelse return),
|
||||
res,
|
||||
&gerr,
|
||||
);
|
||||
if (gerr) |err| {
|
||||
defer c.g_error_free(err);
|
||||
log.warn("failed to read clipboard err={s}", .{err.message});
|
||||
return;
|
||||
}
|
||||
defer c.g_free(cstr);
|
||||
|
||||
const str = std.mem.sliceTo(cstr, 0);
|
||||
self.core_surface.completeClipboardRequest(req.state, str) catch |err| {
|
||||
log.err("failed to complete clipboard request err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn getClipboard(widget: *c.GtkWidget, clipboard: apprt.Clipboard) ?*c.GdkClipboard {
|
||||
return switch (clipboard) {
|
||||
.standard => c.gtk_widget_get_clipboard(widget),
|
||||
|
@ -31,3 +31,13 @@ pub const Clipboard = enum(u1) {
|
||||
standard = 0, // ctrl+c/v
|
||||
selection = 1, // also known as the "primary" clipboard
|
||||
};
|
||||
|
||||
/// Clipboard request. This is used to request clipboard contents and must
|
||||
/// be sent as a response to a ClipboardRequest event.
|
||||
pub const ClipboardRequest = union(enum) {
|
||||
/// A direct paste of clipboard contents.
|
||||
paste: void,
|
||||
|
||||
/// A request to write clipboard contents via OSC 52.
|
||||
osc_52: u8,
|
||||
};
|
||||
|
Reference in New Issue
Block a user