mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-18 21:40:29 +00:00
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:
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user