Add iOS device and simulator slices to the xcframework, gated on
SDK availability via std.zig.LibCInstallation.findNative. Refactor
AppleLibs from a struct with named fields to an EnumMap keyed by
ApplePlatform so that adding new platforms only requires extending
the enum and its sdk_platforms table.
tvOS, watchOS, and visionOS are listed as not yet supported due to
Zig stdlib limitations (missing PATH_MAX, mcontext fields).
Add a build-lib-vt-xcframework job to the release-tip workflow that
builds the universal xcframework with ReleaseFast, zips it, signs
it with minisign, and uploads it to both the GitHub Release and R2
blob storage. Consumers can pull the xcframework zip from the tip
release or by commit hash from tip.files.ghostty.org.
Auto-discover Swift examples via example/*/Package.swift alongside
the existing zig and cmake discovery. The new build-examples-swift
job runs on macOS, builds the xcframework with zig build -Demit-lib-vt,
then runs swift run in each example directory to verify the
xcframework links and functions correctly end-to-end.
The xcframework now generates its own headers directory with a
GhosttyVt module map instead of reusing include/ directly, which
contains the GhosttyKit module map for the macOS app. The generated
directory copies the ghostty headers and adds a module.modulemap
that exposes ghostty/vt.h as the umbrella header.
A new swift-vt-xcframework example demonstrates consuming the
xcframework from a Swift Package. It creates a terminal, writes
VT sequences, and formats the output as plain text, verifying
the full round-trip works with swift build and swift run.
On Darwin targets, the build now automatically produces a universal
(arm64 + x86_64) XCFramework at lib/ghostty-vt.xcframework under
the install prefix. This bundles the fat static library with headers
so consumers using Xcode or Swift PM can link libghostty-vt directly.
Based on the Ghostling implementation, these are APIs that will help
other implementors:
**Z-layer filtering.** The placement iterator now supports a
configurable layer filter via a new
`ghostty_kitty_graphics_placement_iterator_set()` option API. When a
layer is set, `ghostty_kitty_graphics_placement_next()` skips placements
whose z-index doesn't match the requested layer. The three layers follow
the kitty protocol z-index conventions (below background, below text,
above text) and map directly to distinct rendering passes. Default is
`ALL` (no filtering, existing behavior).
**Viewport-relative positioning.**
`ghostty_kitty_graphics_placement_viewport_pos()` converts a placement's
internal pin to viewport-relative grid coordinates. The row value can be
negative for placements that have partially scrolled above the viewport.
Returns `GHOSTTY_NO_VALUE` when the placement is entirely off-screen or
is a virtual (unicode placeholder) placement, so the renderer can skip
it without extra math.
**Source rectangle resolution.**
`ghostty_kitty_graphics_placement_source_rect()` applies kitty protocol
semantics (0 = full image dimension) and clamps to image bounds,
returning pixel coordinates ready for texture sampling.
## New APIs
| Function | Description |
|----------|-------------|
| `ghostty_kitty_graphics_placement_iterator_set` | Set an option on a
placement iterator (currently: z-layer filter) |
| `ghostty_kitty_graphics_placement_viewport_pos` | Get
viewport-relative grid position of the current placement |
| `ghostty_kitty_graphics_placement_source_rect` | Get the resolved
source rectangle in pixels for the current placement |
## New Types
| Type | Description |
|------|-------------|
| `GhosttyKittyPlacementLayer` | Z-layer classification: `ALL`,
`BELOW_BG`, `BELOW_TEXT`, `ABOVE_TEXT` |
| `GhosttyKittyGraphicsPlacementIteratorOption` | Settable iterator
options (currently: `LAYER`) |
Fix three categories of test bugs in the kitty graphics C API tests:
The placement iterator reset in getTyped was clobbering the
layer_filter field when reinitializing the iterator struct,
causing the layer filter test to see unfiltered placements.
Preserve layer_filter across resets.
The viewport position tests were not accounting for the default
cursor_movement=after behavior of the kitty display command,
which calls index() for each row of the placement before the
test scroll sequence. Add C=1 to suppress cursor movement so
the scroll math in the tests is correct.
The source_rect tests used an 88-character all-A base64 payload
which decodes to 66 bytes, but a 4x4 RGBA image requires exactly
64 bytes. Fix the payload to use proper base64 padding (AA==).
Add ghostty_kitty_graphics_placement_source_rect which returns the
fully resolved and clamped source rectangle for a placement. This
applies kitty protocol semantics (width/height of 0 means full
image dimension) and clamps the result to the actual image bounds,
eliminating ~20 lines of protocol-aware logic from each embedder.
Add ghostty_kitty_graphics_placement_viewport_pos which converts a
placement's internal pin to viewport-relative grid coordinates.
The returned row can be negative when the placement's origin has
scrolled above the viewport, allowing embedders to compute the
correct destination rectangle for partially visible images.
Returns GHOSTTY_NO_VALUE only when the placement is completely
outside the viewport (bottom edge above the viewport or top edge
at or below the last row), so embedders do not need to perform
their own visibility checks. Partially visible placements always
return GHOSTTY_SUCCESS with their true signed coordinates.
Add a placement_iterator_set function that configures iterator
properties via an enum, following the same pattern as other set
functions in the C API (e.g. render_state_set). The first settable
option is a z-layer filter.
The GhosttyKittyPlacementLayer enum classifies placements into three
layers based on kitty protocol z-index conventions: below background
(z < INT32_MIN/2), below text (INT32_MIN/2 <= z < 0), and above text
(z >= 0). The default is ALL which preserves existing behavior.
When a layer filter is set, placement_iterator_next automatically
skips non-matching placements, so embedders no longer need to
reimplement the z-index bucketing logic or iterate all placements
three times per frame just to filter by layer.
This adds a C API for inspecting Kitty graphics image storage, images,
and placements from a terminal instance.
I think this is enough of the API surface area for a renderer to draw
images. But I'll have to add it to Ghostling to be sure.
## Example
```c
#include <stdint.h>
#include <stdio.h>
#include <ghostty/vt.h>
/* After creating a terminal and transmitting a Kitty graphics image... */
/* Get the kitty graphics storage from the terminal. */
GhosttyKittyGraphics graphics = NULL;
ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS, &graphics);
/* Iterate over all placements. */
GhosttyKittyGraphicsPlacementIterator iter = NULL;
ghostty_kitty_graphics_placement_iterator_new(NULL, &iter);
ghostty_kitty_graphics_get(graphics,
GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR, &iter);
while (ghostty_kitty_graphics_placement_next(iter)) {
uint32_t image_id = 0;
ghostty_kitty_graphics_placement_get(iter,
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID, &image_id);
/* Look up the image and query its properties. */
GhosttyKittyGraphicsImage image = ghostty_kitty_graphics_image(graphics, image_id);
uint32_t width = 0, height = 0;
GhosttyKittyImageFormat format = 0;
ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_WIDTH, &width);
ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_HEIGHT, &height);
ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_FORMAT, &format);
printf("image %u: %ux%u format=%d\n", image_id, width, height, format);
/* Compute rendered pixel size and grid size. */
uint32_t px_w, px_h, cols, rows;
ghostty_kitty_graphics_placement_pixel_size(iter, image, terminal, &px_w, &px_h);
ghostty_kitty_graphics_placement_grid_size(iter, image, terminal, &cols, &rows);
printf(" rendered: %ux%u px, %ux%u cells\n", px_w, px_h, cols, rows);
}
ghostty_kitty_graphics_placement_iterator_free(iter);
```
## API
### Functions
| Function | Description |
|----------|-------------|
| `ghostty_kitty_graphics_get` | Query data from a kitty graphics
storage (e.g. placement iterator) |
| `ghostty_kitty_graphics_image` | Look up an image by its image ID |
| `ghostty_kitty_graphics_image_get` | Query image properties (ID,
dimensions, format, compression, pixel data) |
| `ghostty_kitty_graphics_placement_iterator_new` | Create a new
placement iterator |
| `ghostty_kitty_graphics_placement_iterator_free` | Free a placement
iterator |
| `ghostty_kitty_graphics_placement_next` | Advance the iterator to the
next placement |
| `ghostty_kitty_graphics_placement_get` | Query placement properties
(image ID, offsets, source rect, z-index, etc.) |
| `ghostty_kitty_graphics_placement_rect` | Compute the bounding grid
rectangle for a placement |
| `ghostty_kitty_graphics_placement_pixel_size` | Compute the rendered
pixel dimensions of a placement |
| `ghostty_kitty_graphics_placement_grid_size` | Compute the grid cell
dimensions of a placement |
### Types
| Type | Description |
|------|-------------|
| `GhosttyKittyGraphics` | Opaque handle to image storage (borrowed from
terminal) |
| `GhosttyKittyGraphicsImage` | Opaque handle to a single image |
| `GhosttyKittyGraphicsPlacementIterator` | Opaque handle to a placement
iterator |
| `GhosttyKittyGraphicsData` | Enum for `ghostty_kitty_graphics_get`
data kinds |
| `GhosttyKittyGraphicsImageData` | Enum for `ghostty_kitty_image_get`
data kinds |
| `GhosttyKittyGraphicsPlacementData` | Enum for
`ghostty_kitty_graphics_placement_get` data kinds |
| `GhosttyKittyImageFormat` | Image pixel format (RGB, RGBA, PNG, gray,
gray+alpha) |
| `GhosttyKittyImageCompression` | Image compression (none, zlib) |
Add the inverse of ghostty_terminal_grid_ref(), converting a grid
reference back to coordinates in a requested coordinate system
(active, viewport, screen, or history). This wraps the existing
internal PageList.pointFromPin and is placed on the terminal API
since it requires terminal-owned PageList state to resolve the
top-left anchor for each coordinate system.
Returns GHOSTTY_NO_VALUE when the ref falls outside the requested
range, e.g. a scrollback ref cannot be expressed in active
coordinates.
The PlacementIterator, PlacementMap, and PlacementIteratorWrapper
types in the C API were unconditionally referencing
kitty_storage.ImageStorage, which transitively pulled in
Image.transmit_time (std.time.Instant). On wasm32-freestanding,
std.time.Instant requires posix.timespec which does not exist,
causing a compilation error.
Gate these types behind build_options.kitty_graphics, matching the
existing pattern used for KittyGraphics and ImageHandle. When
kitty graphics is disabled, they fall back to opaque/void types.
Add early-return guards to placement_iterator_new and
placement_iterator_free which directly operate on the wrapper
struct.
The test transmits an image with f=24 (24-bit RGB) but was asserting
that the format field equals .rgba (32-bit). Corrected the expectation
to .rgb to match the transmitted pixel format.
Expose Placement.pixelSize() and Placement.gridSize() as new C API
functions ghostty_kitty_graphics_placement_pixel_size() and
ghostty_kitty_graphics_placement_grid_size(). Both take the placement
iterator, image handle, and terminal, returning their results via
out params.
Rename the internal Zig method from calculatedSize to pixelSize to
pair naturally with gridSize — one returns pixels, the other grid
cells. Updated all callers including the renderer.
Expose Placement.rect() from the Zig kitty graphics storage as a new
C API function ghostty_kitty_graphics_placement_rect(). It takes the
terminal, image handle, and a positioned placement iterator, and
writes the bounding grid rectangle into a GhosttySelection out param.
Virtual placements return GHOSTTY_NO_VALUE.
Move all opaque handle typedefs (GhosttyTerminal, GhosttyKittyGraphics,
GhosttyRenderState, GhosttySgrParser, GhosttyFormatter, GhosttyOsc*)
into types.h so they are available everywhere without circular includes
and Doxygen renders them in the correct @ingroup sections.
Convert the Transmission.Format, Transmission.Medium, and
Transmission.Compression types from plain Zig enums to lib.Enum so
they get a C-compatible backing type when building with c_abi. This
lets the C API layer reuse the types directly instead of maintaining
separate mirror enums.
Move Format.bpp() to a standalone Transmission.formatBpp() function
since lib.Enum types cannot have decls.
In the C API layer, rename kitty_gfx to kitty_storage and command to
kitty_cmd for clarity, and simplify the format/compression getters
to direct assignment now that the types are shared.
Add a GhosttyKittyGraphicsImage opaque type and API for looking up
images by ID and querying their properties. This complements the
existing placement iterator by allowing direct image introspection.
The new ghostty_kitty_graphics_image() function looks up an image by
its ID from the storage, returning a borrowed opaque handle. Properties
are queried via ghostty_kitty_image_get() using the new
GhosttyKittyGraphicsImageData enum, which exposes id, number, width,
height, format, compression, and a borrowed data pointer with length.
Format and compression are exposed as their own C enum types
(GhosttyKittyImageFormat and GhosttyKittyImageCompression) rather
than raw integers.
Add a C API for iterating over Kitty graphics placements via the
new GhosttyKittyGraphics opaque handle. The API follows the same
pattern as the render state row iterator: allocate an iterator with
ghostty_kitty_graphics_placement_iterator_new, populate it from a
graphics handle via ghostty_kitty_graphics_get with the
PLACEMENT_ITERATOR data kind, advance with
ghostty_kitty_graphics_placement_next, and query per-placement
fields with ghostty_kitty_graphics_placement_get.
This exposes the APIs necessary to enable Kitty image protocol parsing
and state from the C API.
* You can now set the PNG decoder via the `ghostty_sys_set` API.
* You can set Kitty image configs via `ghostty_terminal_set` API.
* An example showing this working has been added.
* **You cannot yet query Kitty images for metadata or rendering.** I'm
going to follow that up in a separate PR.
Demonstrates the sys interface for Kitty Graphics Protocol PNG
support. The example installs a PNG decode callback via
ghostty_sys_set, creates a terminal with image storage enabled,
and sends an inline 1x1 PNG image through vt_write. Snippet
markers are wired up to the sys.h doxygen docs.
The terminal sys module provides runtime-swappable function pointers
for operations that depend on external implementations (e.g. PNG
decoding). This exposes that functionality through the C API via a
ghostty_sys_set() function, modeled after the ghostty_terminal_set()
enum-based option pattern.
Embedders can install a PNG decode callback to enable Kitty Graphics
Protocol PNG support. The callback receives a userdata pointer
(set via GHOSTTY_SYS_OPT_USERDATA) and a GhosttyAllocator that must
be used to allocate the returned pixel data, since the library takes
ownership of the buffer. Passing NULL clears the callback and
disables the feature.
The previous version requested general notification permissions but
omitted the `.badge` option. Because the initial request was granted,
`settings.authorizationStatus` returns `.authorized`, leading the app to
believe it has full notification privileges when it actually lacks the
authority to update the dock icon badge.
Debug hint:
You can reset the notification settings by right-clicking on the app
name.
<img width="307" height="85" alt=""
src="https://github.com/user-attachments/assets/660cd332-eda6-45d6-8bfd-a6f9e28e21e8"
/>
`updateOSView` assumed SwiftUI always propagates frame changes to the
scroll view. Under system load, this can be deferred, leaving the
surface rendering at stale dimensions. Check for size mismatch and mark
layout as needed.
<img width="1408" height="464" alt="ghostty_bug"
src="https://github.com/user-attachments/assets/3a6f81ff-9d02-4ffa-aded-e2eddc9f40a5"
/>
---
AI Disclosure: Used Claude Code for PR preparation.
Add four new terminal options for configuring Kitty graphics at runtime
through the C API: storage limit, and the three loading medium flags
(file, temporary file, shared memory).
The storage limit setter propagates to all initialized screens and
uses setLimit which handles eviction when lowering the limit. The
medium setters similarly propagate to all screens. Getters read from
the active screen. All options compile to no-ops or return no_value
when kitty graphics are disabled at build time.
This enables Kitty Graphics for `libghostty-vt` for the Zig API (C to
come next).
First, a note on security: by default, Kitty graphics will only allow
images transferred via the _direct_ medium (directly via the pty) and
will not allow file or shared memory based images. libghostty-vt
consumers need to manually opt-in via terminal init options or
`terminal.setKittyGraphicsLoadingLimits` to enable file-based things.
**This is so we're as secure as possible by default.**
Second, for PNG decoding, embedders must now set a global
runtime-callback at `ghostty.sys.decode_png`. If this is not set, PNG
formatted images are rejected. If this is set, then we'll use this to
decode and embedders can use any decoder they want.
There is no C API exposed yet to set this, so this is only for Zig to
start.
## Examples (Zig)
### Configuring Allowed Formats
```zig
var term = try Terminal.init(alloc, .{
.cols = 80,
.rows = 24,
// Only allow direct (inline) image data, no file/shm access.
// This is the default so you don't need to specify it.
.kitty_image_loading_limits = .direct,
});
```
```zig
var term = try Terminal.init(alloc, .{
.cols = 80,
.rows = 24,
// Allow all transmission mediums: direct, file, temporary file, shared memory.
.kitty_image_loading_limits = .all,
});
```
```zig
var term = try Terminal.init(alloc, .{
.cols = 80,
.rows = 24,
.kitty_image_loading_limits = .{
.file = true,
.temporary_file = true,
.shared_memory = false,
},
});
```
### Iterate all images
```zig
var it = term.screens.active.kitty_images.images.iterator();
while (it.next()) |kv| {
const img = kv.value_ptr;
std.debug.print("id={} {}x{} format={} bytes={}\n", .{
img.id, img.width, img.height, img.format, img.data.len,
});
}
```
### Delete all images
```zig
term.screens.active.kitty_images.delete(alloc, &term, .{ .all = true });
```
The default kitty image storage limit was 320 MB for all build
artifacts. For libghostty, this is overly generous since it is an
embedded library where conservative memory usage is preferred.
Lower the default to 10 MB when building as the lib artifact while
keeping the 320 MB default for the full Ghostty application.
The previous version requested general notification permissions but omitted the `.badge` option. Because the initial request was granted, `settings.authorizationStatus` returns `.authorized`, leading the app to believe it has full notification privileges when it actually lacks the authority to update the dock icon badge.
Move kitty_image_storage_limit and kitty_image_loading_limits into
Terminal.Options so callers can set them at construction time
rather than calling setter functions after init. The values flow
through to Screen.Options during ScreenSet initialization. Termio
now passes both at construction, keeping the setter functions for
the updateConfig path.
Add a Limits type to LoadingImage that controls which transmission
mediums (file, temporary_file, shared_memory) are allowed when
loading images. This defaults to "direct" (most restrictive) on
ImageStorage and is set to "all" by Termio, allowing apprt
embedders like libghostty to restrict medium types for resource or
security reasons.
The limits are stored on ImageStorage, plumbed through
Screen.Options for screen initialization and inheritance, and
enforced in graphics_exec during both query and transmit. Two new
Terminal methods (setKittyGraphicsSizeLimit, setKittyGraphicsLoadingLimits)
centralize updating all screens, replacing the manual iteration
previously done in Termio.
Introduce terminal/sys.zig which provides runtime-swappable function
pointers for operations that depend on external implementations. This
allows embedders of the terminal package to swap out implementations
at startup without hard dependencies on specific libraries.
The first function exposed is decode_png, which defaults to a wuffs
implementation. The kitty graphics image loader now calls through
sys.decode_png instead of importing wuffs directly.
This allows us to enable Kitty graphics support in libghostty-vt
for all targets except wasm32-freestanding.
Wire up the APC handler to `terminal.TerminalStream` to process APC
sequences, enabling support for kitty graphics commands in libghostty,
in theory.
The "in theory" is because we still don't export a way to actually
enable Kitty graphics in libghostty because we have some other things in
the way: PNG decoding and OS filesystem access that need to be more
conditionally compiled before we can enable the feature. However, this
is a step in the right direction, and we can at least verify that the
APC handler works via a test in Ghostty GUI.
Wire up the APC handler to `terminal.TerminalStream` to process
APC sequences, enabling support for kitty graphics commands in
libghostty, in theory.
The "in theory" is because we still don't export a way to actually
enable Kitty graphics in libghostty because we have some other things in
the way: PNG decoding and OS filesystem access that need to be more
conditionally compiled before we can enable the feature. However, this
is a step in the right direction, and we can at least verify that the
APC handler works via a test in Ghostty GUI.
Add a new GhosttySelection C API type (selection.h / c/selection.zig)
that pairs two GhosttyGridRef endpoints with a rectangle flag. This maps
directly to the internal Selection type using untracked pins.
The formatter terminal options gain an optional selection pointer. When
non-null the formatter restricts output to the specified range instead
of emitting the entire screen. When null the existing behavior of
formatting the full screen is preserved.
Add ghostty_grid_ref_hyperlink_uri to extract the OSC 8 hyperlink URI
from a cell at a grid reference position. Follows the same buffer
pattern as ghostty_grid_ref_graphemes: callers pass a buffer and get
back the byte length, or GHOSTTY_OUT_OF_SPACE with the required size if
the buffer is too small. Cells without a hyperlink return success with
length 0.
Add a new GhosttySelection C API type (selection.h / c/selection.zig)
that pairs two GhosttyGridRef endpoints with a rectangle flag. This
maps directly to the internal Selection type using untracked pins.
The formatter terminal options gain an optional selection pointer.
When non-null the formatter restricts output to the specified range
instead of emitting the entire screen. When null the existing
behavior of formatting the full screen is preserved.
Add ghostty_grid_ref_hyperlink_uri to extract the OSC 8 hyperlink
URI from a cell at a grid reference position. Follows the same
buffer pattern as ghostty_grid_ref_graphemes: callers pass a buffer
and get back the byte length, or GHOSTTY_OUT_OF_SPACE with the
required size if the buffer is too small. Cells without a hyperlink
return success with length 0.