mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
libghostty: Kitty clipboard write permission prompts and grants
The `clipboard_write` effect now is similar to read: it must response to a "reply" callback synchronously. This lets the embedder ask for write permission, too. We also now pass through program name and grant information from Kitty clipboard protocol so that embedders can use that if they want. This is a breaking ABI change.
This commit is contained in:
@@ -40,13 +40,17 @@ void on_title_changed(GhosttyTerminal terminal, void* userdata) {
|
||||
//! [effects-title-changed]
|
||||
|
||||
//! [effects-clipboard-write]
|
||||
GhosttyClipboardWriteResult on_clipboard_write(
|
||||
void on_clipboard_write(
|
||||
GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const GhosttyClipboardWrite* write) {
|
||||
(void)terminal;
|
||||
(void)userdata;
|
||||
|
||||
// The write is synchronous: a real embedder would ask the user for
|
||||
// permission here (unless write->granted) and the VT stream waits until
|
||||
// this callback returns. The replied result is sent to the program
|
||||
// (OSC 5522) through the write_pty callback.
|
||||
printf(" clipboard write (location=%d, contents=%zu)\n",
|
||||
(int)write->location, write->contents_len);
|
||||
if (write->contents_len == 0) {
|
||||
@@ -66,7 +70,12 @@ GhosttyClipboardWriteResult on_clipboard_write(
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
return GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS;
|
||||
GhosttyClipboardWriteReply reply = {
|
||||
.size = sizeof(reply),
|
||||
.result = GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS,
|
||||
.remember = false,
|
||||
};
|
||||
write->reply(write, &reply);
|
||||
}
|
||||
//! [effects-clipboard-write]
|
||||
|
||||
|
||||
@@ -463,41 +463,7 @@ typedef struct {
|
||||
} GhosttyClipboardContent;
|
||||
|
||||
/**
|
||||
* A semantic, atomic clipboard write.
|
||||
*
|
||||
* This is a sized struct. The callback must only access fields present in the
|
||||
* size reported by `size`. The request, contents array, MIME strings, and
|
||||
* data strings are all borrowed and valid only for the callback duration.
|
||||
*
|
||||
* All entries in `contents` are representations of the same logical value
|
||||
* and must be committed atomically. A `contents_len` of zero requests that
|
||||
* the destination be cleared. This is distinct from a content entry whose data
|
||||
* has zero length.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. */
|
||||
size_t size;
|
||||
|
||||
/** Clipboard destination. */
|
||||
GhosttyClipboardLocation location;
|
||||
|
||||
/** Borrowed array of MIME representations. */
|
||||
const GhosttyClipboardContent* contents;
|
||||
|
||||
/** Number of entries in contents; zero means clear the destination. */
|
||||
size_t contents_len;
|
||||
} GhosttyClipboardWrite;
|
||||
|
||||
/**
|
||||
* Result of a clipboard write callback.
|
||||
*
|
||||
* Protocols without write acknowledgements, including OSC 52 and iTerm2
|
||||
* OSC 1337 Copy, ignore this result. The Kitty clipboard protocol
|
||||
* (OSC 5522) acknowledges writes: each result maps to the corresponding
|
||||
* protocol status (DONE, EPERM, ENOSYS, EBUSY, EINVAL, EIO) and is
|
||||
* reported back to the running program through the write_pty callback.
|
||||
* Result of a clipboard write reply.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
@@ -522,21 +488,126 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
GHOSTTY_CLIPBOARD_WRITE_RESULT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyClipboardWriteResult;
|
||||
|
||||
/**
|
||||
* The reply to a clipboard write request.
|
||||
*
|
||||
* This is a sized struct; set `size` to `sizeof(GhosttyClipboardWriteReply)`.
|
||||
* The reply is borrowed only for the duration of the reply call and may be
|
||||
* freed as soon as it returns.
|
||||
*
|
||||
* The result answers the program with the matching protocol status for
|
||||
* protocols with a write acknowledgement (OSC 5522: DONE, EPERM, ENOSYS,
|
||||
* EBUSY, EINVAL, EIO); protocols without one (OSC 52, OSC 1337 Copy)
|
||||
* discard the reply. `remember` is ignored on any result other than
|
||||
* GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. */
|
||||
size_t size;
|
||||
|
||||
/** Outcome of the write. */
|
||||
GhosttyClipboardWriteResult result;
|
||||
|
||||
/**
|
||||
* Record a session grant so future requests from the same program skip
|
||||
* the permission prompt. Only honored on success when
|
||||
* GhosttyClipboardWrite::can_remember is set.
|
||||
*/
|
||||
bool remember;
|
||||
} GhosttyClipboardWriteReply;
|
||||
|
||||
typedef struct GhosttyClipboardWrite GhosttyClipboardWrite;
|
||||
|
||||
/**
|
||||
* Function type used to answer a clipboard write request. Obtained from
|
||||
* GhosttyClipboardWrite::reply; see that struct for the contract.
|
||||
*
|
||||
* @param write The request being answered
|
||||
* @param reply The reply, borrowed only for the duration of this call
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef void (*GhosttyClipboardWriteReplyFn)(
|
||||
const GhosttyClipboardWrite* write,
|
||||
const GhosttyClipboardWriteReply* reply);
|
||||
|
||||
/**
|
||||
* A synchronous request to write clipboard contents.
|
||||
*
|
||||
* This is a sized struct. The callback must only access fields present in the
|
||||
* size reported by `size`. The request, contents array, MIME strings, and
|
||||
* data strings are all borrowed and valid only for the callback duration.
|
||||
*
|
||||
* All entries in `contents` are representations of the same logical value
|
||||
* and must be committed atomically. A `contents_len` of zero requests that
|
||||
* the destination be cleared. This is distinct from a content entry whose data
|
||||
* has zero length.
|
||||
*
|
||||
* The write is answered by calling `reply` with this request and a
|
||||
* GhosttyClipboardWriteReply. This must happen within the clipboard write
|
||||
* request callback. This struct is only valid during that time. Calling
|
||||
* `reply` more than once is safely ignored. Returning without replying
|
||||
* denies the write.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
struct GhosttyClipboardWrite {
|
||||
/** Size of this struct in bytes. */
|
||||
size_t size;
|
||||
|
||||
/** Clipboard destination. */
|
||||
GhosttyClipboardLocation location;
|
||||
|
||||
/** Borrowed array of MIME representations. */
|
||||
const GhosttyClipboardContent* contents;
|
||||
|
||||
/** Number of entries in contents; zero means clear the destination. */
|
||||
size_t contents_len;
|
||||
|
||||
/**
|
||||
* Name of the writing program for permission prompts, if the protocol
|
||||
* carries one. Empty otherwise.
|
||||
*/
|
||||
GhosttyString name;
|
||||
|
||||
/**
|
||||
* True if the terminal already holds a session grant for this request
|
||||
* The embedder should skip any permission prompt and perform the write.
|
||||
*/
|
||||
bool granted;
|
||||
|
||||
/**
|
||||
* True if the program supplied a session password, so the embedder may
|
||||
* offer to remember the user's decision through
|
||||
* GhosttyClipboardWriteReply::remember. When false, remember is ignored.
|
||||
*/
|
||||
bool can_remember;
|
||||
|
||||
/** Terminal-owned reply state. Do not access. */
|
||||
const void* ctx;
|
||||
|
||||
/** Answer the write; see the struct documentation. */
|
||||
GhosttyClipboardWriteReplyFn reply;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback function type for clipboard_write.
|
||||
*
|
||||
* Called synchronously for a complete logical clipboard write. Protocol
|
||||
* details such as OSC 52 selectors, base64 encoding, multipart chunks,
|
||||
* aliases, and terminators are normalized before this callback is invoked.
|
||||
* OSC 52, iTerm2 OSC 1337 Copy, and Kitty clipboard (OSC 5522) writes
|
||||
* therefore use the same callback shape.
|
||||
* The embedder may ask for permission to write or perform the write
|
||||
* async, but the callback itself is synchronous and the reply function
|
||||
* must be called during the lifetime of this function. While this callback
|
||||
* is active the VT stream is paused.
|
||||
*
|
||||
* Every invocation is one complete write: the contents replace whatever
|
||||
* the destination previously held, so there is never a partial update to
|
||||
* detect or a reset to perform. A Kitty clipboard write transaction
|
||||
* results in exactly one invocation, at commit, carrying all of the
|
||||
* transaction's MIME representations together; its protocol response is
|
||||
* generated automatically from the returned result.
|
||||
* Answer by calling `write->reply(write, &reply)` before returning. See
|
||||
* GhosttyClipboardWrite for the full contract.
|
||||
*
|
||||
* The request may carry an optional program name requesting the write
|
||||
* and the state of prior permission granted. If `can_remember` is set
|
||||
* the response may set the `remember` flag and future requests from this
|
||||
* same program will be "granted" and the embedder can skip permission
|
||||
* requests.
|
||||
*
|
||||
* Clipboard read requests (OSC 52 "?" and OSC 5522 reads) are delivered
|
||||
* to GhosttyTerminalClipboardReadFn instead.
|
||||
@@ -544,11 +615,10 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @param write Borrowed atomic clipboard write request
|
||||
* @return The result of attempting the clipboard write
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef GhosttyClipboardWriteResult (*GhosttyTerminalClipboardWriteFn)(
|
||||
typedef void (*GhosttyTerminalClipboardWriteFn)(
|
||||
GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const GhosttyClipboardWrite* write);
|
||||
@@ -558,7 +628,7 @@ typedef GhosttyClipboardWriteResult (*GhosttyTerminalClipboardWriteFn)(
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef enum {
|
||||
typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/** The clipboard was read; the reply carries its contents. */
|
||||
GHOSTTY_CLIPBOARD_READ_RESULT_SUCCESS = 0,
|
||||
|
||||
|
||||
@@ -129,6 +129,8 @@ pub const ClipboardContent = extern struct {
|
||||
};
|
||||
|
||||
/// A protocol-neutral request to replace or clear clipboard contents.
|
||||
/// The embedder answers by calling `reply` with the request before the
|
||||
/// callback returns.
|
||||
///
|
||||
/// C: GhosttyClipboardWrite
|
||||
pub const ClipboardWrite = extern struct {
|
||||
@@ -136,8 +138,28 @@ pub const ClipboardWrite = extern struct {
|
||||
location: clipboard.Location,
|
||||
contents: ?[*]const ClipboardContent,
|
||||
contents_len: usize,
|
||||
name: lib.String,
|
||||
granted: bool,
|
||||
can_remember: bool,
|
||||
/// Terminal-owned reply state; opaque to the embedder.
|
||||
ctx: *const anyopaque,
|
||||
reply: ClipboardWriteReplyFn,
|
||||
};
|
||||
|
||||
/// The reply to a clipboard write request.
|
||||
///
|
||||
/// C: GhosttyClipboardWriteReply
|
||||
pub const ClipboardWriteReply = extern struct {
|
||||
size: usize,
|
||||
result: clipboard.Write.Status,
|
||||
remember: bool,
|
||||
};
|
||||
|
||||
/// C function pointer type for replying to a clipboard write.
|
||||
///
|
||||
/// C: GhosttyClipboardWriteReplyFn
|
||||
pub const ClipboardWriteReplyFn = *const fn (*const ClipboardWrite, *const ClipboardWriteReply) callconv(lib.calling_conv) void;
|
||||
|
||||
/// The reply to a clipboard read request.
|
||||
///
|
||||
/// C: GhosttyClipboardReadReply
|
||||
@@ -293,8 +315,9 @@ const Effects = struct {
|
||||
pub const XtversionFn = *const fn (Terminal, ?*anyopaque) callconv(lib.calling_conv) lib.String;
|
||||
|
||||
/// C function pointer type for the clipboard_write callback. The request
|
||||
/// and its contents are borrowed and only valid for the callback duration.
|
||||
pub const ClipboardWriteFn = *const fn (Terminal, ?*anyopaque, *const ClipboardWrite) callconv(lib.calling_conv) clipboard.WriteResult;
|
||||
/// is borrowed for the callback duration and must be answered through
|
||||
/// its reply function before the callback returns.
|
||||
pub const ClipboardWriteFn = *const fn (Terminal, ?*anyopaque, *const ClipboardWrite) callconv(lib.calling_conv) void;
|
||||
|
||||
/// C function pointer type for the clipboard_read callback. The request
|
||||
/// is borrowed for the callback duration and must be answered through
|
||||
@@ -365,9 +388,15 @@ const Effects = struct {
|
||||
func(@ptrCast(wrapper), wrapper.effects.userdata);
|
||||
}
|
||||
|
||||
fn clipboardWriteTrampoline(handler: *Handler, write: clipboard.Write) clipboard.WriteResult {
|
||||
/// Opaque context behind ClipboardWrite.ctx for the reply trampoline.
|
||||
const ClipboardWriteCtx = struct {
|
||||
write: clipboard.Write,
|
||||
};
|
||||
|
||||
fn clipboardWriteTrampoline(handler: *Handler, write: clipboard.Write) void {
|
||||
const wrapper = TerminalWrapper.fromHandler(handler);
|
||||
const func = wrapper.effects.clipboard_write orelse return .unsupported;
|
||||
const func = wrapper.effects.clipboard_write orelse
|
||||
return write.reply(.unsupported);
|
||||
|
||||
// Most protocols currently produce one representation, so keep that
|
||||
// path allocation-free while supporting arbitrary multi-MIME writes.
|
||||
@@ -376,7 +405,7 @@ const Effects = struct {
|
||||
stack_contents[0..write.contents.len]
|
||||
else
|
||||
wrapper.terminal.gpa().alloc(ClipboardContent, write.contents.len) catch
|
||||
return .io_error;
|
||||
return write.reply(.io_error);
|
||||
defer if (write.contents.len > stack_contents.len)
|
||||
wrapper.terminal.gpa().free(contents);
|
||||
|
||||
@@ -393,13 +422,35 @@ const Effects = struct {
|
||||
};
|
||||
}
|
||||
|
||||
const ctx: ClipboardWriteCtx = .{ .write = write };
|
||||
const request: ClipboardWrite = .{
|
||||
.size = @sizeOf(ClipboardWrite),
|
||||
.location = write.location,
|
||||
.contents = if (contents.len > 0) contents.ptr else null,
|
||||
.contents_len = contents.len,
|
||||
.name = .init(write.name),
|
||||
.granted = write.granted,
|
||||
.can_remember = write.can_remember,
|
||||
.ctx = &ctx,
|
||||
.reply = &clipboardWriteReplyTrampoline,
|
||||
};
|
||||
return func(@ptrCast(wrapper), wrapper.effects.userdata, &request);
|
||||
func(@ptrCast(wrapper), wrapper.effects.userdata, &request);
|
||||
}
|
||||
|
||||
fn clipboardWriteReplyTrampoline(
|
||||
request: *const ClipboardWrite,
|
||||
reply: *const ClipboardWriteReply,
|
||||
) callconv(lib.calling_conv) void {
|
||||
const ctx: *const ClipboardWriteCtx = @ptrCast(@alignCast(request.ctx));
|
||||
const write = ctx.write;
|
||||
switch (reply.result) {
|
||||
.success => write.reply(.{ .success = .{ .remember = reply.remember } }),
|
||||
.denied => write.reply(.denied),
|
||||
.busy => write.reply(.busy),
|
||||
.invalid_data => write.reply(.invalid_data),
|
||||
.io_error => write.reply(.io_error),
|
||||
.unsupported, _ => write.reply(.unsupported),
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque context behind ClipboardRead.ctx for the reply trampoline.
|
||||
@@ -4607,13 +4658,16 @@ test "set clipboard_write callback" {
|
||||
var last_mime_lens: [8]usize = @splat(0);
|
||||
var last_data: [8][64]u8 = undefined;
|
||||
var last_data_lens: [8]usize = @splat(0);
|
||||
var next_result: clipboard.WriteResult = .success;
|
||||
var last_name_len: usize = 0;
|
||||
var last_granted: bool = true;
|
||||
var last_can_remember: bool = true;
|
||||
var next_result: clipboard.Write.Status = .success;
|
||||
|
||||
fn clipboardWrite(
|
||||
terminal_: Terminal,
|
||||
ud: ?*anyopaque,
|
||||
request: *const ClipboardWrite,
|
||||
) callconv(lib.calling_conv) clipboard.WriteResult {
|
||||
) callconv(lib.calling_conv) void {
|
||||
count += 1;
|
||||
last_terminal = terminal_;
|
||||
last_userdata = ud;
|
||||
@@ -4621,6 +4675,9 @@ test "set clipboard_write callback" {
|
||||
last_location = request.location;
|
||||
last_contents_null = request.contents == null;
|
||||
last_contents_len = request.contents_len;
|
||||
last_name_len = request.name.len;
|
||||
last_granted = request.granted;
|
||||
last_can_remember = request.can_remember;
|
||||
|
||||
if (request.contents) |ptr| {
|
||||
for (ptr[0..@min(request.contents_len, last_mimes.len)], 0..) |content, i| {
|
||||
@@ -4638,7 +4695,11 @@ test "set clipboard_write callback" {
|
||||
}
|
||||
}
|
||||
|
||||
return next_result;
|
||||
request.reply(request, &.{
|
||||
.size = @sizeOf(ClipboardWriteReply),
|
||||
.result = next_result,
|
||||
.remember = false,
|
||||
});
|
||||
}
|
||||
};
|
||||
S.count = 0;
|
||||
@@ -4665,6 +4726,11 @@ test "set clipboard_write callback" {
|
||||
try testing.expectEqualStrings("text/plain", S.last_mimes[0][0..S.last_mime_lens[0]]);
|
||||
try testing.expectEqualSlices(u8, "hello\x00world", S.last_data[0][0..S.last_data_lens[0]]);
|
||||
|
||||
// OSC 52 carries no program identity or password grant state.
|
||||
try testing.expectEqual(@as(usize, 0), S.last_name_len);
|
||||
try testing.expect(!S.last_granted);
|
||||
try testing.expect(!S.last_can_remember);
|
||||
|
||||
// OSC 52 destinations are normalized rather than exposed as wire bytes.
|
||||
const location_cases = [_]struct {
|
||||
selector: u8,
|
||||
@@ -4704,8 +4770,9 @@ test "set clipboard_write callback" {
|
||||
try testing.expectEqualStrings("text/plain", S.last_mimes[0][0..S.last_mime_lens[0]]);
|
||||
try testing.expectEqualStrings("iTerm", S.last_data[0][0..S.last_data_lens[0]]);
|
||||
|
||||
// Every representation is converted, and callback results propagate
|
||||
// through the C trampoline for protocols that can acknowledge writes.
|
||||
// Every representation is converted, and callback replies propagate
|
||||
// back through the C trampoline for protocols that can acknowledge
|
||||
// writes.
|
||||
const internal_contents = [_]clipboard.Content{
|
||||
.{ .mime = "text/plain", .data = "plain" },
|
||||
.{ .mime = "application/octet-stream", .data = "a\x00b" },
|
||||
@@ -4713,13 +4780,27 @@ test "set clipboard_write callback" {
|
||||
.{ .mime = "text/rtf", .data = "{\\rtf1 plain}" },
|
||||
.{ .mime = "image/png", .data = "\x89PNG" },
|
||||
};
|
||||
const Reply = struct {
|
||||
var last: ?clipboard.Write.Result = null;
|
||||
fn reply(_: *anyopaque, result: clipboard.Write.Result) void {
|
||||
last = result;
|
||||
}
|
||||
};
|
||||
S.next_result = .busy;
|
||||
const handler = &t.?.stream.handler;
|
||||
const write_result = handler.effects.clipboard_write.?(handler, .{
|
||||
handler.effects.clipboard_write.?(handler, .{
|
||||
.location = .primary,
|
||||
.contents = &internal_contents,
|
||||
.name = "app",
|
||||
.granted = true,
|
||||
.can_remember = true,
|
||||
.reply_ctx = handler,
|
||||
.reply_fn = &Reply.reply,
|
||||
});
|
||||
try testing.expectEqual(clipboard.WriteResult.busy, write_result);
|
||||
try testing.expect(Reply.last.? == .busy);
|
||||
try testing.expectEqual(@as(usize, 3), S.last_name_len);
|
||||
try testing.expect(S.last_granted);
|
||||
try testing.expect(S.last_can_remember);
|
||||
try testing.expectEqual(@as(usize, 7), S.count);
|
||||
try testing.expectEqual(@as(usize, 5), S.last_contents_len);
|
||||
try testing.expectEqualStrings(
|
||||
@@ -4779,6 +4860,9 @@ test "kitty clipboard write via C effects" {
|
||||
var last_mime_lens: [4]usize = @splat(0);
|
||||
var last_data: [4][64]u8 = undefined;
|
||||
var last_data_lens: [4]usize = @splat(0);
|
||||
var last_granted: bool = true;
|
||||
var last_can_remember: bool = true;
|
||||
var next_remember: bool = false;
|
||||
|
||||
fn writePty(
|
||||
_: Terminal,
|
||||
@@ -4794,7 +4878,7 @@ test "kitty clipboard write via C effects" {
|
||||
_: Terminal,
|
||||
_: ?*anyopaque,
|
||||
request: *const ClipboardWrite,
|
||||
) callconv(lib.calling_conv) clipboard.WriteResult {
|
||||
) callconv(lib.calling_conv) void {
|
||||
write_count += 1;
|
||||
last_location = request.location;
|
||||
last_contents_len = request.contents_len;
|
||||
@@ -4812,13 +4896,22 @@ test "kitty clipboard write via C effects" {
|
||||
);
|
||||
}
|
||||
}
|
||||
return .success;
|
||||
last_granted = request.granted;
|
||||
last_can_remember = request.can_remember;
|
||||
request.reply(request, &.{
|
||||
.size = @sizeOf(ClipboardWriteReply),
|
||||
.result = .success,
|
||||
.remember = next_remember,
|
||||
});
|
||||
}
|
||||
};
|
||||
S.responses_len = 0;
|
||||
S.write_count = 0;
|
||||
S.last_mime_lens = @splat(0);
|
||||
S.last_data_lens = @splat(0);
|
||||
S.last_granted = true;
|
||||
S.last_can_remember = true;
|
||||
S.next_remember = false;
|
||||
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
||||
try testing.expectEqual(Result.success, set(t, .clipboard_write, @ptrCast(&S.clipboardWrite)));
|
||||
@@ -4846,12 +4939,36 @@ test "kitty clipboard write via C effects" {
|
||||
"\x1B]5522;type=write:status=DONE:id=c1\x1B\\",
|
||||
S.responses[0..S.responses_len],
|
||||
);
|
||||
try testing.expect(!S.last_granted);
|
||||
try testing.expect(!S.last_can_remember);
|
||||
|
||||
// Password grants round-trip through the C reply: the first pw'd
|
||||
// commit isn't granted and asks to remember, so the next one
|
||||
// arrives granted.
|
||||
S.responses_len = 0;
|
||||
S.next_remember = true;
|
||||
const grant_seqs = [_][]const u8{
|
||||
"\x1B]5522;type=write:id=g1:pw=c2VjcmV0:name=YXBw\x1B\\",
|
||||
"\x1B]5522;type=wdata\x1B\\",
|
||||
"\x1B]5522;type=write:id=g2:pw=c2VjcmV0:name=YXBw\x1B\\",
|
||||
"\x1B]5522;type=wdata\x1B\\",
|
||||
};
|
||||
for (grant_seqs) |seq| vt_write(t, seq.ptr, seq.len);
|
||||
try testing.expectEqual(@as(usize, 3), S.write_count);
|
||||
try testing.expect(S.last_granted);
|
||||
try testing.expect(S.last_can_remember);
|
||||
try testing.expectEqualStrings(
|
||||
"\x1B]5522;type=write:status=DONE:id=g1\x1B\\" ++
|
||||
"\x1B]5522;type=write:status=DONE:id=g2\x1B\\",
|
||||
S.responses[0..S.responses_len],
|
||||
);
|
||||
S.next_remember = false;
|
||||
|
||||
// Without a read callback reads are denied.
|
||||
S.responses_len = 0;
|
||||
const read = "\x1B]5522;type=read:id=r1;dGV4dC9wbGFpbg==\x1B\\";
|
||||
vt_write(t, read, read.len);
|
||||
try testing.expectEqual(@as(usize, 1), S.write_count);
|
||||
try testing.expectEqual(@as(usize, 3), S.write_count);
|
||||
try testing.expectEqualStrings(
|
||||
"\x1B]5522;type=read:status=EPERM:id=r1\x1B\\",
|
||||
S.responses[0..S.responses_len],
|
||||
|
||||
@@ -178,6 +178,7 @@ const type_decls = [_]TypeDecl{
|
||||
.initStruct("GhosttyClipboardRead", terminal.ClipboardRead),
|
||||
.initStruct("GhosttyClipboardReadReply", terminal.ClipboardReadReply),
|
||||
.initStruct("GhosttyClipboardWrite", terminal.ClipboardWrite),
|
||||
.initStruct("GhosttyClipboardWriteReply", terminal.ClipboardWriteReply),
|
||||
.initStruct("GhosttyCodepoints", Codepoints),
|
||||
.initStruct("GhosttyColorPaletteMask", color_c.PaletteMask),
|
||||
.initStruct("GhosttyColorRgb", color.RGB.C),
|
||||
@@ -247,7 +248,7 @@ const type_decls = [_]TypeDecl{
|
||||
.initEnum("GhosttyCellWide", cell.Wide, "GHOSTTY_CELL_WIDE_"),
|
||||
.initEnum("GhosttyClipboardLocation", clipboard.Location, "GHOSTTY_CLIPBOARD_LOCATION_"),
|
||||
.initEnum("GhosttyClipboardReadResult", clipboard.Read.Status, "GHOSTTY_CLIPBOARD_READ_RESULT_"),
|
||||
.initEnum("GhosttyClipboardWriteResult", clipboard.WriteResult, "GHOSTTY_CLIPBOARD_WRITE_RESULT_"),
|
||||
.initEnum("GhosttyClipboardWriteResult", clipboard.Write.Status, "GHOSTTY_CLIPBOARD_WRITE_RESULT_"),
|
||||
.initEnum("GhosttyColorScheme", device_status.ColorScheme, "GHOSTTY_COLOR_SCHEME_"),
|
||||
.initEnum("GhosttyFocusEvent", focus_pkg.Event, "GHOSTTY_FOCUS_"),
|
||||
.initEnum("GhosttyFormatterFormat", formatter_pkg.Format, "GHOSTTY_FORMATTER_FORMAT_"),
|
||||
|
||||
@@ -69,24 +69,82 @@ pub const MimeReader = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// One atomic clipboard write.
|
||||
/// A request from the running program to write a clipboard.
|
||||
///
|
||||
/// Contents are borrowed and only valid for the duration of a clipboard write
|
||||
/// callback. An empty contents slice clears the destination.
|
||||
/// Writes are synchronous: the effect callback must answer through `reply`
|
||||
/// before it returns, and the request (including its reply context) is
|
||||
/// invalid afterwards. An embedder that needs user consent must block until
|
||||
/// it has an answer; the VT stream waits with it. Protocols without a write
|
||||
/// acknowledgement (OSC 52, OSC 1337 Copy) discard the reply.
|
||||
///
|
||||
/// Contents are borrowed and only valid for the duration of the callback.
|
||||
/// An empty contents slice clears the destination.
|
||||
pub const Write = struct {
|
||||
location: Location,
|
||||
contents: []const Content,
|
||||
};
|
||||
|
||||
/// The result of a clipboard write.
|
||||
pub const WriteResult = enum(c_int) {
|
||||
success = 0,
|
||||
denied = 1,
|
||||
unsupported = 2,
|
||||
busy = 3,
|
||||
invalid_data = 4,
|
||||
io_error = 5,
|
||||
_,
|
||||
/// Name of the writing program for permission prompts, if the
|
||||
/// protocol carries one. Empty otherwise.
|
||||
name: []const u8,
|
||||
|
||||
/// True if the terminal already holds a session grant for this
|
||||
/// request (kitty clipboard protocol passwords). The embedder should
|
||||
/// skip any permission prompt and perform the write.
|
||||
granted: bool,
|
||||
|
||||
/// True if the program supplied a session password, so the embedder
|
||||
/// may offer to remember the user's decision via
|
||||
/// Result.Success.remember. When false, remember is ignored.
|
||||
can_remember: bool,
|
||||
|
||||
/// Terminal-owned reply state, only valid during the callback.
|
||||
reply_ctx: *anyopaque,
|
||||
reply_fn: *const fn (*anyopaque, Result) void,
|
||||
|
||||
/// Answer the write. May be called at most once; later calls are
|
||||
/// ignored.
|
||||
pub fn reply(self: Write, result: Result) void {
|
||||
self.reply_fn(self.reply_ctx, result);
|
||||
}
|
||||
|
||||
/// The status of a clipboard write reply.
|
||||
pub const Status = enum(c_int) {
|
||||
success = 0,
|
||||
denied = 1,
|
||||
unsupported = 2,
|
||||
busy = 3,
|
||||
invalid_data = 4,
|
||||
io_error = 5,
|
||||
_,
|
||||
};
|
||||
|
||||
/// The reply to a clipboard write.
|
||||
pub const Result = union(enum) {
|
||||
/// The write was denied by policy or the user.
|
||||
denied,
|
||||
|
||||
/// The embedder cannot write this clipboard.
|
||||
unsupported,
|
||||
|
||||
/// The clipboard is temporarily unavailable.
|
||||
busy,
|
||||
|
||||
/// One or more representations contain invalid data.
|
||||
invalid_data,
|
||||
|
||||
/// Writing the clipboard failed.
|
||||
io_error,
|
||||
|
||||
/// The write succeeded.
|
||||
success: Success,
|
||||
|
||||
pub const Success = struct {
|
||||
/// Record a session grant so future requests from the same
|
||||
/// program skip the permission prompt. Only honored when the
|
||||
/// request set `can_remember`.
|
||||
remember: bool = false,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/// A request from the running program to read a clipboard.
|
||||
|
||||
@@ -81,9 +81,8 @@ pub const Handler = struct {
|
||||
kitty_clipboard_write: ?*kitty_clipboard.WriteState = null,
|
||||
|
||||
/// Kitty clipboard protocol (OSC 5522) session password grants,
|
||||
/// recorded when a clipboard_read reply asks to remember the user's
|
||||
/// decision. Later requests carrying a granted password are forwarded
|
||||
/// with `granted` set so the embedder can skip its prompt.
|
||||
/// recorded when a clipboard_read or clipboard_write reply asks to
|
||||
/// remember the user's decision.
|
||||
kitty_clipboard_grants: kitty_clipboard.Grants = .{},
|
||||
|
||||
/// Called for sequence identifiers not supported by this library.
|
||||
@@ -161,26 +160,8 @@ pub const Handler = struct {
|
||||
/// Called when the running program reports progress via OSC 9;4.
|
||||
progress_report: ?*const fn (*Handler, osc.Command.ProgressReport) void,
|
||||
|
||||
/// Called when the running program writes to a clipboard. The write
|
||||
/// has a normalized destination and one or more decoded MIME
|
||||
/// representations. All request, MIME, and data memory is borrowed
|
||||
/// and only valid for the duration of the callback.
|
||||
///
|
||||
/// A write with no contents clears the destination. A content entry
|
||||
/// with empty data is a distinct empty representation.
|
||||
///
|
||||
/// OSC 52, OSC 1337 Copy, and Kitty clipboard (OSC 5522) writes all
|
||||
/// share this callback. Every call is one complete write whose
|
||||
/// contents replace whatever the destination previously held; there
|
||||
/// is never a partial update. A Kitty clipboard write transaction
|
||||
/// results in exactly one call, at commit, carrying all of the
|
||||
/// transaction's representations, and the returned result is
|
||||
/// reported back to the running program as the commit status (see
|
||||
/// kittyClipboard).
|
||||
///
|
||||
/// Clipboard read requests (OSC 52 with a "?" payload and OSC 5522
|
||||
/// reads) are delivered to clipboard_read instead.
|
||||
clipboard_write: ?*const fn (*Handler, clipboard.Write) clipboard.WriteResult,
|
||||
/// Called when the running program writes to a clipboard.
|
||||
clipboard_write: ?*const fn (*Handler, clipboard.Write) void,
|
||||
|
||||
/// Called when the running program requests clipboard contents
|
||||
/// (OSC 52 with a "?" payload, or a Kitty clipboard (OSC 5522)
|
||||
@@ -661,9 +642,14 @@ pub const Handler = struct {
|
||||
|
||||
// OSC 52 uses an empty payload to clear the selected clipboard.
|
||||
if (data.len == 0) {
|
||||
_ = func(self, .{
|
||||
func(self, .{
|
||||
.location = location,
|
||||
.contents = &.{},
|
||||
.name = "",
|
||||
.granted = false,
|
||||
.can_remember = false,
|
||||
.reply_ctx = self,
|
||||
.reply_fn = &ignoreWriteReply,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -680,12 +666,22 @@ pub const Handler = struct {
|
||||
.mime = "text/plain",
|
||||
.data = decoded,
|
||||
}};
|
||||
_ = func(self, .{
|
||||
func(self, .{
|
||||
.location = location,
|
||||
.contents = &contents,
|
||||
.name = "",
|
||||
.granted = false,
|
||||
.can_remember = false,
|
||||
.reply_ctx = self,
|
||||
.reply_fn = &ignoreWriteReply,
|
||||
});
|
||||
}
|
||||
|
||||
/// Reply target for clipboard writes on protocols without a write
|
||||
/// acknowledgement (OSC 52, OSC 1337 Copy): the reply is accepted
|
||||
/// and discarded.
|
||||
fn ignoreWriteReply(_: *anyopaque, _: clipboard.Write.Result) void {}
|
||||
|
||||
fn clipboardRead(
|
||||
self: *Handler,
|
||||
location: clipboard.Location,
|
||||
@@ -1094,28 +1090,87 @@ pub const Handler = struct {
|
||||
};
|
||||
defer committed.deinit(alloc);
|
||||
|
||||
// The effect result maps 1:1 onto the protocol's commit
|
||||
// statuses. The effect can't be null here (checked when the
|
||||
// transaction began) but if an embedder cleared it
|
||||
// mid-transaction that's ENOSYS.
|
||||
const result: clipboard.WriteResult = if (self.effects.clipboard_write) |func|
|
||||
func(self, .{
|
||||
.location = committed.loc,
|
||||
.contents = committed.contents,
|
||||
})
|
||||
else
|
||||
.unsupported;
|
||||
// The effect can't be null here (checked when the transaction
|
||||
// began) but if an embedder cleared it mid-transaction that's
|
||||
// ENOSYS.
|
||||
const func = self.effects.clipboard_write orelse {
|
||||
self.kittyClipboardFinish(state, .ENOSYS, terminator);
|
||||
return;
|
||||
};
|
||||
|
||||
self.kittyClipboardFinish(state, switch (result) {
|
||||
.success => .DONE,
|
||||
.denied => .EPERM,
|
||||
.unsupported => .ENOSYS,
|
||||
.busy => .EBUSY,
|
||||
.invalid_data => .EINVAL,
|
||||
.io_error, _ => .EIO,
|
||||
}, terminator);
|
||||
// Per the spec a password without a name is no password. A
|
||||
// stored grant for it lets the embedder skip its prompt.
|
||||
const pw: []const u8 = if (committed.name.len > 0) committed.pw else "";
|
||||
const granted = self.kitty_clipboard_grants.use(alloc, pw, .write);
|
||||
|
||||
var reply_state: KittyClipboardWriteReplyState = .{
|
||||
.handler = self,
|
||||
.pw = pw,
|
||||
};
|
||||
func(self, .{
|
||||
.location = committed.loc,
|
||||
.contents = committed.contents,
|
||||
.name = committed.name,
|
||||
.granted = granted,
|
||||
.can_remember = pw.len > 0,
|
||||
.reply_ctx = &reply_state,
|
||||
.reply_fn = &KittyClipboardWriteReplyState.reply,
|
||||
});
|
||||
|
||||
// The program is waiting on the commit status, so a callback
|
||||
// that returned without a reply is answered as a denial rather
|
||||
// than silence.
|
||||
self.kittyClipboardFinish(
|
||||
state,
|
||||
reply_state.status orelse .EPERM,
|
||||
terminator,
|
||||
);
|
||||
}
|
||||
|
||||
/// Reply state for one synchronous Kitty clipboard write. This lives
|
||||
/// on the kittyClipboardCommit stack frame, so it is only valid
|
||||
/// during the callback.
|
||||
const KittyClipboardWriteReplyState = struct {
|
||||
handler: *Handler,
|
||||
|
||||
/// The effective password, empty when the request had none.
|
||||
pw: []const u8,
|
||||
|
||||
/// The replied commit status, mapped 1:1 from the reply result;
|
||||
/// null until the callback replies.
|
||||
status: ?kitty_clipboard.Status = null,
|
||||
|
||||
fn reply(ctx: *anyopaque, result: clipboard.Write.Result) void {
|
||||
const self: *KittyClipboardWriteReplyState = @ptrCast(@alignCast(ctx));
|
||||
if (self.status != null) {
|
||||
log.warn("clipboard write replied more than once, ignoring", .{});
|
||||
return;
|
||||
}
|
||||
self.status = switch (result) {
|
||||
.denied => .EPERM,
|
||||
.unsupported => .ENOSYS,
|
||||
.busy => .EBUSY,
|
||||
.invalid_data => .EINVAL,
|
||||
.io_error => .EIO,
|
||||
.success => |success| status: {
|
||||
// Remembering is only offered when the request
|
||||
// carried a usable password.
|
||||
if (success.remember and self.pw.len > 0) {
|
||||
self.handler.kitty_clipboard_grants.grant(
|
||||
self.handler.terminal.gpa(),
|
||||
self.pw,
|
||||
.write,
|
||||
false,
|
||||
) catch |err| {
|
||||
log.warn("error recording clipboard grant err={}", .{err});
|
||||
};
|
||||
}
|
||||
break :status .DONE;
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Answer a write transaction with its final status and drop it.
|
||||
/// The id echoed is the one from the transaction's opening write
|
||||
/// packet, matching kitty.
|
||||
@@ -3242,7 +3297,7 @@ test "clipboard_write effect callback" {
|
||||
|
||||
const S = struct {
|
||||
var count: usize = 0;
|
||||
var result: clipboard.WriteResult = .success;
|
||||
var result: clipboard.Write.Result = .{ .success = .{} };
|
||||
var last_location: clipboard.Location = .standard;
|
||||
var last_contents_len: usize = 0;
|
||||
var last_mime: ?[]u8 = null;
|
||||
@@ -3256,7 +3311,7 @@ test "clipboard_write effect callback" {
|
||||
last_contents_len = 0;
|
||||
}
|
||||
|
||||
fn clipboardWrite(_: *Handler, write: clipboard.Write) clipboard.WriteResult {
|
||||
fn clipboardWrite(_: *Handler, write: clipboard.Write) void {
|
||||
clearCapture();
|
||||
count += 1;
|
||||
last_location = write.location;
|
||||
@@ -3267,7 +3322,7 @@ test "clipboard_write effect callback" {
|
||||
last_data = testing.allocator.dupe(u8, write.contents[0].data) catch
|
||||
@panic("failed to capture clipboard data");
|
||||
}
|
||||
return result;
|
||||
write.reply(result);
|
||||
}
|
||||
};
|
||||
S.count = 0;
|
||||
@@ -3333,9 +3388,9 @@ test "clipboard_write effect callback" {
|
||||
try testing.expectEqualStrings("text/plain", S.last_mime.?);
|
||||
try testing.expectEqualStrings("fragmented", S.last_data.?);
|
||||
|
||||
// Callback results are intentionally ignored for protocols without a
|
||||
// write acknowledgement. The denied result above did not stop later writes.
|
||||
try testing.expectEqual(clipboard.WriteResult.denied, S.result);
|
||||
// Reply results are intentionally ignored for protocols without a
|
||||
// write acknowledgement. The denied reply above did not stop later writes.
|
||||
try testing.expect(S.result == .denied);
|
||||
}
|
||||
|
||||
test "clipboard_read effect callback" {
|
||||
@@ -3459,9 +3514,9 @@ test "clipboard_write allocation failure is ignored" {
|
||||
const S = struct {
|
||||
var count: usize = 0;
|
||||
|
||||
fn clipboardWrite(_: *Handler, _: clipboard.Write) clipboard.WriteResult {
|
||||
fn clipboardWrite(_: *Handler, write: clipboard.Write) void {
|
||||
count += 1;
|
||||
return .success;
|
||||
write.reply(.{ .success = .{} });
|
||||
}
|
||||
};
|
||||
S.count = 0;
|
||||
@@ -3489,14 +3544,21 @@ test "clipboard_write allocation failure is ignored" {
|
||||
const KittyClipboardCapture = struct {
|
||||
var responses: [1024]u8 = undefined;
|
||||
var responses_len: usize = 0;
|
||||
|
||||
// Write capture. A null write_result returns without replying.
|
||||
var write_count: usize = 0;
|
||||
var result: clipboard.WriteResult = .success;
|
||||
var write_result: ?clipboard.Write.Result = .{ .success = .{} };
|
||||
var write_reply_twice: bool = false;
|
||||
var last_location: clipboard.Location = .standard;
|
||||
var last_contents_len: usize = 0;
|
||||
var last_mimes: [8][64]u8 = undefined;
|
||||
var last_mime_lens: [8]usize = @splat(0);
|
||||
var last_data: [8][256]u8 = undefined;
|
||||
var last_data_lens: [8]usize = @splat(0);
|
||||
var last_write_name: [64]u8 = undefined;
|
||||
var last_write_name_len: usize = 0;
|
||||
var last_write_granted: bool = false;
|
||||
var last_write_can_remember: bool = false;
|
||||
|
||||
// Read capture. A null read_result returns without replying.
|
||||
var read_count: usize = 0;
|
||||
@@ -3515,11 +3577,15 @@ const KittyClipboardCapture = struct {
|
||||
fn reset() void {
|
||||
responses_len = 0;
|
||||
write_count = 0;
|
||||
result = .success;
|
||||
write_result = .{ .success = .{} };
|
||||
write_reply_twice = false;
|
||||
last_location = .standard;
|
||||
last_contents_len = 0;
|
||||
last_mime_lens = @splat(0);
|
||||
last_data_lens = @splat(0);
|
||||
last_write_name_len = 0;
|
||||
last_write_granted = false;
|
||||
last_write_can_remember = false;
|
||||
read_count = 0;
|
||||
read_result = null;
|
||||
read_reply_twice = false;
|
||||
@@ -3537,7 +3603,7 @@ const KittyClipboardCapture = struct {
|
||||
responses_len += data.len;
|
||||
}
|
||||
|
||||
fn clipboardWrite(_: *Handler, write: clipboard.Write) clipboard.WriteResult {
|
||||
fn clipboardWrite(_: *Handler, write: clipboard.Write) void {
|
||||
write_count += 1;
|
||||
last_location = write.location;
|
||||
last_contents_len = write.contents.len;
|
||||
@@ -3547,7 +3613,12 @@ const KittyClipboardCapture = struct {
|
||||
last_data_lens[i] = content.data.len;
|
||||
@memcpy(last_data[i][0..content.data.len], content.data);
|
||||
}
|
||||
return result;
|
||||
last_write_name_len = write.name.len;
|
||||
@memcpy(last_write_name[0..write.name.len], write.name);
|
||||
last_write_granted = write.granted;
|
||||
last_write_can_remember = write.can_remember;
|
||||
if (write_result) |r| write.reply(r);
|
||||
if (write_reply_twice) write.reply(.io_error);
|
||||
}
|
||||
|
||||
fn clipboardRead(_: *Handler, read: clipboard.Read) void {
|
||||
@@ -3579,6 +3650,10 @@ const KittyClipboardCapture = struct {
|
||||
return last_read_name[0..last_read_name_len];
|
||||
}
|
||||
|
||||
fn writeName() []const u8 {
|
||||
return last_write_name[0..last_write_name_len];
|
||||
}
|
||||
|
||||
fn mimeAt(i: usize) []const u8 {
|
||||
return last_mimes[i][0..last_mime_lens[i]];
|
||||
}
|
||||
@@ -3647,20 +3722,22 @@ test "kitty clipboard write result maps to response status" {
|
||||
defer s.deinit();
|
||||
|
||||
const cases = [_]struct {
|
||||
result: clipboard.WriteResult,
|
||||
result: ?clipboard.Write.Result,
|
||||
response: []const u8,
|
||||
}{
|
||||
.{ .result = .success, .response = "\x1B]5522;type=write:status=DONE\x1B\\" },
|
||||
.{ .result = .{ .success = .{} }, .response = "\x1B]5522;type=write:status=DONE\x1B\\" },
|
||||
.{ .result = .denied, .response = "\x1B]5522;type=write:status=EPERM\x1B\\" },
|
||||
.{ .result = .unsupported, .response = "\x1B]5522;type=write:status=ENOSYS\x1B\\" },
|
||||
.{ .result = .busy, .response = "\x1B]5522;type=write:status=EBUSY\x1B\\" },
|
||||
.{ .result = .invalid_data, .response = "\x1B]5522;type=write:status=EINVAL\x1B\\" },
|
||||
.{ .result = .io_error, .response = "\x1B]5522;type=write:status=EIO\x1B\\" },
|
||||
// No reply at all is a denial rather than silence.
|
||||
.{ .result = null, .response = "\x1B]5522;type=write:status=EPERM\x1B\\" },
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
S.reset();
|
||||
S.result = case.result;
|
||||
S.write_result = case.result;
|
||||
|
||||
// An immediately-committed write with no data is a clear.
|
||||
s.nextSlice("\x1B]5522;type=write\x1B\\");
|
||||
@@ -3670,6 +3747,16 @@ test "kitty clipboard write result maps to response status" {
|
||||
try testing.expectEqualStrings(case.response, S.responseSlice());
|
||||
}
|
||||
|
||||
// A second reply is ignored.
|
||||
S.reset();
|
||||
S.write_reply_twice = true;
|
||||
s.nextSlice("\x1B]5522;type=write\x1B\\");
|
||||
s.nextSlice("\x1B]5522;type=wdata\x1B\\");
|
||||
try testing.expectEqualStrings(
|
||||
"\x1B]5522;type=write:status=DONE\x1B\\",
|
||||
S.responseSlice(),
|
||||
);
|
||||
|
||||
// The response echoes the request terminator, unlike kitty which
|
||||
// always uses ST.
|
||||
S.reset();
|
||||
@@ -3948,6 +4035,70 @@ test "kitty clipboard read password grants" {
|
||||
// the leak otherwise).
|
||||
}
|
||||
|
||||
test "kitty clipboard write password grants" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
const S = KittyClipboardCapture;
|
||||
S.reset();
|
||||
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.clipboard_write = &S.clipboardWrite;
|
||||
handler.effects.clipboard_read = &S.clipboardRead;
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// pw="secret", name="app": the first commit isn't granted but the
|
||||
// reply may ask to remember it.
|
||||
S.write_result = .{ .success = .{ .remember = true } };
|
||||
s.nextSlice("\x1B]5522;type=write:pw=c2VjcmV0:name=YXBw\x1B\\");
|
||||
s.nextSlice("\x1B]5522;type=wdata\x1B\\");
|
||||
try testing.expectEqual(@as(usize, 1), S.write_count);
|
||||
try testing.expectEqualStrings("app", S.writeName());
|
||||
try testing.expect(!S.last_write_granted);
|
||||
try testing.expect(S.last_write_can_remember);
|
||||
|
||||
// The same password is now granted; a different one is not.
|
||||
S.write_result = .{ .success = .{} };
|
||||
s.nextSlice("\x1B]5522;type=write:pw=c2VjcmV0:name=YXBw\x1B\\");
|
||||
s.nextSlice("\x1B]5522;type=wdata\x1B\\");
|
||||
try testing.expect(S.last_write_granted);
|
||||
s.nextSlice("\x1B]5522;type=write:pw=b3RoZXI=:name=YXBw\x1B\\");
|
||||
s.nextSlice("\x1B]5522;type=wdata\x1B\\");
|
||||
try testing.expect(!S.last_write_granted);
|
||||
try testing.expect(S.last_write_can_remember);
|
||||
|
||||
// Directions are independent: a write grant doesn't satisfy reads.
|
||||
S.read_result = .{ .success = .{} };
|
||||
s.nextSlice("\x1B]5522;type=read:pw=c2VjcmV0:name=YXBw\x1B\\");
|
||||
try testing.expect(!S.last_read_granted);
|
||||
|
||||
// A password without a name doesn't count: it is neither granted
|
||||
// nor rememberable, even if the reply asks.
|
||||
S.write_result = .{ .success = .{ .remember = true } };
|
||||
s.nextSlice("\x1B]5522;type=write:pw=c2VjcmV0\x1B\\");
|
||||
s.nextSlice("\x1B]5522;type=wdata\x1B\\");
|
||||
try testing.expectEqualStrings("", S.writeName());
|
||||
try testing.expect(!S.last_write_granted);
|
||||
try testing.expect(!S.last_write_can_remember);
|
||||
|
||||
// A grant is advisory: the request is still forwarded and the
|
||||
// embedder may deny it.
|
||||
S.responses_len = 0;
|
||||
S.write_result = .denied;
|
||||
s.nextSlice("\x1B]5522;type=write:id=d:pw=c2VjcmV0:name=YXBw\x1B\\");
|
||||
s.nextSlice("\x1B]5522;type=wdata\x1B\\");
|
||||
try testing.expect(S.last_write_granted);
|
||||
try testing.expectEqualStrings(
|
||||
"\x1B]5522;type=write:status=EPERM:id=d\x1B\\",
|
||||
S.responseSlice(),
|
||||
);
|
||||
|
||||
// Grants are freed with the stream (the testing allocator catches
|
||||
// the leak otherwise).
|
||||
}
|
||||
|
||||
test "kitty clipboard malformed packets are silently dropped" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
@@ -5185,10 +5336,10 @@ test "continuation reconstructs standard stream without duplicate effects" {
|
||||
|
||||
fn clipboardWrite(
|
||||
_: *Handler,
|
||||
_: clipboard.Write,
|
||||
) clipboard.WriteResult {
|
||||
write: clipboard.Write,
|
||||
) void {
|
||||
clipboard_count += 1;
|
||||
return .success;
|
||||
write.reply(.{ .success = .{} });
|
||||
}
|
||||
|
||||
fn reset() void {
|
||||
|
||||
Reference in New Issue
Block a user