This implements only the clipboard _write_ side of the Kitty clipboard
protocol for libghostty-vt. libghostty users don't need to do anything,
this all automatically works since it just piggy-backs on the previous
clipboard write effect.
Clipboard reading is far more complicated because we don't have anything
designed yet for libghostty-vt that does async requests (e.g. to ask the
user for permission). I need to think about that more.
This adds a `clipboard_read` effect to the stream terminal handler and a
matching `GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ` callback to the
libghostty-vt C API so that embedders can answer OSC 52 read requests
(the `?` payload).
This is a _blocking_ effect: if the embedder needs to ask the user for
permission, the entire VT processing pipeline is _blocked_ during the
callback. This is a purposeful simplification choice compared to how
Ghostty GUI works with async requests. I think its reasonable, it
eliminates a TON of complexity.
If the effect isn't set, then any clipboard reads are denied.
This can be expanded easily to support Kitty clipboard protocol later.
This adds all the core logic and tests for the full Kitty Clipboard
protocol in the `src/terminal` package.
This is purposefully shaped similarly to the way we organize Kitty
graphics. There is an umbrella `clipboard.zig` and then a bunch of leaf
zig files that cover: request parsing, response encoding, state
management, etc. I think that worked really well for Kitty graphics so
we're doing it here too.
The core logic covers every part of the protocol: read and write.
The only thing hooked up to the end user is a DECRQM for mode 5522 will
return unset. And it can't be set currently (since it never works yet).
Outside of that, nothing in this diff is actually used in the real
binary.
**AI usage:** Validation against the spec and Kitty impl, test writing
and coverage validation, of course some code writing but within the
broad organizational shape I defined. I went through and either rewrote
or wrote all the comments myself plus this PR message.
Add a snapshot decoder option that leaves continuation tracking
enabled on decoded terminals. This lets caller access the continuation
bytes (if any) that were applied to the terminal.
This lets replay callers export an unfinished parser or UTF-8 sequence
from the returned terminal.
This defaults to off.
Replace a bunch of type-specific Wasm allocation functions with a generic
byte allocator and reusable opaque out-parameters for pointers. This
makes it a lot more ergonomic (relatively) to use the Wasm interface
and removes a dozen or so exports.
This also updates the `ghostty_type_json` `abi` field with a maximum
alignment value that host sides can use to keep every allocation aligned
properly, easily, without hardcoding numbers.
This adds a test to verify this all works as intended and runs in CI.
GhosttyCell was exposed as a raw integer while its manifest entry was only an alias, forcing bulk-read consumers to duplicate the internal cell bit layout.\n\nAdd reflection helpers for packed structs and tagged unions, and keep the C-facing layout metadata next to Cell itself. Extend the ABI manifest and schema with recursive bit descriptors so every content arm, including palette and RGB backgrounds, can be decoded without hardcoded masks.\n\nDocument manifest-driven cell decoding and test the metadata against Zig reflection and real cell values.
The ABI manifest previously had no machine-readable grammar or test that
the public export conformed to it.
Define a Draft 2020-12 schema and add a build check that executes
ghostty_type_json for native and wasm libraries before validation. Run
both forms in CI and publish the schema with the generated API docs.
The type metadata export only described extern struct layouts, leaving embedders to mirror enum values and tagged union relationships.
Describe every public C type in a versioned manifest with target and build metadata. Keep union field renames alongside their source tagged unions so the manifest uses public C names without changing Zig value layouts.
Add render state C APIs for iterating only rows that require a redraw
and for marking a completed frame clean in one call.
A one-row update in a 24-row viewport reduces dirty-row discovery from
50 calls to two, while cleanup becomes one call instead of O(N) of rows.
This lower call count is massive for environments where FFI is expensive
(Go, wasm).
The dirty next API outputs the viewport y because it jumps, unlike the
normal sequential next where its trivial for a caller to keep track.
Add render state C APIs for iterating only rows that require a redraw
and for marking a completed frame clean in one call.
A one-row update in a 24-row viewport reduces dirty-row discovery from
50 calls to two, while cleanup becomes one call instead of O(N) of rows.
This lower call count is massive for environments where FFI is expensive
(Go, wasm).
The dirty next API outputs the viewport y because it jumps, unlike the
normal sequential next where its trivial for a caller to keep track.
A normal renderer would have to call `ghostty_render_state_get`
_eight times_ to reconstruct the cursor. In languages where FFI is
expensive (Go, wasm, etc.), this showed up in profiles of every frame.
Add a sized cursor snapshot and expose it. Also expose the existing color
snapshot through ghostty_render_state_get and remove the older
dedicated color getter.
This makes the `ghostty_render_state_*` C API significantly faster on
wasm32-freestanding, measured in V8 via Node for Chrome. Also verified
in `jsc` for Safari.
The major change is a new bulk row read API that makes full-screen cell reads
roughly 10x faster for wasm embedders. This should help any embedder with
high FFI overhead, such as Go, Python, etc. too.
Non-wasm performance is not impacted, all benchmarks were run on my mac
too w/ no regressions (two of the changes are native wins as well).
## Changes
* color: the "vectorized" palette conversion loop was silently
scalarized by LLVM into per-byte ops because it loaded/stored through
array-typed pointers. Zig 0.16 disables the LLVM loop vectorizer, so
manually vectorized loops must go through vector-typed pointers.
* C styles: major optimizations to converting Zig styles to C styles.
This is a heavy operation for render state.
* render: `endUpdate`'s style-run fill (`@memset` with a struct value)
re-loaded its source every iteration and stored field by field. Now
manually vectorized.
* render: new `GHOSTTY_RENDER_STATE_ROW_DATA_CELLS_RAW` returns a
borrowed `GhosttyCellsView` of the current row's raw cell values, valid
until the next update. One call per row instead of 3-6 calls per cell.
## Benchmarks
| Benchmark | Before | After | Speedup |
|---|---|---|---|
| colors_get | 114 ns | 35 ns | 3.3x |
| style get, per styled cell | 7.8 ns | 6.7 ns | 1.2x |
| raw+style read, per cell | 8.6 ns | 7.7 ns | 1.1x |
| full-screen text read, per cell | 7.5 ns | 0.7 ns | 10.7x |
| full-screen text+style read, per cell | 8.6 ns | 1.7 ns | 5.1x |
| render state update, styled full frame | 3.4 us | 2.6 us | 1.3x |
**AI usage:** Fable did the implementation and benchmarking and drafted
this message. Comments were partially rewritten by me.
Use an immediately invoked lambda for GHOSTTY_INIT_SIZED in C++ so the
macro value-initializes every field before setting the ABI size. The
previous C compound literal and designated initializer required compiler
extensions in C++17 and C++20.
Keep the existing standard compound literal for C callers.
Use fixed int enum types for C++11, C23, Clang's fixed-enum extension,
and GCC 13 or newer. Previously only finalized C23 mode selected an explicit
underlying type, leaving C++ and common older C modes with
implementation-defined enum types.
This adds new functions to both C and Zig to write VT data until the
VT parser reaches a "ground" state. The ground state is when the
parser/stream is stateless: between all partial UTF-8, OSC, CSI, etc.
This lets embedders safely interleave custom VT sequences from multiple
sources. A practical example is a standard terminal reading from a pty
that is then doing custom APC or something mid-stream for their emulator
client.
This PR extends the `open_config` keybind action to allow editing the
Ghostty config in a new Ghostty window using the editor configured in
`$EDITOR` or `$VISUAL`.
Its moniker has been `libghostty-internal` for *quite* a while now among
maintainers but that has never really been clarified for the public aside
from a couple comments on discussions. Judging by how many people still
try to vibe their way into making this work for their purposes, I think
we should clear this up once and for all.
This PR adds a `+new-tab` CLI action, useful for automation on GTK. This
mainly re-uses machinery added for the `+new-window`, but adds in a
unique surface ID for identifying surfaces for IPC purposes (and
eliminates use of raw pointers for callbacks from notifications).
Ghostty's full termio path answers XTGETTCAP from the static terminfo
map, but terminal/stream_terminal.zig, which backs libghostty-vt,
parses the same DCS request and then discards it. There is no XTGETTCAP
effect either, so an embedder cannot restore the replies through the
C API.
Programs query these over SSH instead of assuming the remote host has
the client's terminfo entry. This matters more for an embedder than for
the desktop app, which can install its entry on the remote through
shell integration.
Answer the queries in stream_terminal the same way termio does: look
up each requested key in the static terminfo map and write the reply
to the pty, skipping the lookups entirely when no write_pty effect is
set. The map now stores null-terminated responses so they can be
handed straight to write_pty without copying. terminal/dcs.zig and the
termio path are unchanged.
"TN" is handled separately. It names the terminfo entry the terminal
runs as, so it has to agree with TERM -- which is set in
termio/Exec.zig, a layer libghostty-vt does not contain. The library
never sees TERM and cannot answer on the embedder's behalf, and
answering with Ghostty's own entry from the static map would misreport
every embedder, so "TN" is intercepted before the map lookup. The name
is instead configured through a new option,
GHOSTTY_TERMINAL_OPT_TERMINFO_NAME: the string is copied into the
terminal, names longer than 128 bytes are rejected, and while unset
the query goes unanswered.
This is the first dependency from src/terminal on src/terminfo, so
libghostty-vt now carries Ghostty's terminfo table: +16,023 bytes
(+1.9%) on a wasm32-freestanding ReleaseSmall build.
Signed-off-by: Fredrik Fornwall <fredrik@fornwall.net>
Remove BLAKE3 prefix digests. Keep READY/FINISH as empty records since
they're semantically important markers.
Our existing format (CRC32 per-record, declared counts, strict tag ordering
requirements, etc.) already detect: accidental corruption, truncation,
data omission, and duplication.
BLAKE3 only protects against valid records being swapped or removed entirely.
It is heavy for just that, and callers can solve that anyways via their
own transport (like, just use TCP). For more adversarial protection,
callers can also add layers like TLS or their own alternate signing
methods depending on their own threat models.
Removing the hash improves encode times by ~1.4x, decode times by ~1.3x.
Time-to-READY decoding is effectively unchanged because it was such a
small package to begin with.
ABI BREAKING: This removes `ghostty_terminal_mode_get` and `_mode_set`.
We can now represent these operations completely with standard
`ghostty_terminal_get` and `ghostty_terminal_set`, which makes it much
more flexible to preserve ABI in the future.
This is all centered around a new `GhosttyTerminalModeConfig` structure
that is an in or out parameter depending on use case.
This also adds a new `GHOSTTY_TERMINAL_OPT_MODE_DEFAULT` option that
can be used to set the _default_ value of mode that happens when a RIS
event (full reset) is sent.
OSC8 hyperlinks previously executed directly via the NSWorkspace opener
so a malicious application can just do whatever it wanted and trick the
user into opening something through Launch Services.
This PR notifies apprt of OSC8 hyperlinks so they can be handled
specially. In this PR, I added macOS-specific handling of OSC8 through a
variety of improvements:
- Preview text is sanitized, so invisible Unicode characters now show.
- Questionable-looking URLs require confirmation to open, but a user can
confirm to open.
- Very questionable or definitely unsafe URLs are blocked with an alert
that only allows the user to copy the link. The alert also notifies the
user why.
This PR also adds an explicit `link-osc8` config (default true) that
users can use to disable osc8 completely.
## Demos
### Custom URL Schemes (Confirm)
<img width="1432" height="1110" alt="CleanShot 2026-08-05 at 10 25
57@2x"
src="https://github.com/user-attachments/assets/f7773ca2-3389-4749-a5c9-393ae097c044"
/>
### Invisible Characters (Block)
<img width="1432" height="1110" alt="CleanShot 2026-08-05 at 10 26
44@2x"
src="https://github.com/user-attachments/assets/bd2d0f33-f128-46e8-9bdb-227afecbb942"
/>
### Executable Target (Block)
<img width="1432" height="1110" alt="CleanShot 2026-08-05 at 10 27
31@2x"
src="https://github.com/user-attachments/assets/080c0524-2c8e-4931-892f-d2643a5d0d4e"
/>
OSC8 hyperlinks previously executed directly via the NSWorkspace opener
so a malicious application can just do whatever it wanted and trick the
user into opening something through Launch Services.
This PR notifies apprt of OSC8 hyperlinks so they can be handled
specially. In this PR, I added macOS-specific handling of OSC8 through a
variety of improvements:
- Preview text is sanitized, so invisible Unicode characters now show.
- Questionable-looking URLs require confirmation to open, but a user
can confirm to open.
- Very questionable or definitely unsafe URLs are blocked with an
alert that only allows the user to copy the link. The alert also
notifies the user why.
Add an explicit libghostty-vt title-report option and keep CSI 21 t
disabled unless an embedder enables it.
Previously, registering the general PTY write callback also caused the
terminal to echo attacker-controlled window titles. This exposed
embedders to command injection after user interaction.
Gate the response in the shared terminal stream, append the C API
option without renumbering existing values, and cover the default,
opt-in, and reset behavior in Zig and C API tests.
Represent Kitty image data as a complete/pending tagged union. Kitty
images can now be completed _later_ if we have all their other metadata
up front.
This will be used by the snapshot API to transmit lightweight
information up front so that renderers of the snapshot can show
placeholders and accept mutating pty data, while the real image data
streams in later.
No user-visible behavior changes today.
Represent Kitty image data as a complete/pending tagged union.
Kitty images can now be completed _later_ if we have all their other
metadata up front.
This will be used by the snapshot API to transmit lightweight
information up front so that renderers of the snapshot can show
placeholders and accept mutating pty data, while the real image data
streams in later.
Adds "Copy" and "Export to file" buttons to the Terminal IO inspector
so recorded VT events can be saved outside the app for sharing or
analysis.
Export is wired up through a new export_terminal_io apprt action,
handled with a native save dialog on both macOS and GTK.
Applications cannot infer whether an unfocused terminal remains visible, so
focus reports are insufficient for avoiding expensive rendering while a
view is hidden.
Implement private mode 2033 and the visibility query/report sequences.
Track conservative per-surface visibility, report every effective change
while enabled, and always answer explicit queries and mode enables. Keep
view visibility across terminal resets because it is owned by the host,
not terminal state.
Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019fa965-aa5f-7099-85b4-a9679d2c8bd3
C API callers could configure runtime scrollback limits but could not
read them back. Add terminal data keys for the primary screen byte and
line configurations.
Return GHOSTTY_NO_VALUE for unlimited limits and keep reads stable while
an alternate screen is active. Document the configured-value semantics
and add focused coverage for defaults, updates, and unlimited values.
Terminal construction previously accepted GhosttyTerminalOptions with
dimensions and one scrollback byte limit. Remove the options struct from
the ABI and make ghostty_terminal_new accept columns and rows directly.
Add byte and line limit options to ghostty_terminal_set and forward them
to the runtime Terminal setters. NULL removes a limit, while zero bytes
disables scrollback. Update type metadata, tests, and all API examples.