From 8a419e5526b1fc69eef06cfae1457b217ae900fe Mon Sep 17 00:00:00 2001 From: cyppe Date: Mon, 29 Dec 2025 09:58:09 +0100 Subject: [PATCH 1/4] gtk: pass through keypress when clipboard has no text When paste_from_clipboard is triggered but the clipboard contains no text (e.g., an image), send the raw Ctrl+V keypress to the terminal instead of silently returning. This allows applications to handle their own clipboard reading (e.g., via wl-paste for images on Wayland). --- src/apprt/gtk/class/surface.zig | 34 +++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index a14d53c32..965f518c9 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -3814,21 +3814,34 @@ 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.warn( - "failed to read clipboard err={s}", - .{err.f_message orelse "(no message)"}, - ); + log.debug("clipboard has no text format: {s}", .{err.f_message orelse "(no message)"}); + passthroughKeypress(surface); return; } - const cstr = cstr_ orelse return; + + const cstr = cstr_ orelse { + passthroughKeypress(surface); + return; + }; defer glib.free(cstr); const str = std.mem.sliceTo(cstr, 0); - const surface = self.private().core_surface orelse return; + if (str.len == 0) { + passthroughKeypress(surface); + return; + } + surface.completeClipboardRequest( req.state, str, @@ -3862,6 +3875,15 @@ 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 From 0da650e7dd6d08a376b5d212036d0b9d3231b9e3 Mon Sep 17 00:00:00 2001 From: cyppe Date: Mon, 29 Dec 2025 15:44:46 +0100 Subject: [PATCH 2/4] 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 _ --- src/Surface.zig | 22 +++++++----- src/apprt/embedded.zig | 6 +++- src/apprt/gtk/Surface.zig | 4 +-- src/apprt/gtk/class/surface.zig | 62 ++++++++++++++++----------------- 4 files changed, 50 insertions(+), 44 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index c9223d0ad..614f40475 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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( diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 64900cef1..a1b6a6e9b 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -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( diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 009ce018d..918e77146 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -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, ); diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 965f518c9..cbd444936 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -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 From 83314473983d41b99547144dde83624041b8e58f Mon Sep 17 00:00:00 2001 From: cyppe Date: Mon, 29 Dec 2025 16:15:02 +0100 Subject: [PATCH 3/4] Improve type detection --- src/apprt/gtk/class/surface.zig | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index cbd444936..22e08c598 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -3642,11 +3642,9 @@ const Clipboard = struct { // 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) - { + // G_TYPE_STRING = G_TYPE_MAKE_FUNDAMENTAL(16) = (16 << 2) = 64 + const G_TYPE_STRING: usize = 64; + if (formats.containGtype(G_TYPE_STRING) == 0) { log.debug("clipboard has no text format, not starting paste request", .{}); return false; } From 972b65eb1b12d7872790639066c20c0bf46f783b Mon Sep 17 00:00:00 2001 From: cyppe Date: Mon, 29 Dec 2025 18:32:25 +0100 Subject: [PATCH 4/4] review --- src/apprt/gtk/class/surface.zig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 22e08c598..cb5122314 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -3642,9 +3642,7 @@ const Clipboard = struct { // pass through when the clipboard contains non-text content (e.g., images). if (state == .paste) { const formats = clipboard.getFormats(); - // G_TYPE_STRING = G_TYPE_MAKE_FUNDAMENTAL(16) = (16 << 2) = 64 - const G_TYPE_STRING: usize = 64; - if (formats.containGtype(G_TYPE_STRING) == 0) { + if (formats.containGtype(gobject.ext.types.string) == 0) { log.debug("clipboard has no text format, not starting paste request", .{}); return false; }