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

An effect is a callback the terminal calls when a program asks for something the terminal can't do by itself. That means things like ringing the bell, changing the window title, writing to the clipboard, or answering a query.

Why callbacks? libghostty-vt has no window, no clipboard and no pty. Those are yours. When a program writes a BEL we know what it means, but we can't make a sound. So we tell you, and you decide what a bell is.

By default, the VT write functions only process sequences that directly change terminal state. Anything with a side effect or a reply is ignored until you register a callback for it. That includes queries, so a program that asks a question gets no answer.

If you're only feeding bytes into a terminal to read the result back, you can skip this page. You need effects as soon as a real program runs in the terminal.

Registering an effect

Each effect has a GhosttyTerminalOption value. Register a callback by passing it to ghostty_terminal_set(), and clear it by passing NULL, which turns the effect off again:

(const void *)on_bell);
// Later, stop listening for the bell.
GHOSTTY_API GhosttyResult ghostty_terminal_set(GhosttyTerminal terminal, GhosttyTerminalOption option, const void *value)
@ GHOSTTY_TERMINAL_OPT_BELL
Definition terminal.h:2058

Set a userdata pointer with GHOSTTY_TERMINAL_OPT_USERDATA and we pass it to every callback, so you can get back to your own application state without global variables. There's one userdata pointer per terminal. You can't give different callbacks different ones.

The rules for callbacks

Callbacks run synchronously, in the middle of ghostty_terminal_vt_write() or ghostty_terminal_vt_write_until(). All of these rules come from that.

You can't write to the same terminal from a callback. The terminal is still working through the first write, and it isn't reentrant. This applies to both ghostty_terminal_vt_write() and ghostty_terminal_vt_write_until().

We don't check for this, and you won't get an error. Your bytes go into the parser in whatever state the first write left it, so what happens depends on the sequence that called you. Bytes written from a title callback are read as part of the OSC that fired it, so text never reaches the screen, and a BEL or ESC in them runs that OSC a second time. Bytes written from a bell callback land ahead of the rest of the first write. Don't depend on any of this.

Don't block, and don't do anything expensive. Further IO processing waits until your callback returns, so a slow callback is a slow terminal.

Copy what you want to keep. Strings and buffers passed to a callback are borrowed, and they're only valid until it returns.

The effects

There are 19 effects:

Option Callback type Trigger
GHOSTTY_TERMINAL_OPT_WRITE_PTY GhosttyTerminalWritePtyFn VT query and mode reports written back to the pty
GHOSTTY_TERMINAL_OPT_BELL GhosttyTerminalBellFn BEL character (0x07)
GHOSTTY_TERMINAL_OPT_TITLE_CHANGED GhosttyTerminalTitleChangedFn Title change via OSC 0 / OSC 2
GHOSTTY_TERMINAL_OPT_PWD_CHANGED GhosttyTerminalPwdChangedFn Pwd change via OSC 7 / OSC 9 / OSC 1337
GHOSTTY_TERMINAL_OPT_ENQUIRY GhosttyTerminalEnquiryFn ENQ character (0x05)
GHOSTTY_TERMINAL_OPT_XTVERSION GhosttyTerminalXtversionFn XTVERSION query (CSI > q)
GHOSTTY_TERMINAL_OPT_SIZE GhosttyTerminalSizeFn XTWINOPS query (CSI 14/16/18/19 t) or mode 2048 enable
GHOSTTY_TERMINAL_OPT_COLOR_SCHEME GhosttyTerminalColorSchemeFn Color scheme query (CSI ? 996 n)
GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES GhosttyTerminalDeviceAttributesFn Device attributes query (CSI c, CSI > c, CSI = c)
GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE GhosttyTerminalClipboardWriteFn Clipboard write via OSC 52 / OSC 1337 / OSC 5522
GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ GhosttyTerminalClipboardReadFn Clipboard read via OSC 52 ? / OSC 5522
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_PROGRAM_STATUS GhosttyTerminalProgramStatusFn Program status report via OSC 7501
GHOSTTY_TERMINAL_OPT_UNKNOWN_SEQUENCE GhosttyTerminalUnknownSequenceFn APC or OSC sequence we don't implement. See Custom Sequences
GHOSTTY_TERMINAL_OPT_RENDER_HOLD GhosttyTerminalRenderHoldFn Synchronized output (mode 2026) begins or ends
GHOSTTY_TERMINAL_OPT_SEMANTIC_PROMPT GhosttyTerminalSemanticPromptFn Shell reports a prompt or command step via OSC 133
GHOSTTY_TERMINAL_OPT_RESET GhosttyTerminalResetFn Full reset (RIS, ESC c)
GHOSTTY_TERMINAL_OPT_RESIZE_WINDOW GhosttyTerminalResizeWindowFn Window resize request (CSI 8 t)

Common callbacks

These are the callbacks most terminals start with. Each one is from the same example program, which prints what it receives.

Replying to the program

Start with this one. Every reply the terminal generates comes through it, such as the answer to a device status report or a mode query. Write the bytes to your pty. The data is only valid during the call, so copy it if you can't write it right away.

void on_write_pty(GhosttyTerminal terminal,
void* userdata,
const uint8_t* data,
size_t len) {
(void)terminal;
(void)userdata;
printf(" write_pty (%zu bytes): ", len);
fwrite(data, 1, len, stdout);
printf("\n");
}

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

The bell

The bell has no data at all. This one counts bells in the userdata pointer.

void on_bell(GhosttyTerminal terminal, void* userdata) {
(void)terminal;
int* count = (int*)userdata;
(*count)++;
printf(" bell! (count=%d)\n", *count);
}

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

Title changes

The callback tells you the title changed, but not what it changed to. You can read the new title with GHOSTTY_TERMINAL_DATA_TITLE inside the callback.

void on_title_changed(GhosttyTerminal terminal, void* userdata) {
(void)userdata;
// The new title is already set, so you can read it here with
// GHOSTTY_TERMINAL_DATA_TITLE. This example reads the cursor position
// to show that the terminal can be queried from a callback.
uint16_t col = 0;
printf(" title changed (cursor at col %u)\n", col);
}

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

Clipboard writes

A write is a request, not a command. You should ask the person using your terminal unless granted is set, and the stream waits until your callback returns while you do.

Answer by calling reply before you return. If you return without replying, the write is denied. Every entry in contents is a representation of the same value, so commit them together. A contents_len of zero asks you to clear the clipboard.

void on_clipboard_write(
GhosttyTerminal terminal,
void* userdata,
const GhosttyClipboardWrite* write) {
(void)terminal;
(void)userdata;
// The write is synchronous: a real embedder would ask the user for
// permission here (unless write->granted) and the VT stream waits until
// this callback returns. The replied result is sent to the program
// (OSC 5522) through the write_pty callback.
printf(" clipboard write (location=%d, contents=%zu)\n",
(int)write->location, write->contents_len);
if (write->contents_len == 0) {
printf(" clear\n");
}
for (size_t i = 0; i < write->contents_len; i++) {
const GhosttyClipboardContent* content = &write->contents[i];
printf(" ");
if (content->mime.len > 0) {
fwrite(content->mime.ptr, 1, content->mime.len, stdout);
}
printf(" (%zu bytes): ", content->data.len);
if (content->data.len > 0) {
fwrite(content->data.ptr, 1, content->data.len, stdout);
}
printf("\n");
}
.size = sizeof(reply),
.remember = false,
};
write->reply(write, &reply);
}

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

Clipboard reads

Reads work the same way. Ask first unless granted is set, and call reply before you return. If you return without replying, the program gets an empty clipboard for OSC 52 and EPERM for OSC 5522.

void on_clipboard_read(
GhosttyTerminal terminal,
void* userdata,
const GhosttyClipboardRead* read) {
(void)terminal;
(void)userdata;
// The read is synchronous: a real embedder would ask the user for
// permission here (unless read->granted) and the VT stream waits until
// this callback returns. The reply is sent to the program through the
// write_pty callback.
printf(" clipboard read (location=%d, mimes=%zu)\n",
(int)read->location, read->mimes_len);
for (size_t i = 0; i < read->mimes_len; i++) {
printf(" ");
fwrite(read->mimes[i].ptr, 1, read->mimes[i].len, stdout);
printf("\n");
}
// Reply with every requested representation we have. This example only
// has text.
const char* text = "Hello from the clipboard";
.mime = {.ptr = (const uint8_t*)"text/plain", .len = 10},
.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);
}

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

Sequences we don't implement

The callback for unknown sequences has more to it than the others, so it has its own guide. See Custom Sequences.

Putting it together

This registers the callbacks above, along with the unknown sequence callback, and then writes a sequence for each one so you can watch them fire.

int main() {
// Create a terminal
GhosttyTerminal terminal = NULL;
if (ghostty_terminal_new(NULL, &terminal, 80, 24) != GHOSTTY_SUCCESS) {
fprintf(stderr, "Failed to create terminal\n");
return 1;
}
// Set up userdata — a simple bell counter
int bell_count = 0;
// Register effect callbacks
(const void *)on_write_pty);
(const void *)on_bell);
(const void *)on_title_changed);
(const void *)on_clipboard_write);
(const void *)on_clipboard_read);
(const void *)on_unknown_sequence);
// Unknown sequence capture is independently bounded and disabled by
// default. This limit will apply to every supported unknown sequence type.
size_t unknown_max_bytes = 256;
&unknown_max_bytes);
// Feed VT data that triggers effects:
// 1. Bell (BEL = 0x07)
printf("Sending BEL:\n");
const uint8_t bel = 0x07;
ghostty_terminal_vt_write(terminal, &bel, 1);
// 2. Title change (OSC 2 ; <title> ST)
printf("Sending title change:\n");
const char* title_seq = "\x1B]2;Hello Effects\x1B\\";
ghostty_terminal_vt_write(terminal, (const uint8_t*)title_seq,
strlen(title_seq));
// 3. Device status report (DECRQM for wraparound mode ?7)
// triggers write_pty with the response
printf("Sending DECRQM query:\n");
const char* decrqm = "\x1B[?7$p";
ghostty_terminal_vt_write(terminal, (const uint8_t*)decrqm,
strlen(decrqm));
// 4. Clipboard write (OSC 52 ; c ; <base64 data> ST)
printf("Sending clipboard write:\n");
const char* clipboard_seq =
"\x1B]52;c;SGVsbG8gY2xpcGJvYXJk\x1B\\";
ghostty_terminal_vt_write(terminal, (const uint8_t*)clipboard_seq,
strlen(clipboard_seq));
// 5. Clipboard read (OSC 52 ; c ; ? ST)
printf("Sending clipboard read:\n");
const char* clipboard_read_seq = "\x1B]52;c;?\x1B\\";
ghostty_terminal_vt_write(terminal, (const uint8_t*)clipboard_read_seq,
strlen(clipboard_read_seq));
// 6. Unsupported APC sequence
printf("Sending unknown APC:\n");
const char* unknown_apc = "\x1B_private-command;payload\x1B\\";
ghostty_terminal_vt_write(terminal, (const uint8_t*)unknown_apc,
strlen(unknown_apc));
printf("Sending unknown OSC:\n");
const char* unknown_osc = "\x1B]7400;status=busy\x07";
ghostty_terminal_vt_write(terminal, (const uint8_t*)unknown_osc,
strlen(unknown_osc));
// 7. Another bell to show the counter increments
printf("Sending another BEL:\n");
ghostty_terminal_vt_write(terminal, &bel, 1);
printf("Total bells: %d\n", bell_count);
return 0;
}

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