libghostty-vt 0.0.0
Loading...
Searching...
No Matches
Input

Input is everything a person does that a program needs to hear about: keys, mouse events, focus changes and pastes. All of it reaches the program the same way, as bytes on the pty. These functions produce those bytes.

The catch is that the right bytes keep changing. The up arrow is ESC [ A until a program turns on cursor key application mode, and then it's ESC O A. A program can turn on the Kitty keyboard protocol and get a different encoding for its keys. A click is nothing at all until a program turns on mouse tracking, and then it's one of five formats. A paste is framed differently under bracketed paste.

So an encoder has to follow the terminal's modes, or it writes the wrong bytes.

If you have a GhosttyTerminal, following its modes takes one call per encoder. That's the first section, and it's the one thing on this page you have to get right. If you only feed a terminal output and read the result, you can skip this page. You need it as soon as a person can type into your terminal.

Following the terminal's modes

Each kind of input follows the terminal in its own way:

Input How it follows the terminal
Keys ghostty_key_encoder_set_from_terminal() copies the modes into the encoder.
Mouse ghostty_mouse_encoder_set_from_terminal() copies the tracking mode and format into the encoder.
Focus There's no encoder. You check the mode yourself.
Paste ghostty_terminal_paste() reads the modes itself.

The two set_from_terminal calls read the terminal's state when you make them. They don't stay attached to it, and a program can change its modes in any write. So sync the encoder after you write to the terminal, or right before you encode.

// Create a terminal and feed it some VT data that changes modes.
GhosttyTerminal terminal;
ghostty_terminal_new(NULL, &terminal, 80, 24);
// The program might enable the Kitty keyboard protocol here.
ghostty_terminal_vt_write(terminal, vt_data, vt_len);
// Create an encoder and sync its options from the terminal.
ghostty_key_encoder_new(NULL, &encoder);
// Encode a key event using the terminal's modes.
uint8_t buf[128];
size_t written = 0;
ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
GHOSTTY_API GhosttyResult ghostty_key_encoder_set_from_terminal(GhosttyKeyEncoder encoder, GhosttyTerminal terminal)
struct GhosttyKeyEncoderImpl * GhosttyKeyEncoder
Definition key/encoder.h:25
GHOSTTY_API GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder)
GHOSTTY_API void ghostty_key_encoder_free(GhosttyKeyEncoder encoder)
GHOSTTY_API GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, uint8_t *buf, size_t buf_len, size_t *out_written)
struct GhosttyTerminalImpl * GhosttyTerminal
Definition types.h:196
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)

The mouse works the same way with ghostty_mouse_encoder_new(), ghostty_mouse_encoder_set_from_terminal() and ghostty_mouse_encoder_encode().

Each sync leaves some options to you:

  • The key sync replaces all of the encoder's options. That covers cursor key application mode, keypad mode, ignoring the keypad with numlock, the alt escape prefix, backarrow key mode, modifyOtherKeys state and the Kitty keyboard flags. It can't know your macOS option key setting, so it resets GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT to GHOSTTY_OPTION_AS_ALT_FALSE. If you use that option, set it again with ghostty_key_encoder_set() after every sync.
  • The mouse sync sets the tracking mode and output format. It doesn't touch the size or whether a button is held. Those are yours. See Mouse.

Without a terminal, there's nothing to follow. Set the options yourself with ghostty_key_encoder_set() and ghostty_mouse_encoder_set(), as the examples below do.

Keys

We encode keys with the legacy encoding or the Kitty keyboard protocol, depending on the encoder's options.

  1. Create an encoder with ghostty_key_encoder_new().
  2. Configure it with ghostty_key_encoder_set_from_terminal(), or with ghostty_key_encoder_set() if you don't have a terminal.
  3. For each key event, create an event with ghostty_key_event_new(), set its action, key, modifiers and so on, and encode it with ghostty_key_encoder_encode(). Then free it with ghostty_key_event_free(), or change its properties and use it again.
  4. Free the encoder with ghostty_key_encoder_free() when you're done.

This one has no terminal, so it turns on the Kitty keyboard protocol by hand:

int main() {
// Create encoder
GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder);
assert(result == GHOSTTY_SUCCESS);
// Enable Kitty keyboard protocol with all features
&(uint8_t){GHOSTTY_KITTY_KEY_ALL});
// Create and configure key event for Ctrl+C press
result = ghostty_key_event_new(NULL, &event);
assert(result == GHOSTTY_SUCCESS);
assert(result == GHOSTTY_SUCCESS);
result = ghostty_key_event_set_key(event, GHOSTTY_KEY_C);
assert(result == GHOSTTY_SUCCESS);
assert(result == GHOSTTY_SUCCESS);
assert(result == GHOSTTY_SUCCESS);
// Encode the key event
uint8_t buf[128];
size_t written = 0;
result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
assert(result == GHOSTTY_SUCCESS);
// Use the encoded sequence (e.g., write to terminal)
fwrite(buf, 1, written, stdout);
// Cleanup
return 0;
}

Full program: c-vt-encode-key/src/main.c

Not every key event produces output. An unmodified modifier key usually doesn't, for example. Those events return GHOSTTY_SUCCESS with zero bytes written, so check the count before you write to the pty.

Most sequences are short, so a fixed buffer of 128 bytes usually does. If it's too small, you get GHOSTTY_OUT_OF_SPACE with the size you need. Discard what's in the buffer and encode again with a bigger one.

Mouse

We encode mouse events in the X10, UTF-8, SGR, URxvt and SGR-Pixels formats. The steps are the same as for keys:

  1. Create an encoder with ghostty_mouse_encoder_new().
  2. Configure it with ghostty_mouse_encoder_set_from_terminal(), or with ghostty_mouse_encoder_set() if you don't have a terminal.
  3. For each mouse event, create an event with ghostty_mouse_event_new(), set its action, button, modifiers and position, and encode it with ghostty_mouse_encoder_encode(). Then free it with ghostty_mouse_event_free(), or reuse it.
  4. Free the encoder with ghostty_mouse_encoder_free() when you're done.

Event positions are in surface-space pixels, so the encoder has to know your geometry to turn them into cells. Set it with GHOSTTY_MOUSE_ENCODER_OPT_SIZE and a GhosttyMouseEncoderSize. The default is a 1x1 pixel screen with 1x1 pixel cells, which is never what you have. Set GHOSTTY_MOUSE_ENCODER_OPT_ANY_BUTTON_PRESSED as well, to say whether a button is held. Neither one comes from the terminal.

int main() {
// Create encoder
GhosttyResult result = ghostty_mouse_encoder_new(NULL, &encoder);
assert(result == GHOSTTY_SUCCESS);
// Configure SGR format with normal tracking
&(GhosttyMouseFormat){GHOSTTY_MOUSE_FORMAT_SGR});
// Set terminal geometry for coordinate mapping
.size = sizeof(GhosttyMouseEncoderSize),
.screen_width = 800, .screen_height = 600,
.cell_width = 10, .cell_height = 20,
});
// Create and configure a left button press event
result = ghostty_mouse_event_new(NULL, &event);
assert(result == GHOSTTY_SUCCESS);
assert(result == GHOSTTY_SUCCESS);
result = ghostty_mouse_event_set_button(event, GHOSTTY_MOUSE_BUTTON_LEFT);
assert(result == GHOSTTY_SUCCESS);
(GhosttyMousePosition){.x = 50.0f, .y = 40.0f});
assert(result == GHOSTTY_SUCCESS);
// Encode the mouse event
uint8_t buf[128];
size_t written = 0;
result = ghostty_mouse_encoder_encode(encoder, event,
buf, sizeof(buf), &written);
assert(result == GHOSTTY_SUCCESS);
// Use the encoded sequence (e.g., write to terminal)
fwrite(buf, 1, written, stdout);
// Cleanup
return 0;
}

Full program: c-vt-encode-mouse/src/main.c

As with keys, not every event produces output, so check the count. And as with keys, a 128-byte buffer is enough for typical events.

If you turn on GHOSTTY_MOUSE_ENCODER_OPT_TRACK_LAST_CELL, the encoder deduplicates motion by the last cell. ghostty_mouse_encoder_reset() clears that state.

Focus

A program that turns on focus reporting (mode 1004) wants to know when your terminal gains or loses focus. ghostty_focus_encode() encodes that as CSI I or CSI O. There's no encoder to create and no options to set.

It also doesn't look at the terminal, so it's up to you to check the mode first:

if (focus.value) {
// Encode the event and write it to the pty.
}
#define GHOSTTY_MODE_FOCUS_EVENT
Definition modes.h:75
GHOSTTY_API GhosttyResult ghostty_terminal_get(GhosttyTerminal terminal, GhosttyTerminalData data, void *out)
@ GHOSTTY_TERMINAL_DATA_MODE
Definition terminal.h:3022

If the buffer is too small, you get GHOSTTY_OUT_OF_SPACE and the size you need.

int main() {
uint8_t buf[8];
size_t written = 0;
GHOSTTY_FOCUS_GAINED, buf, sizeof(buf), &written);
if (result == GHOSTTY_SUCCESS) {
printf("Encoded %zu bytes: ", written);
fwrite(buf, 1, written, stdout);
printf("\n");
}
return 0;
}

Full program: c-vt-encode-focus/src/main.c

Paste

What a paste writes to the pty depends on the terminal's state, so the way to paste is ghostty_terminal_paste(). You give it the MIME types the clipboard holds (just text/plain for an ordinary paste), a GhosttyMimeReader that produces the data of any one of them, and where the paste came from. The terminal decides how its current modes apply.

The bytes come out of the GHOSTTY_TERMINAL_OPT_WRITE_PTY callback, so you need one installed. See Effects.

What gets written

The terminal sends the program a paste event instead of the data if all three of these are true:

A paste event lists the clipboard's MIME types with a one-time password. No data is read for it. The program then reads what it wants through your clipboard read callback, which arrives with granted set, so you don't need to ask for permission.

Otherwise the terminal writes the first text representation. Unsafe control bytes are replaced with spaces. If mode 2004 (GHOSTTY_MODE_BRACKETED_PASTE) is on, the text is wrapped in bracketed paste sequences. If it's off, newlines are converted to carriage returns.

We only pull data through GhosttyPaste::reader when a representation is actually pasted, so a clipboard holding a large image next to some text costs nothing. The encoded bytes stream to your callback in chunks as they're produced, never in one piece. It may run several times for one paste, and you have to write the pieces to the pty in order.

Unsafe pastes

Some text could inject commands: a newline or carriage return when the paste isn't bracketed, or the bracketed paste terminator when it is. We refuse that text with GHOSTTY_REJECTED and write nothing, unless GhosttyPaste::allow_unsafe is set.

The usual flow is to call once, confirm with the person on GHOSTTY_REJECTED, and call again with allow_unsafe set. Each call reads the text at most once and buffers it whole while the rule is applied. So your source doesn't have to stay the same between reads. The confirmed retry just pastes whatever the source holds by then, and a refused or failed paste writes nothing at all.

// 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 pasted = false;
GhosttyResult result = ghostty_terminal_paste(terminal, &paste, &pasted);
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, &pasted);
}
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", pasted ? "written" : "nothing to paste");
}

Full program: c-vt-paste/src/main.c

Without a terminal

If you encode without a terminal, there are two building blocks. ghostty_paste_is_safe() checks whether paste data contains potentially dangerous sequences. It's conservative, and it ignores terminal state.

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");
}
}

Full program: c-vt-paste/src/main.c

ghostty_paste_encode() encodes paste data for the pty, including bracketed paste wrapping and unsafe byte stripping. It modifies your input buffer in place.

void encode_example() {
// The input buffer is modified in place (unsafe bytes are stripped).
char data[] = "hello\nworld";
uint8_t 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(buf, written);
printf("\n");
}
}

Full program: c-vt-paste/src/main.c