macos: implement Kitty clipboard protocol reads (OSC 5522)

This commit is contained in:
Mitchell Hashimoto
2026-08-23 21:15:28 -07:00
parent a53771af01
commit 0ce9054bf9
14 changed files with 444 additions and 57 deletions

View File

@@ -86,8 +86,16 @@ typedef enum {
GHOSTTY_CLIPBOARD_REQUEST_PASTE,
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ,
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE,
GHOSTTY_CLIPBOARD_REQUEST_KITTY_READ,
} ghostty_clipboard_request_e;
// apprt.ClipboardReadResult
typedef enum {
GHOSTTY_CLIPBOARD_READ_STARTED,
GHOSTTY_CLIPBOARD_READ_UNAVAILABLE,
GHOSTTY_CLIPBOARD_READ_UNSUPPORTED,
} ghostty_clipboard_read_result_e;
typedef enum {
GHOSTTY_MOUSE_RELEASE,
GHOSTTY_MOUSE_PRESS,
@@ -1023,9 +1031,10 @@ typedef struct {
} ghostty_action_s;
typedef void (*ghostty_runtime_wakeup_cb)(void*);
typedef bool (*ghostty_runtime_read_clipboard_cb)(void*,
ghostty_clipboard_e,
void*);
typedef ghostty_clipboard_read_result_e (*ghostty_runtime_read_clipboard_cb)(
void*,
ghostty_clipboard_e,
void*);
typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
void*,
const char*,

View File

@@ -47,7 +47,7 @@ class ClipboardConfirmationController: NSWindowController {
switch confirmation.kind {
case .paste:
window.title = "Warning: Potentially Unsafe Paste"
case .osc_52_read, .osc_52_write:
case .osc_52_read, .osc_52_write, .kitty_read:
window.title = "Authorize Clipboard Access"
}

View File

@@ -15,11 +15,11 @@ struct ClipboardConfirmationView: View {
switch (action, reason) {
case (.cancel, .paste):
return "Cancel"
case (.cancel, .osc_52_read), (.cancel, .osc_52_write):
case (.cancel, .osc_52_read), (.cancel, .osc_52_write), (.cancel, .kitty_read):
return "Deny"
case (.confirm, .paste):
return "Paste"
case (.confirm, .osc_52_read), (.confirm, .osc_52_write):
case (.confirm, .osc_52_read), (.confirm, .osc_52_write), (.confirm, .kitty_read):
return "Allow"
}
}

View File

@@ -287,19 +287,24 @@ extension Ghostty {
_ userdata: UnsafeMutableRawPointer?,
location: ghostty_clipboard_e,
state: UnsafeMutableRawPointer?
) -> Bool {
) -> ghostty_clipboard_read_result_e {
let surfaceView = self.surfaceUserdata(from: userdata)
guard let surface = surfaceView.surface else { return false }
guard let surface = surfaceView.surface else {
return GHOSTTY_CLIPBOARD_READ_UNSUPPORTED
}
// Get our pasteboard
guard let pasteboard = NSPasteboard.ghostty(location) else { return false }
guard let pasteboard = NSPasteboard.ghostty(location) else {
return GHOSTTY_CLIPBOARD_READ_UNSUPPORTED
}
// Return false if there is no text-like clipboard content so
// performable paste bindings can pass through to the terminal.
guard let str = pasteboard.getOpinionatedStringContents() else { return false }
// We can only serve text-like clipboard contents.
guard let str = pasteboard.getOpinionatedStringContents() else {
return GHOSTTY_CLIPBOARD_READ_UNAVAILABLE
}
completeClipboardRequest(surface, data: str, state: state)
return true
return GHOSTTY_CLIPBOARD_READ_STARTED
}
static func confirmReadClipboard(
@@ -325,7 +330,7 @@ extension Ghostty {
guard let surface = surfaceView.surface else { return }
completeClipboardRequest(
surface,
data: contents ?? "",
data: contents,
state: state,
confirmed: true)
}
@@ -334,10 +339,16 @@ extension Ghostty {
private static func completeClipboardRequest(
_ surface: ghostty_surface_t,
data: String,
data: String?,
state: UnsafeMutableRawPointer?,
confirmed: Bool = false
) {
// Nil data denies the request.
guard let data else {
ghostty_surface_complete_clipboard_request(surface, nil, state, confirmed)
return
}
data.withCString { ptr in
ghostty_surface_complete_clipboard_request(surface, ptr, state, confirmed)
}

View File

@@ -13,6 +13,10 @@ extension Ghostty {
/// An application is attempting to write to the clipboard using OSC 52.
case osc_52_write
/// An application is attempting to read from the clipboard using
/// the Kitty clipboard protocol (OSC 5522).
case kitty_read
/// The text to show in the clipboard confirmation prompt for this request.
func text() -> String {
switch self {
@@ -20,7 +24,7 @@ extension Ghostty {
return """
Pasting this text to the terminal may be dangerous as it looks like some commands may be executed.
"""
case .osc_52_read:
case .osc_52_read, .kitty_read:
return """
An application is attempting to read from the clipboard.
The current clipboard contents are shown below.
@@ -41,6 +45,8 @@ extension Ghostty {
return .osc_52_read
case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE:
return .osc_52_write
case GHOSTTY_CLIPBOARD_REQUEST_KITTY_READ:
return .kitty_read
default:
return nil
}

View File

@@ -1061,6 +1061,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
_ = try self.startClipboardRequest(.standard, .{ .osc_52_read = clipboard });
},
.kitty_clipboard_read => |req| try self.kittyClipboardRead(req),
.clipboard_write => |w| switch (w.req) {
.small => |v| try self.clipboardWrite(v.data[0..v.len], w.clipboard_type),
.stable => |v| try self.clipboardWrite(v, w.clipboard_type),
@@ -5107,15 +5109,15 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
{},
),
.paste_from_clipboard => return try self.startClipboardRequest(
.paste_from_clipboard => return (try self.startClipboardRequest(
.standard,
.{ .paste = {} },
),
)) == .started,
.paste_from_selection => return try self.startClipboardRequest(
.paste_from_selection => return (try self.startClipboardRequest(
.selection,
.{ .paste = {} },
),
)) == .started,
.increase_font_size => |delta| {
// Max delta is somewhat arbitrary.
@@ -5879,20 +5881,64 @@ pub fn completeClipboardRequest(
.mime = "text/plain",
.data = data,
}}, !confirmed),
.kitty_read => |kitty| {
// If we need confirmation we return an error without
// consuming the request state; the apprt keeps it alive
// for the confirmation flow.
if (self.config.clipboard_read == .ask and !confirmed) {
return error.UnauthorizedPaste;
}
defer kitty.destroy();
try self.completeKittyClipboardRead(kitty, data);
},
}
}
/// Deny an in-flight clipboard request. This consumes the request: for
/// request types whose protocol expects an answer, the denial reply is
/// written to the pty.
pub fn denyClipboardRequest(self: *Surface, req: apprt.ClipboardRequest) void {
switch (req) {
// A denied paste simply doesn't happen.
.paste => {},
// OSC 52 has no error responses, but the client is waiting on
// a reply, so a denied read is answered with empty contents.
.osc_52_read => |clipboard| self.completeClipboardReadOSC52(
"",
clipboard,
true,
) catch |err| {
log.warn("error replying to OSC 52 clipboard read err={}", .{err});
},
// A denied write simply doesn't happen.
.osc_52_write => {},
// The Kitty clipboard protocol reports denial explicitly.
.kitty_read => |kitty| {
defer kitty.destroy();
self.kittyClipboardReadStatus(kitty, .EPERM) catch |err| {
log.warn("error replying to kitty clipboard read err={}", .{err});
};
},
}
}
/// 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.
/// The result reports whether the request was started; requests that
/// weren't started never complete. Callers own reacting to that, e.g.
/// performable paste keybinds pass through and Kitty clipboard reads
/// answer the program.
fn startClipboardRequest(
self: *Surface,
loc: apprt.Clipboard,
req: apprt.ClipboardRequest,
) !bool {
) !apprt.ClipboardReadResult {
switch (req) {
.paste => {}, // always allowed
.osc_52_read => if (self.config.clipboard_read == .deny) {
@@ -5900,9 +5946,13 @@ fn startClipboardRequest(
"application attempted to read clipboard, but 'clipboard-read' is set to deny",
.{},
);
return false;
return .unsupported;
},
// The clipboard-read policy was already applied by
// kittyClipboardRead, which owns replying on denial.
.kitty_read => {},
// No clipboard write code paths travel through this function
.osc_52_write => unreachable,
}
@@ -6034,6 +6084,116 @@ fn completeClipboardReadOSC52(
} }, .unlocked);
}
/// Handle a Kitty clipboard protocol (OSC 5522) read request forwarded
/// by the IO thread. This takes ownership of the request state.
fn kittyClipboardRead(
self: *Surface,
req: *apprt.ClipboardRequest.KittyRead,
) !void {
// A read denied by policy answers EPERM so clients degrade
// gracefully instead of waiting on a response that never comes.
if (self.config.clipboard_read == .deny) {
defer req.destroy();
log.info("application attempted to read clipboard, but 'clipboard-read' is set to deny", .{});
try self.kittyClipboardReadStatus(req, .EPERM);
return;
}
const result = self.startClipboardRequest(
req.location,
.{ .kitty_read = req },
) catch |err| {
defer req.destroy();
self.kittyClipboardReadStatus(req, .EIO) catch {};
return err;
};
switch (result) {
// The request completes asynchronously.
.started => {},
// The clipboard has nothing we can serve, which is a
// successful read that serves no representations. This never
// prompts even under an ask policy since there are no contents
// to disclose.
.unavailable => {
defer req.destroy();
try self.completeKittyClipboardRead(req, "");
},
// The apprt can't serve this clipboard at all, e.g. an
// unsupported primary selection.
.unsupported => {
defer req.destroy();
try self.kittyClipboardReadStatus(req, .ENOSYS);
},
}
}
/// Reply to a Kitty clipboard read with a single status packet.
fn kittyClipboardReadStatus(
self: *Surface,
req: *const apprt.ClipboardRequest.KittyRead,
status: terminal.kitty.clipboard.Status,
) !void {
var aw: std.Io.Writer.Allocating = .init(self.alloc);
defer aw.deinit();
try (terminal.kitty.clipboard.Response{
.op = .read,
.status = status,
.id = req.id,
.terminator = req.terminator,
}).encode(&aw.writer);
self.queueIo(.{ .write_alloc = .{
.alloc = self.alloc,
.data = try aw.toOwnedSlice(),
} }, .unlocked);
}
/// Complete a Kitty clipboard protocol read with the clipboard
/// contents.
fn completeKittyClipboardRead(
self: *Surface,
req: *const apprt.ClipboardRequest.KittyRead,
data: []const u8,
) !void {
const kitty_clipboard = terminal.kitty.clipboard;
// Serve the requested representations in request order. The apprt
// clipboard read path only carries text today, so the contents are
// served under every requested text MIME name; other types are
// simply never served, which is how the protocol communicates an
// unavailable representation.
var contents_buf: [kitty_clipboard.max_read_mimes]terminal.clipboard.Content = undefined;
var contents_len: usize = 0;
for (req.mimes) |mime| {
if (!terminal.clipboard.isTextMime(mime)) continue;
contents_buf[contents_len] = .{ .mime = mime, .data = data };
contents_len += 1;
}
// Encode the full success sequence: the OK packet, the targets
// listing if it was requested (reporting the canonical text type
// only when we have contents to serve), DATA chunks for each
// served representation, and the final DONE packet.
var aw: std.Io.Writer.Allocating = .init(self.alloc);
defer aw.deinit();
try (kitty_clipboard.ReadSuccess{
.primary = req.location == .primary,
.id = req.id,
.list = req.list,
.available = if (data.len > 0) &.{"text/plain"} else &.{},
.contents = contents_buf[0..contents_len],
.terminator = req.terminator,
}).encode(&aw.writer);
self.queueIo(.{ .write_alloc = .{
.alloc = self.alloc,
.data = try aw.toOwnedSlice(),
} }, .unlocked);
}
fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const u8) !void {
// Wyhash is used to hash the contents of the desktop notification to limit
// how fast identical notifications can be sent sequentially.

View File

@@ -27,6 +27,7 @@ pub const Target = action.Target;
pub const ContentScale = structs.ContentScale;
pub const Clipboard = structs.Clipboard;
pub const ClipboardContent = structs.ClipboardContent;
pub const ClipboardReadResult = structs.ClipboardReadResult;
pub const ClipboardRequest = structs.ClipboardRequest;
pub const ClipboardRequestType = structs.ClipboardRequestType;
pub const ColorScheme = structs.ColorScheme;

View File

@@ -52,11 +52,17 @@ pub const App = struct {
/// Callback called to handle an action.
action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.c) bool,
/// Read the clipboard value. Returns true if the clipboard request
/// was started and complete_clipboard_request may be called with the
/// given state pointer. Returns false if the clipboard request couldn't
/// be started (such as when no text is available for a paste request).
read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.c) bool,
/// Read the clipboard value. The result only reports facts about
/// the clipboard: whether the request was started (in which case
/// complete_clipboard_request must eventually be called with the
/// given state pointer), whether the clipboard has no servable
/// contents, or whether the clipboard can't be read at all. How
/// each non-started state is answered is up to the core.
read_clipboard: *const fn (
SurfaceUD,
c_int,
*apprt.ClipboardRequest,
) callconv(.c) apprt.ClipboardReadResult,
/// This may be called after a read clipboard call to request
/// confirmation that the clipboard value is safe to read. The embedder
@@ -692,7 +698,7 @@ pub const Surface = struct {
self: *Surface,
clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest,
) !bool {
) !apprt.ClipboardReadResult {
// 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
@@ -702,27 +708,32 @@ pub const Surface = struct {
errdefer alloc.destroy(state_ptr);
state_ptr.* = state;
const started = self.app.opts.read_clipboard(
const result = self.app.opts.read_clipboard(
self.userdata,
@intCast(@intFromEnum(clipboard_type)),
state_ptr,
);
if (!started) {
alloc.destroy(state_ptr);
return false;
}
return true;
// Only a started request completes later and keeps the state.
if (result != .started) alloc.destroy(state_ptr);
return result;
}
fn completeClipboardRequest(
self: *Surface,
str: [:0]const u8,
str_: ?[:0]const u8,
state: *apprt.ClipboardRequest,
confirmed: bool,
) void {
const alloc = self.app.core_app.alloc;
// No string means the request was denied by the user.
const str = str_ orelse {
self.core_surface.denyClipboardRequest(state.*);
alloc.destroy(state);
return;
};
// Attempt to complete the request, but we may request
// confirmation.
self.core_surface.completeClipboardRequest(
@@ -1998,14 +2009,18 @@ pub const CAPI = struct {
/// Complete a clipboard read request started 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.
///
/// A null string denies the request: request types whose protocol
/// expects an answer (e.g. Kitty clipboard protocol reads) have
/// their denial reply written to the pty.
export fn ghostty_surface_complete_clipboard_request(
ptr: *Surface,
str: [*:0]const u8,
str: ?[*:0]const u8,
state: *apprt.ClipboardRequest,
confirmed: bool,
) void {
ptr.completeClipboardRequest(
std.mem.sliceTo(str, 0),
if (str) |v| std.mem.sliceTo(v, 0) else null,
state,
confirmed,
);

View File

@@ -74,7 +74,7 @@ pub fn clipboardRequest(
self: *Self,
clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest,
) !bool {
) !apprt.ClipboardReadResult {
return try self.surface.clipboardRequest(
clipboard_type,
state,

View File

@@ -197,7 +197,7 @@ pub const ClipboardConfirmationDialog = extern struct {
self.as(Dialog.Parent).setHeading(i18n._("Authorize Clipboard Access"));
self.as(Dialog.Parent).setBody(i18n._("An application is attempting to write to the clipboard. The current clipboard contents are shown below."));
},
.osc_52_read => {
.osc_52_read, .kitty_read => {
self.as(Dialog.Parent).setHeading(i18n._("Authorize Clipboard Access"));
self.as(Dialog.Parent).setBody(i18n._("An application is attempting to read from the clipboard. The current clipboard contents are shown below."));
},

View File

@@ -1725,7 +1725,7 @@ pub const Surface = extern struct {
self: *Self,
clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest,
) !bool {
) !apprt.ClipboardReadResult {
return try Clipboard.request(
self,
clipboard_type,
@@ -4111,22 +4111,23 @@ 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).
/// Request data from the clipboard (read the clipboard). A started
/// request completes asynchronously and will call the
/// `completeClipboardRequest` core surface API when done.
pub fn request(
self: *Surface,
clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest,
) Allocator.Error!bool {
) Allocator.Error!apprt.ClipboardReadResult {
// The GTK apprt doesn't support Kitty clipboard protocol reads
// yet.
if (state == .kitty_read) return .unsupported;
// Get our requested clipboard
const clipboard = get(
self.private().gl_area.as(gtk.Widget),
clipboard_type,
) orelse return false;
) orelse return .unsupported;
// For paste requests, check if clipboard has text format available.
// This is a synchronous check that allows performable keybinds to
@@ -4135,7 +4136,7 @@ const Clipboard = struct {
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;
return .unavailable;
}
}
@@ -4158,7 +4159,7 @@ const Clipboard = struct {
ud,
);
return true;
return .started;
}
/// Paste explicit text directly into the surface, regardless of the
@@ -4224,7 +4225,7 @@ const Clipboard = struct {
.request = &req,
.@"can-remember" = switch (req) {
.osc_52_read, .osc_52_write => true,
.paste => false,
.paste, .kitty_read => false,
},
.@"clipboard-contents" = contents_buf,
},
@@ -4261,7 +4262,7 @@ const Clipboard = struct {
if (remember) switch (req.*) {
.osc_52_read => surface.config.clipboard_read = .allow,
.osc_52_write => surface.config.clipboard_write = .allow,
.paste => {},
.paste, .kitty_read => {},
};
// Get our text
@@ -4300,7 +4301,7 @@ const Clipboard = struct {
if (remember) switch (req.*) {
.osc_52_read => surface.config.clipboard_read = .deny,
.osc_52_write => surface.config.clipboard_write = .deny,
.paste => @panic("paste should not be able to be remembered"),
.paste, .kitty_read => @panic("request should not be able to be remembered"),
};
}

View File

@@ -1,4 +1,6 @@
const std = @import("std");
const build_config = @import("../build_config.zig");
const terminal = @import("../terminal/main.zig");
/// ContentScale is the ratio between the current DPI and the platform's
/// default DPI. This is used to determine how much certain rendered elements
@@ -67,6 +69,26 @@ pub const ClipboardRequestType = enum(u8) {
paste,
osc_52_read,
osc_52_write,
kitty_read,
};
/// The result of starting a clipboard read request. This only reports
/// facts about the clipboard; how each state is answered on the wire
/// (if at all) is up to the protocol handling of the requester.
///
/// If this is changed, you must also update ghostty.h
pub const ClipboardReadResult = enum(c_int) {
/// The request was started and will be completed asynchronously
/// via the core surface completeClipboardRequest API.
started = 0,
/// The clipboard exists but has no contents the apprt can serve
/// (e.g. no text-like data). The request was not started.
unavailable = 1,
/// The clipboard itself can't be read (e.g. a primary selection on
/// a platform without one). The request was not started.
unsupported = 2,
};
/// Clipboard request. This is used to request clipboard contents and must
@@ -81,6 +103,43 @@ pub const ClipboardRequest = union(ClipboardRequestType) {
/// A request to write clipboard contents via OSC 52.
osc_52_write: Clipboard,
/// A request to read clipboard contents via the Kitty clipboard
/// protocol (OSC 5522).
kitty_read: *KittyRead,
/// State for one in-flight Kitty clipboard protocol read. This is
/// created on the IO thread and completed on the app thread, so it
/// owns all of its memory: everything, including the struct itself,
/// is allocated from the arena.
pub const KittyRead = struct {
arena: std.heap.ArenaAllocator,
/// The clipboard being read. The protocol can only name the
/// standard clipboard or the primary selection.
location: Clipboard,
/// The requested MIME types in request order, already capped at
/// terminal.kitty.clipboard.max_read_mimes by the sender. Only
/// these representations may be served in the response.
mimes: []const []const u8,
/// True when the targets ('.') listing was requested.
list: bool,
/// The sanitized request id, echoed in every response packet.
id: []const u8,
/// The response terminator, matching the request's.
terminator: terminal.osc.Terminator,
pub fn destroy(self: *KittyRead) void {
// The struct itself lives in the arena, so move the arena
// out before tearing it down.
var arena = self.arena;
arena.deinit();
}
};
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk => @import("gobject").ext.defineBoxed(

View File

@@ -72,6 +72,11 @@ pub const Message = union(enum) {
/// Read the clipboard and write to the pty.
clipboard_read: apprt.Clipboard,
/// A Kitty clipboard protocol (OSC 5522) read request. The receiver
/// takes ownership of the request state and must eventually destroy
/// it.
kitty_clipboard_read: *apprt.ClipboardRequest.KittyRead,
/// Write the clipboard contents.
clipboard_write: struct {
clipboard_type: apprt.Clipboard,

View File

@@ -352,11 +352,11 @@ pub const StreamHandler = struct {
.apc_end => try self.apcEnd(),
.apc_put => self.apc.feed(self.alloc, value),
.apc_put_slice => self.apc.feedSlice(self.alloc, value.bytes),
.kitty_clipboard => try self.kittyClipboard(value),
// Unimplemented
.title_push,
.title_pop,
.kitty_clipboard,
.kitty_dnd,
=> {},
}
@@ -990,6 +990,126 @@ pub const StreamHandler = struct {
});
}
/// Handle one Kitty clipboard protocol (OSC 5522) packet.
fn kittyClipboard(
self: *StreamHandler,
v: terminal.osc.Command.KittyClipboardProtocol,
) !void {
const kitty_clipboard = terminal.kitty.clipboard;
// Decode and validate the metadata. Malformed metadata drops
// the packet without any response, matching kitty.
var arena: std.heap.ArenaAllocator = .init(self.alloc);
defer arena.deinit();
const meta = (try kitty_clipboard.Metadata.parse(
arena.allocator(),
v.metadata,
)) orelse return;
switch (meta.op) {
.read => try self.kittyClipboardRead(
&meta,
v.payload orelse "",
v.terminator,
),
// Writes aren't implemented in the GUI yet. Failing the
// transaction up front matches a libghostty-vt handler
// without a clipboard_write effect and spares the program
// from waiting on a commit response that never comes.
.write => {
var stream: std.Io.Writer.Allocating = .init(self.alloc);
defer stream.deinit();
try (kitty_clipboard.Response{
.op = .write,
.status = .ENOSYS,
.id = meta.id,
.terminator = v.terminator,
}).encode(&stream.writer);
self.messageWriter(.{ .write_alloc = .{
.alloc = self.alloc,
.data = try stream.toOwnedSlice(),
} });
},
// Data packets without an accepted write transaction are
// silently ignored, matching kitty.
.wdata, .walias => {},
}
}
fn kittyClipboardRead(
self: *StreamHandler,
meta: *const terminal.kitty.clipboard.Metadata,
payload: []const u8,
terminator: terminal.osc.Terminator,
) !void {
const kitty_clipboard = terminal.kitty.clipboard;
// Everything about the request, including the request struct
// itself, lives in a single arena that crosses to the surface
// thread, which owns it from the moment the message is sent.
var arena: std.heap.ArenaAllocator = .init(self.alloc);
errdefer arena.deinit();
const alloc = arena.allocator();
// The payload is the requested MIME list. A read request with
// an undecodable payload is dropped without any response,
// matching kitty.
const decoded = kitty_clipboard.Payload.init(
alloc,
payload,
) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.Invalid => {
arena.deinit();
return;
},
};
// The targets type ('.') asks for the listing of available
// types rather than data. Requested types beyond the cap are
// dropped and simply never served, which is how the protocol
// reports an unavailable type anyway. The MIME slices point
// into the decoded payload, which shares the request arena.
var mimes_buf: [kitty_clipboard.max_read_mimes][]const u8 = undefined;
var mimes_len: usize = 0;
var list = false;
var it = decoded.mimeIterator();
while (it.next()) |mime| {
if (std.mem.eql(u8, mime, kitty_clipboard.targets_mime)) {
list = true;
continue;
}
if (mimes_len == mimes_buf.len) continue;
mimes_buf[mimes_len] = mime;
mimes_len += 1;
}
// Note: session grants (the pw/name metadata) aren't
// implemented in the GUI clipboard path yet, so every request
// goes through the configured clipboard-read policy.
const req = try alloc.create(apprt.ClipboardRequest.KittyRead);
const mimes = try alloc.dupe([]const u8, mimes_buf[0..mimes_len]);
const id = try alloc.dupe(u8, meta.id);
req.* = .{
// The arena must be copied in last so it tracks every
// allocation above.
.arena = arena,
.location = switch (meta.loc) {
.primary => .primary,
else => .standard,
},
.mimes = mimes,
.list = list,
.id = id,
.terminator = terminator,
};
self.surfaceMessageWriter(.{ .kitty_clipboard_read = req });
}
fn semanticPrompt(
self: *StreamHandler,
cmd: Stream.Action.SemanticPrompt,