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.
The libghostty-vt pkg-config file was missing Libs.private, so
pkg-config --libs --static returned the same flags as the shared case,
omitting the C++ standard library needed by the SIMD code.
Additionally, the static archive did not bundle the vendored SIMD
dependencies (simdutf, highway, utfcpp), leaving consumers with
unresolved symbols when linking. If we're choosing to vendor (no -fsys)
then we should produce a fat static archive that includes them. If
`-fsys` is used, then we should not bundle them and instead reference
them via Requires.private, letting pkg-config chain to their own .pc
files.
Add Libs.private with the C++ runtime (-lc++ on Darwin, -lstdc++ on
Linux) and Requires.private for any SIMD deps provided via system
integration. When SIMD deps are vendored (the default), produce a fat
static archive that bundles them using libtool on Darwin and ar on
Linux. When they come from the system (-fsys=), reference them via
Requires.private instead, letting pkg-config chain to their own .pc
files.
The libghostty-vt pkg-config file was missing Libs.private, so
pkg-config --libs --static returned the same flags as the shared
case, omitting the C++ standard library needed by the SIMD code.
Additionally, the static archive did not bundle the vendored SIMD
dependencies (simdutf, highway, utfcpp), leaving consumers with
unresolved symbols when linking. If we're choosing to vendor (no -fsys)
then we should produce a fat static archive that includes them. If `-fsys`
is used, then we should not bundle them and instead reference them via
Requires.private, letting pkg-config chain to their own .pc files.
Add Libs.private with the C++ runtime (-lc++ on Darwin, -lstdc++
on Linux) and Requires.private for any SIMD deps provided via
system integration. When SIMD deps are vendored (the default),
produce a fat static archive that bundles them using libtool on
Darwin and ar on Linux. When they come from the system (-fsys=),
reference them via Requires.private instead, letting pkg-config
chain to their own .pc files.
I don’t know why the search-related commands were added as performable
keybinds in 240d5e0fc5, but **I asked
Claude to add some tests for that**
> This won't fix cmd+g/G not working when the search bar is focused.
This reverts commit 20cfaae2e5, reversing
changes made to 3509ccf78e.
This breaks some behaviours when there are multiple splits, which
requires another click to focus to another split in the same window
This is known issues before key-related PRs, tested on
fa9265636b.
The following config is mapped incorrectly to the menu shortcut:
```
keybind=A=goto_split:left
```
<img width="223" height="106" alt="image"
src="https://github.com/user-attachments/assets/b80da251-9cff-4b29-b143-64854a5c4271"
/>
Surfaces only accept `a` as a trigger to select left split, not
`shift+a`