libghostty: add ghostty_terminal_paste C API with paste events example

This commit is contained in:
Mitchell Hashimoto
2026-08-22 15:00:22 -07:00
parent dda8e6f314
commit 60a1ae2df7
11 changed files with 662 additions and 31 deletions

View File

@@ -1,7 +1,130 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <ghostty/vt.h>
#define GS(s) ((GhosttyString){.ptr = (const uint8_t*)(s), .len = sizeof(s) - 1})
// Print bytes destined for the pty with control characters made visible.
static void print_escaped(const uint8_t* data, size_t len) {
for (size_t i = 0; i < len; i++) {
switch (data[i]) {
case 0x1b: printf("ESC"); break;
case '\r': printf("\\r"); break;
case '\n': printf("\\n"); break;
default: putchar(data[i]); break;
}
}
}
// The base64 password of the last paste event, captured from the OK
// packet so the example can play the program's side of the protocol.
static char event_pw[128];
// Everything the terminal writes to the running program: the pasted
// text, or the paste event packets when mode 5522 is enabled.
static void on_write_pty(GhosttyTerminal terminal,
void* userdata,
const uint8_t* data,
size_t len) {
(void)terminal;
(void)userdata;
printf(" -> pty (%zu bytes): ", len);
print_escaped(data, len);
printf("\n");
// A paste event's OK packet: OSC 5522 ; type=read:status=OK:pw=<b64> ST
const char* prefix = "\x1b]5522;type=read:status=OK:pw=";
size_t prefix_len = strlen(prefix);
if (len > prefix_len && memcmp(data, prefix, prefix_len) == 0) {
size_t end = prefix_len;
while (end < len && data[end] != 0x1b) end++;
size_t pw_len = end - prefix_len;
if (pw_len < sizeof(event_pw)) {
memcpy(event_pw, data + prefix_len, pw_len);
event_pw[pw_len] = 0;
}
}
}
// Serves clipboard reads. After a paste event the program's read arrives
// with `granted` set, because it carries the event's one-time password,
// so the embedder skips its permission prompt.
static void on_clipboard_read(GhosttyTerminal terminal,
void* userdata,
const GhosttyClipboardRead* read) {
(void)terminal;
(void)userdata;
printf(" clipboard read: name=\"");
fwrite(read->name.ptr, 1, read->name.len, stdout);
printf("\" granted=%s\n", read->granted ? "yes (no prompt needed)" : "no");
const char* text = "hello from the clipboard";
GhosttyClipboardContent content = {
.mime = GS("text/plain"),
.data = {.ptr = (const uint8_t*)text, .len = strlen(text)},
};
GhosttyClipboardReadReply reply = {
.size = sizeof(reply),
.result = GHOSTTY_CLIPBOARD_READ_RESULT_SUCCESS,
.contents = &content,
.contents_len = 1,
.available = NULL,
.available_len = 0,
.remember = false,
};
read->reply(read, &reply);
}
// A real embedder would show a dialog here.
static bool confirm_with_user(void) {
printf(" paste could inject commands; user confirmed\n");
return true;
}
//! [terminal-paste]
// Paste whatever the clipboard holds. The terminal applies its own
// state: bracketed paste framing (mode 2004) or a Kitty paste event
// (mode 5522) instead of the text.
static void paste_clipboard(GhosttyTerminal terminal, const char* text) {
GhosttyClipboardContent contents[] = {
// The first text representation is what a text paste writes.
{.mime = GS("text/plain"),
.data = {.ptr = (const uint8_t*)text, .len = strlen(text)}},
// Listed on a paste event, never written, so no data is needed.
{.mime = GS("image/png"), .data = {.ptr = NULL, .len = 0}},
};
GhosttyPaste paste = {
.size = sizeof(paste),
.location = GHOSTTY_CLIPBOARD_LOCATION_STANDARD,
.source = GHOSTTY_PASTE_SOURCE_CLIPBOARD,
.contents = contents,
.contents_len = sizeof(contents) / sizeof(contents[0]),
.allow_unsafe = false,
};
bool written = false;
GhosttyResult result = ghostty_terminal_paste(terminal, &paste, &written);
if (result == GHOSTTY_REJECTED) {
// The text could inject commands (e.g. a newline outside of a
// bracketed paste). Nothing was written; ask, then retry.
if (!confirm_with_user()) return;
paste.allow_unsafe = true;
result = ghostty_terminal_paste(terminal, &paste, &written);
}
if (result != GHOSTTY_SUCCESS) {
fprintf(stderr, "paste failed: %d\n", (int)result);
return;
}
// Whether the pty got the text or a paste event depends on the
// terminal's modes; either way it went through write_pty above.
printf(" %s\n", written ? "written" : "nothing to paste");
}
//! [terminal-paste]
//! [paste-safety]
void safety_example() {
const char* safe_data = "hello world";
@@ -29,27 +152,89 @@ void encode_example() {
if (result == GHOSTTY_SUCCESS) {
printf("Encoded %zu bytes: ", written);
fwrite(buf, 1, written, stdout);
print_escaped((const uint8_t*)buf, written);
printf("\n");
}
}
//! [paste-encode]
static void vt_write(GhosttyTerminal terminal, const char* seq) {
ghostty_terminal_vt_write(terminal, (const uint8_t*)seq, strlen(seq));
}
int main() {
GhosttyTerminal terminal = NULL;
if (ghostty_terminal_new(NULL, &terminal, 80, 24) != GHOSTTY_SUCCESS) {
fprintf(stderr, "Failed to create terminal\n");
return 1;
}
// Pasted bytes and paste events go to write_pty. Serving clipboard
// reads is what lets the terminal send paste events at all: without
// this callback the program could never read the clipboard, so pastes
// stay text even when mode 5522 is enabled.
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_WRITE_PTY,
(const void*)on_write_pty);
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ,
(const void*)on_clipboard_read);
printf("Plain paste:\n");
paste_clipboard(terminal, "hello world");
printf("Paste with a newline (refused, then confirmed):\n");
paste_clipboard(terminal, "echo hi\n");
// The program enables bracketed paste: newlines are safe inside the
// frame and are preserved.
printf("Bracketed paste (mode 2004):\n");
vt_write(terminal, "\x1b[?2004h");
paste_clipboard(terminal, "line one\nline two");
// The program enables paste events: the clipboard's MIME types are
// listed with a one-time password instead of writing the data.
printf("Paste event (mode 5522):\n");
vt_write(terminal, "\x1b[?5522h");
paste_clipboard(terminal, "hello world");
// Play the program's side: read the clipboard with the password from
// the event. The read arrives granted and the data is served through
// write_pty as base64 without any permission prompt.
if (event_pw[0] != 0) {
printf("Program reads with the event password:\n");
char read_seq[256];
snprintf(read_seq, sizeof(read_seq),
"\x1b]5522;type=read:pw=%s:name=UGFzdGUgZXZlbnQ=;dGV4dC9wbGFpbg==\x1b\\",
event_pw);
vt_write(terminal, read_seq);
}
// Text inserted by other means (IME, drag and drop) is never an event.
printf("IME text with mode 5522 enabled:\n");
{
const char* text = "committed";
GhosttyClipboardContent content = {
.mime = GS("text/plain"),
.data = {.ptr = (const uint8_t*)text, .len = strlen(text)},
};
GhosttyPaste paste = {
.size = sizeof(paste),
.location = GHOSTTY_CLIPBOARD_LOCATION_STANDARD,
.source = GHOSTTY_PASTE_SOURCE_TEXT,
.contents = &content,
.contents_len = 1,
.allow_unsafe = true,
};
bool written = false;
if (ghostty_terminal_paste(terminal, &paste, &written) == GHOSTTY_SUCCESS &&
written) {
printf(" written\n");
}
}
ghostty_terminal_free(terminal);
printf("\nTerminal-free building blocks:\n");
safety_example();
// Test unsafe paste data with bracketed paste end sequence
const char *unsafe_escape = "evil\x1b[201~code";
if (!ghostty_paste_is_safe(unsafe_escape, strlen(unsafe_escape))) {
printf("Data with escape sequence is UNSAFE\n");
}
// Test empty data
const char *empty_data = "";
if (ghostty_paste_is_safe(empty_data, 0)) {
printf("Empty data is safe\n");
}
encode_example();
return 0;