libghostty: clipboard_read effect, enables OSC52 reads

This adds a `clipboard_read` effect to the stream terminal handler and a
matching `GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ` callback to the
libghostty-vt C API so that embedders can answer OSC 52 read requests
(the `?` payload). 

This is a _blocking_ effect: if the embedder needs to ask the user for
permission, the entire VT processing pipeline is _blocked_ during the
callback. This is a purposeful simplification choice compared to how
Ghostty GUI works with async requests. I think its reasonable, it
eliminates a TON of complexity.

If the effect isn't set, then any clipboard reads are denied.

This can be expanded easily to support Kitty clipboard protocol later.
This commit is contained in:
Mitchell Hashimoto
2026-08-21 20:34:38 -07:00
parent ca9e5b1301
commit e03475c0cc
11 changed files with 852 additions and 22 deletions

View File

@@ -96,6 +96,7 @@ extern "C" {
* | `GHOSTTY_TERMINAL_OPT_COLOR_SCHEME` | `GhosttyTerminalColorSchemeFn` | Color scheme query (CSI ? 996 n) |
* | `GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES`| `GhosttyTerminalDeviceAttributesFn`| Device attributes query (CSI c / > c / = c)|
* | `GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE` | `GhosttyTerminalClipboardWriteFn` | Clipboard write via OSC 52 / OSC 1337 |
* | `GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ` | `GhosttyTerminalClipboardReadFn` | Clipboard read via OSC 52 "?" |
* | `GHOSTTY_TERMINAL_OPT_DESKTOP_NOTIFICATION`| `GhosttyTerminalDesktopNotificationFn` | Desktop notification via OSC 9 / OSC 777 |
* | `GHOSTTY_TERMINAL_OPT_PROGRESS_REPORT` | `GhosttyTerminalProgressReportFn` | Progress report via OSC 9;4 |
* | `GHOSTTY_TERMINAL_OPT_UNKNOWN_SEQUENCE` | `GhosttyTerminalUnknownSequenceFn` | Unsupported sequence identifier |
@@ -112,6 +113,9 @@ extern "C" {
* ### Defining a clipboard_write callback
* @snippet c-vt-effects/src/main.c effects-clipboard-write
*
* ### Defining a clipboard_read callback
* @snippet c-vt-effects/src/main.c effects-clipboard-read
*
* ### Defining an unknown_sequence callback
* @snippet c-vt-effects/src/main.c effects-unknown-sequence
*
@@ -522,8 +526,8 @@ typedef enum GHOSTTY_ENUM_TYPED {
* details such as OSC 52 selectors, base64 encoding, multipart chunks,
* aliases, and terminators are normalized before this callback is invoked.
* OSC 52 and iTerm2 OSC 1337 Copy writes therefore use the same callback
* shape. OSC 52 clipboard read requests ("?") are always ignored and never
* forwarded to this callback.
* shape. OSC 52 clipboard read requests ("?") are delivered to
* GhosttyTerminalClipboardReadFn instead.
*
* @param terminal The terminal handle
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
@@ -537,6 +541,180 @@ typedef GhosttyClipboardWriteResult (*GhosttyTerminalClipboardWriteFn)(
void* userdata,
const GhosttyClipboardWrite* write);
/**
* Result of a clipboard read reply.
*
* @ingroup terminal
*/
typedef enum {
/** The clipboard was read; the reply carries its contents. */
GHOSTTY_CLIPBOARD_READ_RESULT_SUCCESS = 0,
/** The clipboard read was denied by policy or the user. */
GHOSTTY_CLIPBOARD_READ_RESULT_DENIED = 1,
/** The embedder cannot read this clipboard. */
GHOSTTY_CLIPBOARD_READ_RESULT_UNSUPPORTED = 2,
/** The clipboard is temporarily unavailable. */
GHOSTTY_CLIPBOARD_READ_RESULT_BUSY = 3,
/** Reading the clipboard failed due to an I/O error. */
GHOSTTY_CLIPBOARD_READ_RESULT_IO_ERROR = 4,
GHOSTTY_CLIPBOARD_READ_RESULT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyClipboardReadResult;
/**
* The reply to a clipboard read request.
*
* This is a sized struct; set `size` to `sizeof(GhosttyClipboardReadReply)`.
* All arrays and the strings they point to are borrowed only for the
* duration of the reply call and may be freed as soon as it returns.
*
* Any result other than GHOSTTY_CLIPBOARD_READ_RESULT_SUCCESS answers the
* program with an empty clipboard; the other fields are ignored in that
* case. On success, `contents` should carry one representation per
* requested MIME type (GhosttyClipboardRead::mimes) that the clipboard
* has; unrequested representations are ignored. Protocols that carry a
* single text value (OSC 52) use the first entry with a text MIME type
* such as "text/plain".
*
* @ingroup terminal
*/
typedef struct {
/** Size of this struct in bytes. */
size_t size;
/** Outcome of the read. */
GhosttyClipboardReadResult result;
/** Borrowed array of MIME representations of the clipboard contents. */
const GhosttyClipboardContent* contents;
/** Number of entries in contents. */
size_t contents_len;
/**
* Borrowed array of all MIME types available on the clipboard. Only
* used when GhosttyClipboardRead::list is set; may be NULL otherwise.
*/
const GhosttyString* available;
/** Number of entries in available. */
size_t available_len;
/**
* Record a session grant so future requests from the same program skip
* the permission prompt. Only honored on success when
* GhosttyClipboardRead::can_remember is set.
*/
bool remember;
} GhosttyClipboardReadReply;
typedef struct GhosttyClipboardRead GhosttyClipboardRead;
/**
* Function type used to answer a clipboard read request. Obtained from
* GhosttyClipboardRead::reply; see that struct for the contract.
*
* @param read The request being answered
* @param reply The reply, borrowed only for the duration of this call
*
* @ingroup terminal
*/
typedef void (*GhosttyClipboardReadReplyFn)(
const GhosttyClipboardRead* read,
const GhosttyClipboardReadReply* reply);
/**
* A synchronous request to read clipboard contents.
*
* This is a sized struct. The callback must only access fields present in the
* size reported by `size`. The request is borrowed and valid only for the
* callback duration.
*
* The read is answered by calling `reply` with this request and a
* GhosttyClipboardReadReply. This must happen before the callback returns;
* the request is invalid afterwards. Calling `reply` more than once is
* ignored. Returning without replying answers the program with an empty
* clipboard.
*
* @ingroup terminal
*/
struct GhosttyClipboardRead {
/** Size of this struct in bytes. */
size_t size;
/** Clipboard to read. */
GhosttyClipboardLocation location;
/**
* Borrowed array of the MIME types the program wants, in order of
* preference. Protocols that only carry text (OSC 52) request
* "text/plain". NULL when mimes_len is zero.
*/
const GhosttyString* mimes;
/** Number of entries in mimes. */
size_t mimes_len;
/**
* True if the program also wants the list of MIME types available on the
* clipboard, delivered through GhosttyClipboardReadReply::available.
*/
bool list;
/**
* Name of the requesting 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
* (kitty clipboard protocol passwords). The embedder should skip any
* permission prompt and serve the read.
*/
bool granted;
/**
* True if the program supplied a session password, so the embedder may
* offer to remember the user's decision through
* GhosttyClipboardReadReply::remember. When false, remember is ignored.
*/
bool can_remember;
/** Terminal-owned reply state. Do not access. */
const void* ctx;
/** Answer the read; see the struct documentation. */
GhosttyClipboardReadReplyFn reply;
};
/**
* Callback function type for clipboard_read.
*
* Called synchronously when the running program requests clipboard contents
* via OSC 52 with a "?" payload. Answering lets the program read the user's
* clipboard, so the embedder is expected to mediate consent. Because the
* read is synchronous, an embedder that needs to ask the user must block
* (for example by running a modal prompt) until it has an answer; the VT
* stream waits until the callback returns.
*
* Answer by calling `read->reply(read, &reply)` before returning. See
* GhosttyClipboardRead for the full contract.
*
* @param terminal The terminal handle
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
* @param read Borrowed clipboard read request
*
* @ingroup terminal
*/
typedef void (*GhosttyTerminalClipboardReadFn)(
GhosttyTerminal terminal,
void* userdata,
const GhosttyClipboardRead* read);
/**
* A request to show a desktop notification.
*
@@ -1067,8 +1245,8 @@ typedef enum GHOSTTY_ENUM_TYPED {
* Callback invoked when the running program performs a clipboard write.
* OSC 52 and iTerm2 OSC 1337 Copy writes are normalized to an atomic set
* of decoded MIME representations. Set to NULL to ignore clipboard writes.
* Clipboard read requests are always ignored; see
* GhosttyTerminalClipboardWriteFn.
* Clipboard read requests are delivered to
* GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ instead.
*
* Input type: GhosttyTerminalClipboardWriteFn
*/
@@ -1230,6 +1408,16 @@ typedef enum GHOSTTY_ENUM_TYPED {
* Input type: GhosttyString*
*/
GHOSTTY_TERMINAL_OPT_TERMINFO_NAME = 37,
/**
* Callback invoked when the running program requests clipboard contents
* via OSC 52 with a "?" payload. The read is synchronous and must be
* answered before the callback returns. Set to NULL to ignore clipboard
* read requests (the default).
*
* Input type: GhosttyTerminalClipboardReadFn
*/
GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ = 38,
GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyTerminalOption;