libghostty
Loading...
Searching...
No Matches
c-vt-paste/src/main.c

This example demonstrates how to paste into a terminal, including the unsafe-paste confirmation flow and Kitty clipboard protocol paste events (mode 5522), as well as the terminal-free paste safety and encoding utilities.

#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";
.mime = GS("text/plain"),
.data = {.ptr = (const uint8_t*)text, .len = strlen(text)},
};
.size = sizeof(reply),
.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;
}
// What the clipboard holds. A real embedder would keep a handle to the
// pasteboard or its items here; the data is only produced on demand.
typedef struct {
const char* text;
} clipboard_t;
// Produces the data of one representation when the terminal needs it.
// Only the text is ever read: the image is listed on a paste event
// but never requested, so a large image costs nothing to paste.
// Nothing written to the writer is retained, so the data can be
// streamed from anywhere in pieces of any size.
static bool read_clipboard(void* userdata, GhosttyString mime, GhosttyWriter writer) {
clipboard_t* clipboard = userdata;
if (mime.len == strlen("text/plain") &&
memcmp(mime.ptr, "text/plain", mime.len) == 0) {
// Stream the text in small pieces just to show that it works.
const uint8_t* data = (const uint8_t*)clipboard->text;
size_t len = strlen(clipboard->text);
for (size_t offset = 0; offset < len; offset += 4) {
size_t n = len - offset < 4 ? len - offset : 4;
if (!writer.write(writer.userdata, data + offset, n)) return false;
}
return true;
}
printf(" image read requested, which never happens\n");
return false;
}
// 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) {
clipboard_t clipboard = {.text = text};
GhosttyString mimes[] = {
// The first text representation is what a text paste writes.
GS("text/plain"),
// Listed on a paste event, never read.
GS("image/png"),
};
GhosttyPaste paste = {
.size = sizeof(paste),
.mimes = mimes,
.mimes_len = sizeof(mimes) / sizeof(mimes[0]),
.reader = {.read = read_clipboard, .userdata = &clipboard},
.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, in
// chunks as the text was read.
printf(" %s\n", written ? "written" : "nothing to paste");
}
void safety_example() {
const char* safe_data = "hello world";
const char* unsafe_data = "rm -rf /\n";
if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) {
printf("Safe to paste\n");
}
if (!ghostty_paste_is_safe(unsafe_data, strlen(unsafe_data))) {
printf("Unsafe! Contains newline\n");
}
}
void encode_example() {
// The input buffer is modified in place (unsafe bytes are stripped).
char data[] = "hello\nworld";
char buf[64];
size_t written = 0;
data, strlen(data), true, buf, sizeof(buf), &written);
if (result == GHOSTTY_SUCCESS) {
printf("Encoded %zu bytes: ", written);
print_escaped((const uint8_t*)buf, written);
printf("\n");
}
}
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.
(const void*)on_write_pty);
(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");
{
clipboard_t clipboard = {.text = "committed"};
GhosttyString mime = GS("text/plain");
GhosttyPaste paste = {
.size = sizeof(paste),
.mimes = &mime,
.mimes_len = 1,
.reader = {.read = read_clipboard, .userdata = &clipboard},
.allow_unsafe = true,
};
bool written = false;
if (ghostty_terminal_paste(terminal, &paste, &written) == GHOSTTY_SUCCESS &&
written) {
printf(" written\n");
}
}
printf("\nTerminal-free building blocks:\n");
safety_example();
encode_example();
return 0;
}
GHOSTTY_API GhosttyResult ghostty_paste_encode(char *data, size_t data_len, bool bracketed, char *buf, size_t buf_len, size_t *out_written)
GHOSTTY_API GhosttyResult ghostty_terminal_paste(GhosttyTerminal terminal, const GhosttyPaste *paste, bool *out_written)
GHOSTTY_API bool ghostty_paste_is_safe(const char *data, size_t len)
@ GHOSTTY_PASTE_SOURCE_CLIPBOARD
Definition paste.h:91
@ GHOSTTY_PASTE_SOURCE_TEXT
Definition paste.h:99
GHOSTTY_API GhosttyResult ghostty_terminal_set(GhosttyTerminal terminal, GhosttyTerminalOption option, const void *value)
struct GhosttyTerminalImpl * GhosttyTerminal
Definition types.h:119
GHOSTTY_API void ghostty_terminal_free(GhosttyTerminal terminal)
GHOSTTY_API void ghostty_terminal_vt_write(GhosttyTerminal terminal, const uint8_t *data, size_t len)
GHOSTTY_API GhosttyResult ghostty_terminal_new(const GhosttyAllocator *allocator, GhosttyTerminal *terminal, uint16_t cols, uint16_t rows)
@ GHOSTTY_CLIPBOARD_READ_RESULT_SUCCESS
Definition terminal.h:633
@ GHOSTTY_CLIPBOARD_LOCATION_STANDARD
Definition terminal.h:435
@ GHOSTTY_TERMINAL_OPT_WRITE_PTY
Definition terminal.h:1109
@ GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ
Definition terminal.h:1524
GhosttyString name
Definition terminal.h:754
GhosttyClipboardReadReplyFn reply
Definition terminal.h:779
bool allow_unsafe
Definition paste.h:156
size_t len
Definition types.h:271
const uint8_t * ptr
Definition types.h:268
GhosttyResult
Definition types.h:88
@ GHOSTTY_REJECTED
Definition types.h:108
@ GHOSTTY_SUCCESS
Definition types.h:90