mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-01-01 03:02:15 +00:00
gtk: pass through keypress when clipboard has no text (#10089)
## Summary When `paste_from_clipboard` is triggered but the clipboard contains no text (e.g., an image), the action now returns `false` to indicate it couldn't be performed. This enables the `performable:` keybind prefix to work correctly for paste actions. ## Problem On GTK/Linux, when a user has `keybind = ctrl+v=paste_from_clipboard` and the clipboard contains an image (not text), pressing Ctrl+V does nothing. Applications like `opencode` that handle their own clipboard reading via `wl-paste` never receive the keypress. ## Solution Make `clipboardRequest` return `bool` to indicate whether the action could be performed. For paste requests on GTK, synchronously check if the clipboard contains text formats before starting the async read. When no text format is available, return `false`. Users can now use: ``` keybind = performable:ctrl+v=paste_from_clipboard ``` When the clipboard has no text, the keybind is not consumed and Ctrl+V passes through to the terminal application. ## 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) ## Testing 1. Add `keybind = performable:ctrl+v=paste_from_clipboard` to config 2. Copy an image to clipboard 3. Open an application that handles image paste (e.g., `opencode`) 4. Press Ctrl+V 5. Image pastes successfully (app receives keypress and handles clipboard itself) ## Disclaimer Most of the changes is done with Opus 4.5
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,30 @@ 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.containGtype(gobject.ext.types.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 +3666,8 @@ const Clipboard = struct {
|
||||
clipboardReadText,
|
||||
ud,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Paste explicit text directly into the surface, regardless of the
|
||||
|
||||
Reference in New Issue
Block a user