gtk: support performable keybinds for clipboard paste

Make clipboardRequest return bool to indicate whether the action could
be performed. For paste requests, synchronously check if the clipboard
contains text formats before starting the async read.

This allows 'performable:paste_from_clipboard' keybinds to pass through
when the clipboard contains non-text content (e.g., images), enabling
terminal applications to handle their own clipboard reading.

Changes:
- Surface.startClipboardRequest now returns bool
- paste_from_clipboard/paste_from_selection actions return the result
- GTK apprt checks clipboard formats synchronously before async read
- Embedded apprt always returns true (can't check synchronously)
- All other call sites discard the return value with _
This commit is contained in:
cyppe
2025-12-29 15:44:46 +01:00
parent 8a419e5526
commit 0da650e7dd
4 changed files with 50 additions and 44 deletions

View File

@@ -1026,7 +1026,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
return;
}
try self.startClipboardRequest(.standard, .{ .osc_52_read = clipboard });
_ = try self.startClipboardRequest(.standard, .{ .osc_52_read = clipboard });
},
.clipboard_write => |w| switch (w.req) {
@@ -4173,7 +4173,7 @@ pub fn mouseButtonCallback(
.selection
else
.standard;
try self.startClipboardRequest(clipboard, .{ .paste = {} });
_ = try self.startClipboardRequest(clipboard, .{ .paste = {} });
}
// Right-click down selects word for context menus. If the apprt
@@ -4251,7 +4251,7 @@ pub fn mouseButtonCallback(
// request so we need to unlock.
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
try self.startClipboardRequest(.standard, .paste);
_ = try self.startClipboardRequest(.standard, .paste);
// We don't need to clear selection because we didn't have
// one to begin with.
@@ -4266,7 +4266,7 @@ pub fn mouseButtonCallback(
// request so we need to unlock.
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
try self.startClipboardRequest(.standard, .paste);
_ = try self.startClipboardRequest(.standard, .paste);
},
}
@@ -5330,12 +5330,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
return true;
},
.paste_from_clipboard => try self.startClipboardRequest(
.paste_from_clipboard => return try self.startClipboardRequest(
.standard,
.{ .paste = {} },
),
.paste_from_selection => try self.startClipboardRequest(
.paste_from_selection => return try self.startClipboardRequest(
.selection,
.{ .paste = {} },
),
@@ -6049,11 +6049,15 @@ pub fn completeClipboardRequest(
/// This starts a clipboard request, with some basic validation. For example,
/// an OSC 52 request is not actually requested if OSC 52 is disabled.
///
/// Returns true if the request was started, false if it was not (e.g., clipboard
/// doesn't contain text for paste requests). This allows performable keybinds
/// to pass through when the action cannot be performed.
fn startClipboardRequest(
self: *Surface,
loc: apprt.Clipboard,
req: apprt.ClipboardRequest,
) !void {
) !bool {
switch (req) {
.paste => {}, // always allowed
.osc_52_read => if (self.config.clipboard_read == .deny) {
@@ -6061,14 +6065,14 @@ fn startClipboardRequest(
"application attempted to read clipboard, but 'clipboard-read' is set to deny",
.{},
);
return;
return false;
},
// No clipboard write code paths travel through this function
.osc_52_write => unreachable,
}
try self.rt_surface.clipboardRequest(loc, req);
return try self.rt_surface.clipboardRequest(loc, req);
}
fn completeClipboardPaste(

View File

@@ -652,7 +652,7 @@ pub const Surface = struct {
self: *Surface,
clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest,
) !void {
) !bool {
// 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
@@ -667,6 +667,10 @@ pub const Surface = struct {
@intCast(@intFromEnum(clipboard_type)),
state_ptr,
);
// Embedded apprt can't synchronously check clipboard content types,
// so we always return true to indicate the request was started.
return true;
}
fn completeClipboardRequest(

View File

@@ -73,8 +73,8 @@ pub fn clipboardRequest(
self: *Self,
clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest,
) !void {
try self.surface.clipboardRequest(
) !bool {
return try self.surface.clipboardRequest(
clipboard_type,
state,
);

View File

@@ -1666,8 +1666,8 @@ pub const Surface = extern struct {
self: *Self,
clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest,
) !void {
try Clipboard.request(
) !bool {
return try Clipboard.request(
self,
clipboard_type,
state,
@@ -3623,16 +3623,34 @@ const Clipboard = struct {
/// Request data from the clipboard (read the clipboard). This
/// completes asynchronously and will call the `completeClipboardRequest`
/// core surface API when done.
///
/// Returns true if the request was started, false if the clipboard
/// doesn't contain text (allowing performable keybinds to pass through).
pub fn request(
self: *Surface,
clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest,
) Allocator.Error!void {
) Allocator.Error!bool {
// Get our requested clipboard
const clipboard = get(
self.private().gl_area.as(gtk.Widget),
clipboard_type,
) orelse return;
) orelse return false;
// For paste requests, check if clipboard has text format available.
// This is a synchronous check that allows performable keybinds to
// pass through when the clipboard contains non-text content (e.g., images).
if (state == .paste) {
const formats = clipboard.getFormats();
if (formats.containMimeType("text/plain") == 0 and
formats.containMimeType("UTF8_STRING") == 0 and
formats.containMimeType("TEXT") == 0 and
formats.containMimeType("STRING") == 0)
{
log.debug("clipboard has no text format, not starting paste request", .{});
return false;
}
}
// Allocate our userdata
const alloc = Application.default().allocator();
@@ -3652,6 +3670,8 @@ const Clipboard = struct {
clipboardReadText,
ud,
);
return true;
}
/// Paste explicit text directly into the surface, regardless of the
@@ -3814,34 +3834,21 @@ const Clipboard = struct {
const self = req.self;
defer self.unref();
const surface = self.private().core_surface orelse return;
var gerr: ?*glib.Error = null;
const cstr_ = clipboard.readTextFinish(res, &gerr);
// If clipboard has no text (error, null, or empty), pass through the
// original keypress so applications can handle their own clipboard
// (e.g., reading images via wl-paste). We send raw Ctrl+V (0x16)
// directly using the text action to bypass bracketed paste encoding.
if (gerr) |err| {
defer err.free();
log.debug("clipboard has no text format: {s}", .{err.f_message orelse "(no message)"});
passthroughKeypress(surface);
log.warn(
"failed to read clipboard err={s}",
.{err.f_message orelse "(no message)"},
);
return;
}
const cstr = cstr_ orelse {
passthroughKeypress(surface);
return;
};
const cstr = cstr_ orelse return;
defer glib.free(cstr);
const str = std.mem.sliceTo(cstr, 0);
if (str.len == 0) {
passthroughKeypress(surface);
return;
}
const surface = self.private().core_surface orelse return;
surface.completeClipboardRequest(
req.state,
str,
@@ -3875,15 +3882,6 @@ const Clipboard = struct {
);
}
/// Send raw Ctrl+V (ASCII 22) to the terminal, bypassing paste encoding.
/// This allows applications to handle their own clipboard reading
/// (e.g., for image paste via wl-paste on Wayland).
fn passthroughKeypress(surface: *CoreSurface) void {
_ = surface.performBindingAction(.{ .text = "\\x16" }) catch |err| {
log.warn("error sending passthrough keypress: {}", .{err});
};
}
/// The request we send as userdata to the clipboard read.
const Request = struct {
/// "Self" is reffed so we can't dispose it until the clipboard