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

@@ -34,7 +34,7 @@
* - @ref snapshot "Terminal Snapshot" - Encode and incrementally restore terminal state
* - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences
* - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences
* - @ref paste "Paste Utilities" - Validate paste data safety
* - @ref paste "Paste" - Paste into a terminal, validate and encode paste data
* - @ref unicode "Unicode Utilities" - Codepoint properties for text layout
* - @ref build_info "Build Info" - Query compile-time build configuration
* - @ref allocator "Memory Management" - Memory management and custom allocators
@@ -53,7 +53,7 @@
* - @ref c-vt/src/main.c - OSC parser example
* - @ref c-vt-encode-key/src/main.c - Key encoding example
* - @ref c-vt-encode-mouse/src/main.c - Mouse encoding example
* - @ref c-vt-paste/src/main.c - Paste safety check example
* - @ref c-vt-paste/src/main.c - Paste example
* - @ref c-vt-sgr/src/main.c - SGR parser example
* - @ref c-vt-formatter/src/main.c - Terminal formatter example
* - @ref c-vt-grid-traverse/src/main.c - Grid traversal example using grid refs
@@ -83,8 +83,10 @@
*/
/** @example c-vt-paste/src/main.c
* This example demonstrates how to use the paste utilities to check if
* paste data is safe before sending it to the terminal.
* 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.
*/
/** @example c-vt-sgr/src/main.c

View File

@@ -1,26 +1,57 @@
/**
* @file paste.h
*
* Paste utilities - validate and encode paste data for terminal input.
* Paste - paste into a terminal, and validate and encode paste data.
*/
#ifndef GHOSTTY_VT_PASTE_H
#define GHOSTTY_VT_PASTE_H
/** @defgroup paste Paste Utilities
/** @defgroup paste Paste
*
* Utilities for validating and encoding paste data for terminal input.
* Pasting into a terminal, plus the terminal-free utilities for
* validating and encoding paste data.
*
* ## Basic Usage
* ## Pasting into a Terminal
*
* Use ghostty_paste_is_safe() to check if paste data contains potentially
* dangerous sequences before sending it to the terminal.
* What a paste writes to the pty depends on the terminal's state, so
* the recommended way to paste is ghostty_terminal_paste(). The embedder
* hands over what the clipboard holds as MIME-typed contents (just
* `text/plain` for an ordinary paste) and where it came from, and the
* terminal decides how its current modes apply:
*
* Use ghostty_paste_encode() to encode paste data for writing to the pty,
* - If Kitty clipboard protocol paste events (mode 5522,
* GHOSTTY_MODE_PASTE_EVENTS) are enabled, the paste was user-initiated
* (GHOSTTY_PASTE_SOURCE_CLIPBOARD), and a clipboard_read callback is
* installed, the terminal sends the program a paste event listing the
* clipboard's MIME types with a one-time password instead of the data.
* The program then reads what it wants through the clipboard_read
* callback, which arrives with `granted` set so no permission prompt
* is needed.
* - Otherwise the first text representation is written: unsafe control
* bytes are replaced with spaces, and it is wrapped in bracketed paste
* sequences if mode 2004 (GHOSTTY_MODE_BRACKETED_PASTE) is enabled, or
* has its newlines converted to carriage returns if not.
*
* Text that could inject commands (a newline when unbracketed, or the
* bracketed paste terminator when bracketed) is refused with
* GHOSTTY_REJECTED unless GhosttyPaste::allow_unsafe is set. The usual
* flow is to call once, confirm with the user on GHOSTTY_REJECTED, and
* call again with `allow_unsafe` set.
*
* Output is delivered through the write_pty callback
* (GHOSTTY_TERMINAL_OPT_WRITE_PTY) in a single call.
*
* @snippet c-vt-paste/src/main.c terminal-paste
*
* ## Building Blocks
*
* For embedders that encode without a terminal, ghostty_paste_is_safe()
* checks if paste data contains potentially dangerous sequences
* (conservatively, regardless of terminal state) and
* ghostty_paste_encode() encodes paste data for writing to the pty,
* including bracketed paste wrapping and unsafe byte stripping.
*
* ## Examples
*
* ### Safety Check
*
* @snippet c-vt-paste/src/main.c paste-safety
@@ -35,11 +66,99 @@
#include <stdbool.h>
#include <stddef.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/terminal.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Why a paste happened.
*/
typedef enum GHOSTTY_ENUM_TYPED {
/** The user pasted from a clipboard: keybind, menu, middle click. */
GHOSTTY_PASTE_SOURCE_CLIPBOARD = 0,
/**
* Text inserted some other way: IME commit, drag and drop, scripted
* input. Always written as text, never as a paste event, matching
* kitty. This is not a way to opt out of paste events; an embedder
* that doesn't want them doesn't install a clipboard_read callback.
*/
GHOSTTY_PASTE_SOURCE_TEXT = 1,
GHOSTTY_PASTE_SOURCE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyPasteSource;
/**
* A paste of clipboard contents into the terminal.
*
* This is a sized struct; set `size` to `sizeof(GhosttyPaste)`. The
* contents array and the strings it points to are borrowed only for the
* duration of the ghostty_terminal_paste() call.
*/
typedef struct {
/** Size of this struct in bytes. */
size_t size;
/**
* The clipboard the contents came from. Reported to the program on a
* paste event (the selection and primary locations are both reported
* as the primary selection, the protocol knows only two); no effect
* on a text paste.
*/
GhosttyClipboardLocation location;
/** Why this paste happened. */
GhosttyPasteSource source;
/**
* Borrowed array of the representations available, in preferred
* order. A text paste writes the first entry with a text MIME type
* such as "text/plain" and ignores the rest. A paste event lists every
* entry's MIME type and never reads data, so non-text entries may have
* empty data. May be NULL when contents_len is zero.
*/
const GhosttyClipboardContent* contents;
/** Number of entries in contents. */
size_t contents_len;
/**
* Write text that could inject commands. Call with false, confirm
* with the user on GHOSTTY_REJECTED, and call again with true.
*/
bool allow_unsafe;
} GhosttyPaste;
/**
* Paste into the terminal according to its current state: a Kitty
* clipboard protocol paste event if mode 5522 is enabled and a
* clipboard_read callback is installed, otherwise the text framed per
* mode 2004. See the group documentation for the full behavior. Output
* goes through the write_pty callback in a single call. The viewport is
* not scrolled; that is up to the embedder, as for key input.
*
* @param terminal The terminal handle
* @param paste The paste request, borrowed for the duration of the call
* @param[out] out_written On success, whether anything was written to
* the pty (the encoded text or a paste event). False means
* there was nothing to paste: no non-empty text
* representation. May be NULL.
* @return GHOSTTY_SUCCESS on success (see @p out_written);
* GHOSTTY_REJECTED if the text could inject commands and
* GhosttyPaste::allow_unsafe is false (nothing was written);
* GHOSTTY_INVALID_VALUE for a NULL terminal or paste, or when no
* write_pty callback is installed; GHOSTTY_OUT_OF_MEMORY;
* GHOSTTY_IO_ERROR if there is no secure entropy source to mint
* a paste event password (wasm32-freestanding without
* GHOSTTY_SYS_OPT_RANDOM_SECURE set), in which case nothing was
* written and no grant was recorded.
*/
GHOSTTY_API GhosttyResult ghostty_terminal_paste(
GhosttyTerminal terminal,
const GhosttyPaste* paste,
bool* out_written);
/**
* Check if paste data is safe to paste into the terminal.
*
@@ -49,7 +168,9 @@ extern "C" {
* to exit bracketed paste mode and inject commands
*
* This check is conservative and considers data unsafe regardless of
* current terminal state.
* current terminal state. ghostty_terminal_paste() applies the
* terminal-state-aware rule itself (newlines are safe inside a
* bracketed paste); use this to apply the stricter rule on top.
*
* @param data The paste data to check (must not be NULL)
* @param len The length of the data in bytes
@@ -74,6 +195,9 @@ GHOSTTY_API bool ghostty_paste_is_safe(const char* data, size_t len);
* GHOSTTY_OUT_OF_SPACE and sets the required size in @p out_written.
* The caller can then retry with a sufficiently sized buffer.
*
* This is the encoder ghostty_terminal_paste() uses for a text paste;
* use it directly when there is no terminal to paste into.
*
* @param data The paste data to encode (modified in place, may be NULL)
* @param data_len The length of the input data in bytes
* @param bracketed Whether bracketed paste mode is active

View File

@@ -724,6 +724,11 @@ struct GhosttyClipboardRead {
* serves a request for only the targets listing (`list` with no `mimes`)
* without prompting.
*
* Installing this callback also enables Kitty paste events (mode 5522):
* ghostty_terminal_paste() sends the program an event instead of the text,
* and the program's follow-up read arrives here with `granted` set since
* the user already pasted. See ghostty_terminal_paste().
*
* @param terminal The terminal handle
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
* @param read Borrowed clipboard read request

View File

@@ -100,6 +100,12 @@ typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_IO_ERROR = -5,
/** Operation failed because encoded input exceeded a configured limit */
GHOSTTY_LIMIT_EXCEEDED = -6,
/**
* Operation was rejected by a safety check (e.g. pasted text that could
* inject commands). Nothing was done. Confirm with the user and retry
* with the operation's allow flag set.
*/
GHOSTTY_REJECTED = -7,
GHOSTTY_RESULT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyResult;