···11+---
22+name: discovering-upstream-apis
33+description: >-
44+ Discovers new or requested C APIs in upstream libghostty-vt headers
55+ and adds Go bindings. Use when asked to add new APIs, add bindings,
66+ check for new upstream APIs, or bind a specific API like 'add the
77+ decode png api'.
88+---
99+1010+# Discovering Upstream APIs
1111+1212+Finds new or requested C APIs in the upstream libghostty-vt headers
1313+and creates or updates Go bindings for them.
1414+1515+## Checking for upstream changes
1616+1717+The pinned commit is in `CMakeLists.txt` under
1818+`FetchContent_Declare(ghostty ...)`.
1919+2020+Run `scripts/latest-upstream-commit.sh` to compare the pinned commit
2121+against the latest upstream `main` branch. It prints the pinned SHA,
2222+the latest SHA, and whether an update is available.
2323+2424+When an update is available:
2525+2626+1. Update the `GIT_TAG` in `CMakeLists.txt` to the latest commit.
2727+2. Run `make clean && make build` to fetch the new source.
2828+3. Run the discovery steps above.
2929+4. Compare against the current Go bindings.
3030+5. Present findings to the user.
3131+3232+## Workflow
3333+3434+### 1. Identify what to bind
3535+3636+Determine the scope based on the user's request:
3737+3838+- **Specific API** (e.g. "add the decode png api"): Search the
3939+ upstream headers for matching functions/types.
4040+- **All new APIs** (e.g. "add the new apis"): Diff the upstream
4141+ headers against existing Go bindings to find unbound APIs.
4242+4343+### 2. Locate upstream headers
4444+4545+Headers live at:
4646+4747+```
4848+build/_deps/ghostty-src/zig-out/include/ghostty/vt/
4949+```
5050+5151+The umbrella header is at
5252+`build/_deps/ghostty-src/zig-out/include/ghostty/vt.h` and includes
5353+all sub-headers. Individual headers are in the `vt/` subdirectory.
5454+5555+If the build directory does not exist, run `make build` first.
5656+5757+### 3. Discover unbound APIs
5858+5959+To find what's new or missing:
6060+6161+1. List all C functions in the upstream headers:
6262+6363+ ```
6464+ grep -rh '^[A-Za-z].*ghostty_' \
6565+ build/_deps/ghostty-src/zig-out/include/ghostty/vt/ \
6666+ | grep '('
6767+ ```
6868+6969+2. List all C functions already referenced in Go files:
7070+7171+ ```
7272+ grep -rh 'C\.\(ghostty_[a-z_]*\)' *.go \
7373+ | grep -oE 'ghostty_[a-z_]+' | sort -u
7474+ ```
7575+7676+3. Cross-reference to find unbound functions.
7777+4. Also check `TODO.md` for known missing items.
7878+7979+For type/enum discovery:
8080+8181+```
8282+grep -rh 'typedef\|^} Ghostty\|GHOSTTY_[A-Z_]*[, ]' \
8383+ build/_deps/ghostty-src/zig-out/include/ghostty/vt/*.h
8484+```
8585+8686+### 4. Confirm with the user
8787+8888+Before writing code, present the list of new APIs found and ask
8989+which ones the user wants bound. Group them by header file. Include:
9090+9191+- Function signatures
9292+- Related types/enums they depend on
9393+- Which header they come from
9494+9595+### 5. Write Go bindings
9696+9797+Follow the conventions specified in AGENTS.md and patterns in
9898+existing code. Here are some examples:
9999+100100+- **Simple getters** (`ghostty_terminal_get`): See
101101+ `terminal_data.go` — call `ghostty_terminal_get` with the
102102+ appropriate enum, cast result to Go type.
103103+- **New/Free lifecycle**: See `render_state.go` — `NewX()` returns
104104+ `(*X, error)`, `Close()` frees.
105105+- **Effect callbacks**: See `terminal_effect.go` — use C trampolines
106106+ with `//export` and `cgo.Handle` for userdata round-tripping.
107107+- **Tagged unions**: See `terminal.go` `ScrollViewport*` — set tag
108108+ then poke the value union via `unsafe.Pointer`.
109109+- **Formatters/iterators**: See `formatter.go` — functional options
110110+ pattern, alloc + copy + free for output buffers.
111111+112112+### 6. Write tests
113113+114114+- Add tests in a `_test.go` file matching the source file name.
115115+- Follow existing test patterns (see `terminal_test.go`,
116116+ `formatter_test.go`).
117117+- Run `make test` to verify.
118118+119119+### 7. Update TODO.md
120120+121121+- Remove any items from `TODO.md` that have been bound.
122122+- Add any new partial items if applicable.
···11+#!/usr/bin/env bash
22+# Prints the latest commit SHA on the main branch of the upstream
33+# ghostty repo. Compares it against the pinned commit in
44+# CMakeLists.txt and reports whether an update is needed.
55+66+set -euo pipefail
77+88+REPO="ghostty-org/ghostty"
99+BRANCH="main"
1010+CMAKE="CMakeLists.txt"
1111+1212+# Get the currently pinned commit from CMakeLists.txt.
1313+pinned=$(grep -oP '(?<=GIT_TAG )[0-9a-f]+' "$CMAKE" 2>/dev/null || true)
1414+if [ -z "$pinned" ]; then
1515+ echo "ERROR: could not find GIT_TAG in $CMAKE" >&2
1616+ exit 1
1717+fi
1818+1919+# Fetch the latest commit from GitHub.
2020+latest=$(git ls-remote "https://github.com/${REPO}.git" "refs/heads/${BRANCH}" \
2121+ | awk '{print $1}')
2222+if [ -z "$latest" ]; then
2323+ echo "ERROR: could not fetch latest commit from ${REPO}" >&2
2424+ exit 1
2525+fi
2626+2727+echo "pinned: $pinned"
2828+echo "latest: $latest"
2929+3030+if [ "$pinned" = "$latest" ]; then
3131+ echo "status: up-to-date"
3232+else
3333+ echo "status: update-available"
3434+fi
+148
.agents/skills/upstream-api-feedback/SKILL.md
···11+---
22+name: upstream-api-feedback
33+description: >-
44+ Reviews Go bindings against upstream libghostty-vt C headers and
55+ produces concrete upstream API improvement suggestions. Use when
66+ asked to review bindings for upstream feedback, suggest C API
77+ changes, find API friction, or improve the upstream API.
88+---
99+1010+# Upstream API Feedback
1111+1212+Analyzes the Go bindings in this project against the upstream
1313+libghostty-vt C headers to identify concrete, actionable API
1414+changes that would make the bindings more performant, idiomatic,
1515+or easier to use. Output is written to directly to the chat.
1616+1717+## When to use
1818+1919+- The user asks for upstream API feedback or suggestions.
2020+- The user asks to review bindings for performance or ergonomics.
2121+- After binding a new API, to check for friction points.
2222+2323+## Workflow
2424+2525+### 1. Identify scope
2626+2727+Determine which bindings to review:
2828+2929+- **Specific file** (e.g. "review kitty_graphics.go"): Focus on
3030+ that file and its corresponding C header.
3131+- **All bindings** (e.g. "review everything"): Scan all `*.go`
3232+ files (excluding `_test.go`) and cross-reference with headers.
3333+- **Recent changes**: Use `jj diff` or `git diff` to find what
3434+ changed and focus on those files.
3535+3636+### 2. Read the Go bindings and C headers together
3737+3838+For each file in scope:
3939+4040+1. Read the Go file to understand the binding patterns used.
4141+2. Read the corresponding C header in
4242+ `build/_deps/ghostty-src/zig-out/include/ghostty/vt/`.
4343+3. Note every CGo crossing — each `C.ghostty_*()` call.
4444+4545+### 3. Apply the analysis checklist
4646+4747+For each API surface, check for these patterns:
4848+4949+#### Multi-call data fetching (highest impact)
5050+5151+Look for Go functions that make multiple `C.ghostty_*_get()` calls
5252+to fetch logically related fields from the same object. Each CGo
5353+call has ~100ns overhead that compounds in hot paths.
5454+5555+**Signal**: A Go type whose methods each call the same C
5656+`_get(handle, ENUM_VARIANT, &out)` function with different enum
5757+values — especially when callers typically need several fields
5858+together.
5959+6060+**Suggestion**: A sized struct (like `GhosttyRenderStateColors`)
6161+that returns all fields in a single call. Reference the existing
6262+`GHOSTTY_INIT_SIZED` pattern.
6363+6464+#### Pointer/length splits
6565+6666+Look for cases where pointer and length are separate enum variants
6767+in the same `_get()` API (e.g. `DATA_PTR` + `DATA_LEN`). These
6868+are semantically one value split across two calls.
6969+7070+**Signal**: The Go binding must make two sequential calls and has
7171+no atomicity guarantee between them.
7272+7373+**Suggestion**: Either fold into the sized struct above, use
7474+`GhosttyString`-style `{ptr, len}` as a single variant, or add a
7575+dedicated function.
7676+7777+#### Repeated parameter triples
7878+7979+Look for multiple C functions that take the same parameter
8080+combination (e.g. `(iterator, image, terminal)`) and are typically
8181+called together per iteration step.
8282+8383+**Signal**: The Go code calls 3-4 functions with identical
8484+arguments in sequence during a loop body.
8585+8686+**Suggestion**: A combined function returning a struct with all
8787+results, cutting N CGo crossings to 1.
8888+8989+#### Two-phase initialization
9090+9191+Look for patterns where an object must be allocated, then populated
9292+via a separate call before it's usable (e.g. `_new()` then
9393+`_get(POPULATE, &handle)`).
9494+9595+**Signal**: The Go binding wraps this in a helper but the C API
9696+still requires two calls where one would suffice.
9797+9898+**Suggestion**: A combined constructor, or making the populate step
9999+part of `_new()`.
100100+101101+#### Missing convenience variants
102102+103103+Look for C APIs that could offer a simpler overload for the common
104104+case while keeping the flexible version.
105105+106106+**Signal**: The Go binding wraps a complex C API with a simpler Go
107107+function that most callers use, hiding parameters that are almost
108108+always the same value.
109109+110110+**Suggestion**: A C convenience function for the common case.
111111+112112+### 4. Assess impact
113113+114114+Rate each suggestion:
115115+116116+- **Hot path**: Called per-frame or per-placement in a render loop.
117117+ CGo overhead is multiplied by iteration count. High impact.
118118+- **Setup path**: Called once during initialization or
119119+ configuration. Low impact — ergonomics matter more than perf.
120120+- **Ergonomic**: Doesn't affect performance but makes the API
121121+ harder to use correctly or requires awkward binding code.
122122+123123+### 5. Write the output
124124+125125+Write findings to chat. Format:
126126+127127+```markdown
128128+# Upstream API Feedback
129129+130130+Summary of the review scope and methodology.
131131+132132+## Suggestion Title
133133+134134+**Impact**: Hot path / Setup path / Ergonomic
135135+**Files**: `header.h`, `binding.go`
136136+137137+Description of the current pattern, why it's suboptimal, and the
138138+concrete C API change. Include a struct/function signature sketch.
139139+140140+## Next Suggestion
141141+142142+...
143143+```
144144+145145+Group suggestions by impact (hot path first). Include concrete C
146146+type/function signatures — not vague ideas. Reference existing
147147+libghostty-vt patterns (like `GhosttyRenderStateColors`) as
148148+precedent when applicable.
···11+package libghostty
22+33+// Kitty graphics protocol bindings wrapping the C API from kitty_graphics.h.
44+// Provides access to images and placements stored via the Kitty graphics
55+// protocol.
66+77+/*
88+#include <ghostty/vt.h>
99+1010+// Helper to create a properly initialized GhosttySelection (sized struct).
1111+static inline GhosttySelection init_selection() {
1212+ GhosttySelection s = GHOSTTY_INIT_SIZED(GhosttySelection);
1313+ return s;
1414+}
1515+*/
1616+import "C"
1717+1818+import "unsafe"
1919+2020+// KittyGraphics is a handle to the Kitty graphics image storage
2121+// associated with a terminal's active screen. It is borrowed from
2222+// the terminal and remains valid until the next mutating terminal
2323+// call (e.g. VTWrite or Reset).
2424+//
2525+// C: GhosttyKittyGraphics
2626+type KittyGraphics struct {
2727+ ptr C.GhosttyKittyGraphics
2828+}
2929+3030+// KittyGraphicsImage is a handle to a single Kitty graphics image.
3131+// It is borrowed from the storage and remains valid until the next
3232+// mutating terminal call.
3333+//
3434+// C: GhosttyKittyGraphicsImage
3535+type KittyGraphicsImage struct {
3636+ ptr C.GhosttyKittyGraphicsImage
3737+}
3838+3939+// KittyGraphicsPlacementIterator iterates over placements in the
4040+// Kitty graphics storage. It is independently owned and must be
4141+// freed by calling Close, but the data it yields is only valid
4242+// while the underlying terminal is not mutated.
4343+//
4444+// C: GhosttyKittyGraphicsPlacementIterator
4545+type KittyGraphicsPlacementIterator struct {
4646+ ptr C.GhosttyKittyGraphicsPlacementIterator
4747+}
4848+4949+// KittyPlacementLayer classifies z-layer for kitty graphics placements.
5050+// Based on the kitty protocol z-index conventions.
5151+//
5252+// C: GhosttyKittyPlacementLayer
5353+type KittyPlacementLayer int
5454+5555+const (
5656+ // KittyPlacementLayerAll disables layer filtering (all placements).
5757+ KittyPlacementLayerAll KittyPlacementLayer = C.GHOSTTY_KITTY_PLACEMENT_LAYER_ALL
5858+5959+ // KittyPlacementLayerBelowBG matches placements below cell background
6060+ // (z < INT32_MIN/2).
6161+ KittyPlacementLayerBelowBG KittyPlacementLayer = C.GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_BG
6262+6363+ // KittyPlacementLayerBelowText matches placements above background but
6464+ // below text (INT32_MIN/2 <= z < 0).
6565+ KittyPlacementLayerBelowText KittyPlacementLayer = C.GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_TEXT
6666+6767+ // KittyPlacementLayerAboveText matches placements above text (z >= 0).
6868+ KittyPlacementLayerAboveText KittyPlacementLayer = C.GHOSTTY_KITTY_PLACEMENT_LAYER_ABOVE_TEXT
6969+)
7070+7171+// KittyImageFormat describes the pixel format of a Kitty graphics image.
7272+//
7373+// C: GhosttyKittyImageFormat
7474+type KittyImageFormat int
7575+7676+const (
7777+ // KittyImageFormatRGB is 24-bit RGB (3 bytes per pixel).
7878+ KittyImageFormatRGB KittyImageFormat = C.GHOSTTY_KITTY_IMAGE_FORMAT_RGB
7979+8080+ // KittyImageFormatRGBA is 32-bit RGBA (4 bytes per pixel).
8181+ KittyImageFormatRGBA KittyImageFormat = C.GHOSTTY_KITTY_IMAGE_FORMAT_RGBA
8282+8383+ // KittyImageFormatPNG is compressed PNG data.
8484+ KittyImageFormatPNG KittyImageFormat = C.GHOSTTY_KITTY_IMAGE_FORMAT_PNG
8585+8686+ // KittyImageFormatGrayAlpha is 16-bit gray+alpha (2 bytes per pixel).
8787+ KittyImageFormatGrayAlpha KittyImageFormat = C.GHOSTTY_KITTY_IMAGE_FORMAT_GRAY_ALPHA
8888+8989+ // KittyImageFormatGray is 8-bit grayscale (1 byte per pixel).
9090+ KittyImageFormatGray KittyImageFormat = C.GHOSTTY_KITTY_IMAGE_FORMAT_GRAY
9191+)
9292+9393+// KittyImageCompression describes the compression of a Kitty graphics image.
9494+//
9595+// C: GhosttyKittyImageCompression
9696+type KittyImageCompression int
9797+9898+const (
9999+ // KittyImageCompressionNone means no compression.
100100+ KittyImageCompressionNone KittyImageCompression = C.GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE
101101+102102+ // KittyImageCompressionZlibDeflate means zlib/deflate compression.
103103+ KittyImageCompressionZlibDeflate KittyImageCompression = C.GHOSTTY_KITTY_IMAGE_COMPRESSION_ZLIB_DEFLATE
104104+)
105105+106106+// Selection represents a grid selection range defined by two grid references.
107107+//
108108+// C: GhosttySelection
109109+type Selection struct {
110110+ // Start is the start of the selection range (inclusive).
111111+ Start GridRef
112112+113113+ // End is the end of the selection range (inclusive).
114114+ End GridRef
115115+116116+ // Rectangle indicates whether the selection is rectangular (block)
117117+ // rather than linear.
118118+ Rectangle bool
119119+}
120120+121121+// selectionFromC converts a C GhosttySelection to a Go Selection.
122122+func selectionFromC(cs C.GhosttySelection) Selection {
123123+ return Selection{
124124+ Start: GridRef{ref: cs.start},
125125+ End: GridRef{ref: cs.end},
126126+ Rectangle: bool(cs.rectangle),
127127+ }
128128+}
129129+130130+// PlacementIterator populates the given iterator with placement data
131131+// from this storage. The iterator must have been created with
132132+// NewKittyGraphicsPlacementIterator.
133133+func (kg *KittyGraphics) PlacementIterator(iter *KittyGraphicsPlacementIterator) error {
134134+ return resultError(C.ghostty_kitty_graphics_get(
135135+ kg.ptr,
136136+ C.GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR,
137137+ unsafe.Pointer(&iter.ptr),
138138+ ))
139139+}
140140+141141+// Image looks up a Kitty graphics image by its image ID. Returns nil
142142+// if no image with the given ID exists.
143143+func (kg *KittyGraphics) Image(imageID uint32) *KittyGraphicsImage {
144144+ ptr := C.ghostty_kitty_graphics_image(kg.ptr, C.uint32_t(imageID))
145145+ if ptr == nil {
146146+ return nil
147147+ }
148148+ return &KittyGraphicsImage{ptr: ptr}
149149+}
150150+151151+// ID returns the image ID.
152152+func (img *KittyGraphicsImage) ID() (uint32, error) {
153153+ var v C.uint32_t
154154+ if err := resultError(C.ghostty_kitty_graphics_image_get(
155155+ img.ptr,
156156+ C.GHOSTTY_KITTY_IMAGE_DATA_ID,
157157+ unsafe.Pointer(&v),
158158+ )); err != nil {
159159+ return 0, err
160160+ }
161161+ return uint32(v), nil
162162+}
163163+164164+// Number returns the image number.
165165+func (img *KittyGraphicsImage) Number() (uint32, error) {
166166+ var v C.uint32_t
167167+ if err := resultError(C.ghostty_kitty_graphics_image_get(
168168+ img.ptr,
169169+ C.GHOSTTY_KITTY_IMAGE_DATA_NUMBER,
170170+ unsafe.Pointer(&v),
171171+ )); err != nil {
172172+ return 0, err
173173+ }
174174+ return uint32(v), nil
175175+}
176176+177177+// Width returns the image width in pixels.
178178+func (img *KittyGraphicsImage) Width() (uint32, error) {
179179+ var v C.uint32_t
180180+ if err := resultError(C.ghostty_kitty_graphics_image_get(
181181+ img.ptr,
182182+ C.GHOSTTY_KITTY_IMAGE_DATA_WIDTH,
183183+ unsafe.Pointer(&v),
184184+ )); err != nil {
185185+ return 0, err
186186+ }
187187+ return uint32(v), nil
188188+}
189189+190190+// Height returns the image height in pixels.
191191+func (img *KittyGraphicsImage) Height() (uint32, error) {
192192+ var v C.uint32_t
193193+ if err := resultError(C.ghostty_kitty_graphics_image_get(
194194+ img.ptr,
195195+ C.GHOSTTY_KITTY_IMAGE_DATA_HEIGHT,
196196+ unsafe.Pointer(&v),
197197+ )); err != nil {
198198+ return 0, err
199199+ }
200200+ return uint32(v), nil
201201+}
202202+203203+// Format returns the pixel format of the image.
204204+func (img *KittyGraphicsImage) Format() (KittyImageFormat, error) {
205205+ var v C.GhosttyKittyImageFormat
206206+ if err := resultError(C.ghostty_kitty_graphics_image_get(
207207+ img.ptr,
208208+ C.GHOSTTY_KITTY_IMAGE_DATA_FORMAT,
209209+ unsafe.Pointer(&v),
210210+ )); err != nil {
211211+ return 0, err
212212+ }
213213+ return KittyImageFormat(v), nil
214214+}
215215+216216+// Compression returns the compression of the image.
217217+func (img *KittyGraphicsImage) Compression() (KittyImageCompression, error) {
218218+ var v C.GhosttyKittyImageCompression
219219+ if err := resultError(C.ghostty_kitty_graphics_image_get(
220220+ img.ptr,
221221+ C.GHOSTTY_KITTY_IMAGE_DATA_COMPRESSION,
222222+ unsafe.Pointer(&v),
223223+ )); err != nil {
224224+ return 0, err
225225+ }
226226+ return KittyImageCompression(v), nil
227227+}
228228+229229+// Data returns a borrowed slice of the raw pixel data. The slice is
230230+// only valid until the next mutating terminal call.
231231+func (img *KittyGraphicsImage) Data() ([]byte, error) {
232232+ var ptr *C.uint8_t
233233+ if err := resultError(C.ghostty_kitty_graphics_image_get(
234234+ img.ptr,
235235+ C.GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR,
236236+ unsafe.Pointer(&ptr),
237237+ )); err != nil {
238238+ return nil, err
239239+ }
240240+241241+ var length C.size_t
242242+ if err := resultError(C.ghostty_kitty_graphics_image_get(
243243+ img.ptr,
244244+ C.GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN,
245245+ unsafe.Pointer(&length),
246246+ )); err != nil {
247247+ return nil, err
248248+ }
249249+250250+ if ptr == nil || length == 0 {
251251+ return nil, nil
252252+ }
253253+254254+ return unsafe.Slice((*byte)(unsafe.Pointer(ptr)), int(length)), nil
255255+}
256256+257257+// NewKittyGraphicsPlacementIterator creates a new placement iterator.
258258+// Call KittyGraphics.PlacementIterator to populate it with data, then
259259+// iterate with Next and read fields with the getter methods.
260260+// The iterator must be freed by calling Close.
261261+func NewKittyGraphicsPlacementIterator() (*KittyGraphicsPlacementIterator, error) {
262262+ var ptr C.GhosttyKittyGraphicsPlacementIterator
263263+ if err := resultError(C.ghostty_kitty_graphics_placement_iterator_new(nil, &ptr)); err != nil {
264264+ return nil, err
265265+ }
266266+ return &KittyGraphicsPlacementIterator{ptr: ptr}, nil
267267+}
268268+269269+// Close frees the placement iterator. After this call, the iterator
270270+// must not be used.
271271+func (it *KittyGraphicsPlacementIterator) Close() {
272272+ C.ghostty_kitty_graphics_placement_iterator_free(it.ptr)
273273+}
274274+275275+// SetLayer sets the z-layer filter for the iterator. Only placements
276276+// matching the given layer will be returned by Next. The default is
277277+// KittyPlacementLayerAll (no filtering).
278278+func (it *KittyGraphicsPlacementIterator) SetLayer(layer KittyPlacementLayer) error {
279279+ v := C.GhosttyKittyPlacementLayer(layer)
280280+ return resultError(C.ghostty_kitty_graphics_placement_iterator_set(
281281+ it.ptr,
282282+ C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_LAYER,
283283+ unsafe.Pointer(&v),
284284+ ))
285285+}
286286+287287+// Next advances the iterator to the next placement. Returns true if
288288+// a placement is available, false when iteration is complete.
289289+func (it *KittyGraphicsPlacementIterator) Next() bool {
290290+ return bool(C.ghostty_kitty_graphics_placement_next(it.ptr))
291291+}
292292+293293+// ImageID returns the image ID of the current placement.
294294+func (it *KittyGraphicsPlacementIterator) ImageID() (uint32, error) {
295295+ var v C.uint32_t
296296+ if err := resultError(C.ghostty_kitty_graphics_placement_get(
297297+ it.ptr,
298298+ C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID,
299299+ unsafe.Pointer(&v),
300300+ )); err != nil {
301301+ return 0, err
302302+ }
303303+ return uint32(v), nil
304304+}
305305+306306+// PlacementID returns the placement ID of the current placement.
307307+func (it *KittyGraphicsPlacementIterator) PlacementID() (uint32, error) {
308308+ var v C.uint32_t
309309+ if err := resultError(C.ghostty_kitty_graphics_placement_get(
310310+ it.ptr,
311311+ C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID,
312312+ unsafe.Pointer(&v),
313313+ )); err != nil {
314314+ return 0, err
315315+ }
316316+ return uint32(v), nil
317317+}
318318+319319+// IsVirtual reports whether the current placement is a virtual
320320+// (unicode placeholder) placement.
321321+func (it *KittyGraphicsPlacementIterator) IsVirtual() (bool, error) {
322322+ var v C.bool
323323+ if err := resultError(C.ghostty_kitty_graphics_placement_get(
324324+ it.ptr,
325325+ C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL,
326326+ unsafe.Pointer(&v),
327327+ )); err != nil {
328328+ return false, err
329329+ }
330330+ return bool(v), nil
331331+}
332332+333333+// XOffset returns the pixel offset from the left edge of the cell.
334334+func (it *KittyGraphicsPlacementIterator) XOffset() (uint32, error) {
335335+ var v C.uint32_t
336336+ if err := resultError(C.ghostty_kitty_graphics_placement_get(
337337+ it.ptr,
338338+ C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET,
339339+ unsafe.Pointer(&v),
340340+ )); err != nil {
341341+ return 0, err
342342+ }
343343+ return uint32(v), nil
344344+}
345345+346346+// YOffset returns the pixel offset from the top edge of the cell.
347347+func (it *KittyGraphicsPlacementIterator) YOffset() (uint32, error) {
348348+ var v C.uint32_t
349349+ if err := resultError(C.ghostty_kitty_graphics_placement_get(
350350+ it.ptr,
351351+ C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET,
352352+ unsafe.Pointer(&v),
353353+ )); err != nil {
354354+ return 0, err
355355+ }
356356+ return uint32(v), nil
357357+}
358358+359359+// SourceX returns the source rectangle x origin in pixels.
360360+func (it *KittyGraphicsPlacementIterator) SourceX() (uint32, error) {
361361+ var v C.uint32_t
362362+ if err := resultError(C.ghostty_kitty_graphics_placement_get(
363363+ it.ptr,
364364+ C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_X,
365365+ unsafe.Pointer(&v),
366366+ )); err != nil {
367367+ return 0, err
368368+ }
369369+ return uint32(v), nil
370370+}
371371+372372+// SourceY returns the source rectangle y origin in pixels.
373373+func (it *KittyGraphicsPlacementIterator) SourceY() (uint32, error) {
374374+ var v C.uint32_t
375375+ if err := resultError(C.ghostty_kitty_graphics_placement_get(
376376+ it.ptr,
377377+ C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_Y,
378378+ unsafe.Pointer(&v),
379379+ )); err != nil {
380380+ return 0, err
381381+ }
382382+ return uint32(v), nil
383383+}
384384+385385+// SourceWidth returns the source rectangle width in pixels
386386+// (0 = full image width).
387387+func (it *KittyGraphicsPlacementIterator) SourceWidth() (uint32, error) {
388388+ var v C.uint32_t
389389+ if err := resultError(C.ghostty_kitty_graphics_placement_get(
390390+ it.ptr,
391391+ C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_WIDTH,
392392+ unsafe.Pointer(&v),
393393+ )); err != nil {
394394+ return 0, err
395395+ }
396396+ return uint32(v), nil
397397+}
398398+399399+// SourceHeight returns the source rectangle height in pixels
400400+// (0 = full image height).
401401+func (it *KittyGraphicsPlacementIterator) SourceHeight() (uint32, error) {
402402+ var v C.uint32_t
403403+ if err := resultError(C.ghostty_kitty_graphics_placement_get(
404404+ it.ptr,
405405+ C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_HEIGHT,
406406+ unsafe.Pointer(&v),
407407+ )); err != nil {
408408+ return 0, err
409409+ }
410410+ return uint32(v), nil
411411+}
412412+413413+// Columns returns the number of columns this placement occupies.
414414+func (it *KittyGraphicsPlacementIterator) Columns() (uint32, error) {
415415+ var v C.uint32_t
416416+ if err := resultError(C.ghostty_kitty_graphics_placement_get(
417417+ it.ptr,
418418+ C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS,
419419+ unsafe.Pointer(&v),
420420+ )); err != nil {
421421+ return 0, err
422422+ }
423423+ return uint32(v), nil
424424+}
425425+426426+// Rows returns the number of rows this placement occupies.
427427+func (it *KittyGraphicsPlacementIterator) Rows() (uint32, error) {
428428+ var v C.uint32_t
429429+ if err := resultError(C.ghostty_kitty_graphics_placement_get(
430430+ it.ptr,
431431+ C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_ROWS,
432432+ unsafe.Pointer(&v),
433433+ )); err != nil {
434434+ return 0, err
435435+ }
436436+ return uint32(v), nil
437437+}
438438+439439+// Z returns the z-index of the current placement.
440440+func (it *KittyGraphicsPlacementIterator) Z() (int32, error) {
441441+ var v C.int32_t
442442+ if err := resultError(C.ghostty_kitty_graphics_placement_get(
443443+ it.ptr,
444444+ C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z,
445445+ unsafe.Pointer(&v),
446446+ )); err != nil {
447447+ return 0, err
448448+ }
449449+ return int32(v), nil
450450+}
451451+452452+// Rect computes the grid rectangle occupied by the current placement.
453453+// Virtual placements (unicode placeholders) return an error with
454454+// ResultNoValue.
455455+func (it *KittyGraphicsPlacementIterator) Rect(img *KittyGraphicsImage, t *Terminal) (*Selection, error) {
456456+ cs := C.init_selection()
457457+ if err := resultError(C.ghostty_kitty_graphics_placement_rect(
458458+ it.ptr,
459459+ img.ptr,
460460+ t.ptr,
461461+ &cs,
462462+ )); err != nil {
463463+ return nil, err
464464+ }
465465+ sel := selectionFromC(cs)
466466+ return &sel, nil
467467+}
468468+469469+// PixelSize computes the rendered pixel dimensions of the current
470470+// placement, accounting for the source rectangle, specified
471471+// columns/rows, and aspect ratio.
472472+func (it *KittyGraphicsPlacementIterator) PixelSize(img *KittyGraphicsImage, t *Terminal) (width, height uint32, err error) {
473473+ var w, h C.uint32_t
474474+ if err := resultError(C.ghostty_kitty_graphics_placement_pixel_size(
475475+ it.ptr,
476476+ img.ptr,
477477+ t.ptr,
478478+ &w,
479479+ &h,
480480+ )); err != nil {
481481+ return 0, 0, err
482482+ }
483483+ return uint32(w), uint32(h), nil
484484+}
485485+486486+// GridSize computes the number of grid columns and rows the current
487487+// placement occupies.
488488+func (it *KittyGraphicsPlacementIterator) GridSize(img *KittyGraphicsImage, t *Terminal) (cols, rows uint32, err error) {
489489+ var c, r C.uint32_t
490490+ if err := resultError(C.ghostty_kitty_graphics_placement_grid_size(
491491+ it.ptr,
492492+ img.ptr,
493493+ t.ptr,
494494+ &c,
495495+ &r,
496496+ )); err != nil {
497497+ return 0, 0, err
498498+ }
499499+ return uint32(c), uint32(r), nil
500500+}
501501+502502+// ViewportPos returns the viewport-relative grid position of the
503503+// current placement. The row can be negative for partially visible
504504+// placements. Returns an error with ResultNoValue when fully
505505+// off-screen or for virtual placements.
506506+func (it *KittyGraphicsPlacementIterator) ViewportPos(img *KittyGraphicsImage, t *Terminal) (col, row int32, err error) {
507507+ var c, r C.int32_t
508508+ if err := resultError(C.ghostty_kitty_graphics_placement_viewport_pos(
509509+ it.ptr,
510510+ img.ptr,
511511+ t.ptr,
512512+ &c,
513513+ &r,
514514+ )); err != nil {
515515+ return 0, 0, err
516516+ }
517517+ return int32(c), int32(r), nil
518518+}
519519+520520+// SourceRect returns the resolved source rectangle for the current
521521+// placement in pixels, clamped to the actual image bounds. A width
522522+// or height of 0 in the placement means "use the full image dimension".
523523+func (it *KittyGraphicsPlacementIterator) SourceRect(img *KittyGraphicsImage) (x, y, width, height uint32, err error) {
524524+ var cx, cy, cw, ch C.uint32_t
525525+ if err := resultError(C.ghostty_kitty_graphics_placement_source_rect(
526526+ it.ptr,
527527+ img.ptr,
528528+ &cx,
529529+ &cy,
530530+ &cw,
531531+ &ch,
532532+ )); err != nil {
533533+ return 0, 0, 0, 0, err
534534+ }
535535+ return uint32(cx), uint32(cy), uint32(cw), uint32(ch), nil
536536+}
+399
kitty_graphics_test.go
···11+package libghostty
22+33+import (
44+ "testing"
55+)
66+77+// newKittyTerminal creates a terminal with Kitty graphics enabled
88+// (PNG decode callback, WritePty handler, storage limit, and cell
99+// pixel dimensions), ready for Kitty graphics protocol testing.
1010+func newKittyTerminal(t *testing.T) *Terminal {
1111+ t.Helper()
1212+1313+ // Install the PNG decoder.
1414+ if err := SysSetDecodePng(SysDecodePng); err != nil {
1515+ t.Fatal(err)
1616+ }
1717+1818+ term, err := NewTerminal(
1919+ WithSize(80, 24),
2020+ // Install a WritePty handler so the terminal can send
2121+ // protocol responses (required for kitty graphics).
2222+ WithWritePty(func(data []byte) {}),
2323+ )
2424+ if err != nil {
2525+ t.Fatal(err)
2626+ }
2727+2828+ // Set cell pixel dimensions (required for image placement calculations).
2929+ if err := term.Resize(80, 24, 8, 16); err != nil {
3030+ t.Fatal(err)
3131+ }
3232+3333+ // Enable Kitty graphics with a generous storage limit.
3434+ limit := uint64(64 * 1024 * 1024)
3535+ if err := term.SetKittyImageStorageLimit(&limit); err != nil {
3636+ t.Fatal(err)
3737+ }
3838+3939+ return term
4040+}
4141+4242+// sendKittyImage sends a 1x1 PNG image to the terminal using the Kitty
4343+// graphics protocol. Uses the same image as the upstream C example.
4444+// The terminal auto-assigns the image ID.
4545+func sendKittyImage(t *testing.T, term *Terminal) {
4646+ t.Helper()
4747+4848+ // Kitty graphics protocol: transmit+display, PNG format (f=100),
4949+ // direct transmission (t=d, implicit), request response (q=1).
5050+ // Uses the same 1x1 red PNG as the upstream C example.
5151+ cmd := "\x1b_Ga=T,f=100,q=1;" +
5252+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA" +
5353+ "DUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==" +
5454+ "\x1b\\"
5555+ term.VTWrite([]byte(cmd))
5656+}
5757+5858+func TestKittyGraphicsStorageLimit(t *testing.T) {
5959+ term, err := NewTerminal(WithSize(80, 24))
6060+ if err != nil {
6161+ t.Fatal(err)
6262+ }
6363+ defer term.Close()
6464+6565+ // Set a storage limit.
6666+ limit := uint64(1024 * 1024)
6767+ if err := term.SetKittyImageStorageLimit(&limit); err != nil {
6868+ t.Fatal(err)
6969+ }
7070+7171+ // Disable by passing nil.
7272+ if err := term.SetKittyImageStorageLimit(nil); err != nil {
7373+ t.Fatal(err)
7474+ }
7575+}
7676+7777+func TestKittyGraphicsMediumSetters(t *testing.T) {
7878+ term, err := NewTerminal(WithSize(80, 24))
7979+ if err != nil {
8080+ t.Fatal(err)
8181+ }
8282+ defer term.Close()
8383+8484+ if err := term.SetKittyImageMediumFile(true); err != nil {
8585+ t.Fatal(err)
8686+ }
8787+ if err := term.SetKittyImageMediumFile(false); err != nil {
8888+ t.Fatal(err)
8989+ }
9090+ if err := term.SetKittyImageMediumTempFile(true); err != nil {
9191+ t.Fatal(err)
9292+ }
9393+ if err := term.SetKittyImageMediumSharedMem(true); err != nil {
9494+ t.Fatal(err)
9595+ }
9696+}
9797+9898+func TestKittyGraphicsHandle(t *testing.T) {
9999+ term := newKittyTerminal(t)
100100+ defer term.Close()
101101+102102+ kg, err := term.KittyGraphics()
103103+ if err != nil {
104104+ t.Fatal(err)
105105+ }
106106+ if kg == nil {
107107+ t.Fatal("expected non-nil KittyGraphics handle")
108108+ }
109109+}
110110+111111+func TestKittyGraphicsPlacementIteratorEmpty(t *testing.T) {
112112+ term := newKittyTerminal(t)
113113+ defer term.Close()
114114+115115+ kg, err := term.KittyGraphics()
116116+ if err != nil {
117117+ t.Fatal(err)
118118+ }
119119+120120+ iter, err := NewKittyGraphicsPlacementIterator()
121121+ if err != nil {
122122+ t.Fatal(err)
123123+ }
124124+ defer iter.Close()
125125+126126+ if err := kg.PlacementIterator(iter); err != nil {
127127+ t.Fatal(err)
128128+ }
129129+130130+ // No images sent; iterator should be empty.
131131+ if iter.Next() {
132132+ t.Fatal("expected no placements in empty terminal")
133133+ }
134134+}
135135+136136+func TestKittyGraphicsImageLookupMiss(t *testing.T) {
137137+ term := newKittyTerminal(t)
138138+ defer term.Close()
139139+140140+ kg, err := term.KittyGraphics()
141141+ if err != nil {
142142+ t.Fatal(err)
143143+ }
144144+145145+ // No image with ID 999 should exist.
146146+ img := kg.Image(999)
147147+ if img != nil {
148148+ t.Fatal("expected nil for non-existent image ID")
149149+ }
150150+}
151151+152152+func TestKittyGraphicsImageSendAndLookup(t *testing.T) {
153153+ term := newKittyTerminal(t)
154154+ defer term.Close()
155155+156156+ sendKittyImage(t, term)
157157+158158+ kg, err := term.KittyGraphics()
159159+ if err != nil {
160160+ t.Fatal(err)
161161+ }
162162+163163+ // Find the image ID by iterating placements (terminal auto-assigns IDs).
164164+ iter, err := NewKittyGraphicsPlacementIterator()
165165+ if err != nil {
166166+ t.Fatal(err)
167167+ }
168168+ defer iter.Close()
169169+170170+ if err := kg.PlacementIterator(iter); err != nil {
171171+ t.Fatal(err)
172172+ }
173173+174174+ if !iter.Next() {
175175+ t.Fatal("expected at least one placement after sending image")
176176+ }
177177+178178+ imageID, err := iter.ImageID()
179179+ if err != nil {
180180+ t.Fatal(err)
181181+ }
182182+183183+ // Look up the image by its ID.
184184+ img := kg.Image(imageID)
185185+ if img == nil {
186186+ t.Fatal("expected non-nil image for placement's image ID")
187187+ }
188188+189189+ // Verify image properties.
190190+ id, err := img.ID()
191191+ if err != nil {
192192+ t.Fatal(err)
193193+ }
194194+ if id != imageID {
195195+ t.Fatalf("expected image ID %d, got %d", imageID, id)
196196+ }
197197+198198+ w, err := img.Width()
199199+ if err != nil {
200200+ t.Fatal(err)
201201+ }
202202+ h, err := img.Height()
203203+ if err != nil {
204204+ t.Fatal(err)
205205+ }
206206+ if w != 1 || h != 1 {
207207+ t.Fatalf("expected 1x1 image, got %dx%d", w, h)
208208+ }
209209+210210+ // Check format — after PNG decoding it should be RGBA.
211211+ format, err := img.Format()
212212+ if err != nil {
213213+ t.Fatal(err)
214214+ }
215215+ if format != KittyImageFormatRGBA {
216216+ t.Fatalf("expected RGBA format, got %d", format)
217217+ }
218218+219219+ // Check compression.
220220+ compression, err := img.Compression()
221221+ if err != nil {
222222+ t.Fatal(err)
223223+ }
224224+ if compression != KittyImageCompressionNone {
225225+ t.Fatalf("expected no compression after decode, got %d", compression)
226226+ }
227227+228228+ // Check data is accessible.
229229+ data, err := img.Data()
230230+ if err != nil {
231231+ t.Fatal(err)
232232+ }
233233+ if len(data) == 0 {
234234+ t.Fatal("expected non-empty pixel data")
235235+ }
236236+ // 1x1 RGBA = 4 bytes.
237237+ if len(data) != 4 {
238238+ t.Fatalf("expected 4 bytes of pixel data, got %d", len(data))
239239+ }
240240+}
241241+242242+func TestKittyGraphicsPlacementIteration(t *testing.T) {
243243+ term := newKittyTerminal(t)
244244+ defer term.Close()
245245+246246+ sendKittyImage(t, term)
247247+248248+ kg, err := term.KittyGraphics()
249249+ if err != nil {
250250+ t.Fatal(err)
251251+ }
252252+253253+ iter, err := NewKittyGraphicsPlacementIterator()
254254+ if err != nil {
255255+ t.Fatal(err)
256256+ }
257257+ defer iter.Close()
258258+259259+ if err := kg.PlacementIterator(iter); err != nil {
260260+ t.Fatal(err)
261261+ }
262262+263263+ if !iter.Next() {
264264+ t.Fatal("expected at least one placement")
265265+ }
266266+267267+ // Verify we can read placement fields.
268268+ _, err = iter.PlacementID()
269269+ if err != nil {
270270+ t.Fatal(err)
271271+ }
272272+273273+ isVirtual, err := iter.IsVirtual()
274274+ if err != nil {
275275+ t.Fatal(err)
276276+ }
277277+ if isVirtual {
278278+ t.Fatal("expected non-virtual placement for direct display")
279279+ }
280280+281281+ _, err = iter.Z()
282282+ if err != nil {
283283+ t.Fatal(err)
284284+ }
285285+286286+ // Look up the image for rendering helpers.
287287+ imageID, err := iter.ImageID()
288288+ if err != nil {
289289+ t.Fatal(err)
290290+ }
291291+ img := kg.Image(imageID)
292292+ if img == nil {
293293+ t.Fatal("expected image lookup to succeed")
294294+ }
295295+296296+ // PixelSize should return valid dimensions.
297297+ pw, ph, err := iter.PixelSize(img, term)
298298+ if err != nil {
299299+ t.Fatal(err)
300300+ }
301301+ if pw == 0 || ph == 0 {
302302+ t.Fatalf("expected non-zero pixel size, got %dx%d", pw, ph)
303303+ }
304304+305305+ // GridSize should return valid dimensions.
306306+ gc, gr, err := iter.GridSize(img, term)
307307+ if err != nil {
308308+ t.Fatal(err)
309309+ }
310310+ if gc == 0 || gr == 0 {
311311+ t.Fatalf("expected non-zero grid size, got %dx%d", gc, gr)
312312+ }
313313+314314+ // SourceRect should succeed.
315315+ _, _, sw, sh, err := iter.SourceRect(img)
316316+ if err != nil {
317317+ t.Fatal(err)
318318+ }
319319+ if sw == 0 || sh == 0 {
320320+ t.Fatalf("expected non-zero source rect size, got %dx%d", sw, sh)
321321+ }
322322+}
323323+324324+func TestKittyGraphicsPlacementLayerFilter(t *testing.T) {
325325+ term := newKittyTerminal(t)
326326+ defer term.Close()
327327+328328+ sendKittyImage(t, term)
329329+330330+ kg, err := term.KittyGraphics()
331331+ if err != nil {
332332+ t.Fatal(err)
333333+ }
334334+335335+ iter, err := NewKittyGraphicsPlacementIterator()
336336+ if err != nil {
337337+ t.Fatal(err)
338338+ }
339339+ defer iter.Close()
340340+341341+ if err := kg.PlacementIterator(iter); err != nil {
342342+ t.Fatal(err)
343343+ }
344344+345345+ // Set layer filter to ABOVE_TEXT (default z=0 should match).
346346+ if err := iter.SetLayer(KittyPlacementLayerAboveText); err != nil {
347347+ t.Fatal(err)
348348+ }
349349+350350+ // Should still find the placement (z=0 is above text).
351351+ if !iter.Next() {
352352+ t.Fatal("expected placement with ABOVE_TEXT layer filter")
353353+ }
354354+}
355355+356356+func TestKittyGraphicsPlacementViewportPos(t *testing.T) {
357357+ term := newKittyTerminal(t)
358358+ defer term.Close()
359359+360360+ sendKittyImage(t, term)
361361+362362+ kg, err := term.KittyGraphics()
363363+ if err != nil {
364364+ t.Fatal(err)
365365+ }
366366+367367+ iter, err := NewKittyGraphicsPlacementIterator()
368368+ if err != nil {
369369+ t.Fatal(err)
370370+ }
371371+ defer iter.Close()
372372+373373+ if err := kg.PlacementIterator(iter); err != nil {
374374+ t.Fatal(err)
375375+ }
376376+377377+ if !iter.Next() {
378378+ t.Fatal("expected at least one placement")
379379+ }
380380+381381+ imageID, err := iter.ImageID()
382382+ if err != nil {
383383+ t.Fatal(err)
384384+ }
385385+ img := kg.Image(imageID)
386386+ if img == nil {
387387+ t.Fatal("expected image lookup to succeed")
388388+ }
389389+390390+ // The image was just placed, so it should be visible in the viewport.
391391+ col, row, err := iter.ViewportPos(img, term)
392392+ if err != nil {
393393+ t.Fatal(err)
394394+ }
395395+ // Position should be non-negative for a freshly placed image.
396396+ if col < 0 || row < 0 {
397397+ t.Fatalf("expected non-negative viewport position, got col=%d row=%d", col, row)
398398+ }
399399+}
+102
sys.go
···1818 uint8_t* message,
1919 size_t message_len);
20202121+// Forward declaration for the Go decode-PNG trampoline.
2222+// Uses compatible types (no const) to match what cgo generates for
2323+// the //export function.
2424+extern _Bool goSysDecodePngTrampoline(
2525+ void* userdata,
2626+ GhosttyAllocator* allocator,
2727+ uint8_t* data,
2828+ size_t data_len,
2929+ GhosttySysImage* out);
3030+2131// Helper to install the Go log trampoline via ghostty_sys_set.
2232// We need this because cgo cannot take the address of a Go-exported
2333// function directly as a C function pointer.
···3444static inline GhosttyResult sys_clear_log(void) {
3545 return ghostty_sys_set(GHOSTTY_SYS_OPT_LOG, NULL);
3646}
4747+4848+// Helper to install the Go decode-PNG trampoline via ghostty_sys_set.
4949+static inline GhosttyResult sys_set_decode_png_go(void) {
5050+ return ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, (const void*)goSysDecodePngTrampoline);
5151+}
5252+5353+// Helper to clear the decode-PNG callback.
5454+static inline GhosttyResult sys_clear_decode_png(void) {
5555+ return ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, NULL);
5656+}
3757*/
3858import "C"
39594060import "unsafe"
41616262+// SysImage holds the result of decoding an image (e.g. PNG) into raw
6363+// RGBA pixel data. Returned by the user-supplied decode callback.
6464+// C: GhosttySysImage
6565+type SysImage struct {
6666+ // Width of the decoded image in pixels.
6767+ Width uint32
6868+6969+ // Height of the decoded image in pixels.
7070+ Height uint32
7171+7272+ // Data is the decoded RGBA pixel data (4 bytes per pixel).
7373+ Data []byte
7474+}
7575+7676+// SysDecodePngFn is the Go callback type for PNG decoding. It receives
7777+// raw PNG data and must return a decoded SysImage. The returned pixel
7878+// data will be copied into library-managed memory; the caller does not
7979+// need to keep the slice alive after returning.
8080+//
8181+// Return a non-nil error to indicate decode failure.
8282+// C: GhosttySysDecodePngFn
8383+type SysDecodePngFn func(data []byte) (*SysImage, error)
8484+8585+// sysDecodePngFn is the currently installed Go decode-PNG callback.
8686+var sysDecodePngFn SysDecodePngFn
8787+8888+// SysSetDecodePng installs a Go callback that decodes PNG image data
8989+// into RGBA pixels. This enables PNG support in the Kitty Graphics
9090+// Protocol. Pass nil to clear the callback and disable PNG decoding.
9191+//
9292+// This function is not safe for concurrent use. Callers must ensure
9393+// that decode configuration is not modified while terminals may
9494+// process image data (e.g. configure at startup before creating
9595+// terminals).
9696+func SysSetDecodePng(fn SysDecodePngFn) error {
9797+ sysDecodePngFn = fn
9898+ if fn == nil {
9999+ return resultError(C.sys_clear_decode_png())
100100+ }
101101+ return resultError(C.sys_set_decode_png_go())
102102+}
103103+42104// SysLogLevel represents the severity level of a log message from the
43105// library. Maps directly to the C enum values.
44106// C: GhosttySysLogLevel
···138200139201 fn(SysLogLevel(level), scope, message)
140202}
203203+204204+//export goSysDecodePngTrampoline
205205+func goSysDecodePngTrampoline(
206206+ _ unsafe.Pointer,
207207+ allocator *C.GhosttyAllocator,
208208+ dataPtr *C.uint8_t,
209209+ dataLen C.size_t,
210210+ out *C.GhosttySysImage,
211211+) C.bool {
212212+ fn := sysDecodePngFn
213213+ if fn == nil {
214214+ return false
215215+ }
216216+217217+ // Build a Go slice over the input PNG data without copying.
218218+ data := unsafe.Slice((*byte)(unsafe.Pointer(dataPtr)), int(dataLen))
219219+220220+ img, err := fn(data)
221221+ if err != nil || img == nil {
222222+ return false
223223+ }
224224+225225+ // Allocate output pixel buffer through the library's allocator so
226226+ // the library can free it later.
227227+ pixelLen := C.size_t(len(img.Data))
228228+ buf := C.ghostty_alloc(allocator, pixelLen)
229229+ if buf == nil {
230230+ return false
231231+ }
232232+233233+ // Copy decoded pixels into the library-owned buffer.
234234+ copy(unsafe.Slice((*byte)(unsafe.Pointer(buf)), int(pixelLen)), img.Data)
235235+236236+ out.width = C.uint32_t(img.Width)
237237+ out.height = C.uint32_t(img.Height)
238238+ out.data = buf
239239+ out.data_len = pixelLen
240240+241241+ return true
242242+}
+55
sys_builtin.go
···11+package libghostty
22+33+// Built-in implementations for system callbacks using Go standard library
44+// packages. These are optional convenience functions that can be passed
55+// directly to their corresponding SysSet* installers.
66+77+import (
88+ "bytes"
99+ "fmt"
1010+ "image"
1111+ "image/png"
1212+)
1313+1414+// SysDecodePng is a ready-to-use [SysDecodePngFn] implementation that
1515+// decodes PNG data using Go's standard [image/png] package. It converts
1616+// any decoded image format to NRGBA (non-premultiplied alpha) before
1717+// returning the raw pixel bytes.
1818+//
1919+// Usage:
2020+//
2121+// libghostty.SysSetDecodePng(libghostty.SysDecodePng)
2222+func SysDecodePng(data []byte) (*SysImage, error) {
2323+ img, err := png.Decode(bytes.NewReader(data))
2424+ if err != nil {
2525+ return nil, fmt.Errorf("png decode: %w", err)
2626+ }
2727+2828+ bounds := img.Bounds()
2929+ w := bounds.Dx()
3030+ h := bounds.Dy()
3131+3232+ // Fast path: if the image is already NRGBA we can use the pixels
3333+ // directly without a per-pixel conversion.
3434+ if nrgba, ok := img.(*image.NRGBA); ok {
3535+ return &SysImage{
3636+ Width: uint32(w),
3737+ Height: uint32(h),
3838+ Data: nrgba.Pix,
3939+ }, nil
4040+ }
4141+4242+ // Slow path: convert arbitrary image types to NRGBA.
4343+ dst := image.NewNRGBA(bounds)
4444+ for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
4545+ for x := bounds.Min.X; x < bounds.Max.X; x++ {
4646+ dst.Set(x, y, img.At(x, y))
4747+ }
4848+ }
4949+5050+ return &SysImage{
5151+ Width: uint32(w),
5252+ Height: uint32(h),
5353+ Data: dst.Pix,
5454+ }, nil
5555+}
···5656 t.Fatalf("SysSetLog(nil) = %v, want nil", err)
5757 }
5858}
5959+6060+func TestSysSetDecodePngNil(t *testing.T) {
6161+ // Clearing with nil should always succeed.
6262+ if err := SysSetDecodePng(nil); err != nil {
6363+ t.Fatalf("SysSetDecodePng(nil) = %v, want nil", err)
6464+ }
6565+}
6666+6767+func TestSysSetDecodePngCallback(t *testing.T) {
6868+ // Install a Go decode-PNG callback and verify it can be set and cleared.
6969+ if err := SysSetDecodePng(func(data []byte) (*SysImage, error) {
7070+ return &SysImage{Width: 1, Height: 1, Data: []byte{0, 0, 0, 255}}, nil
7171+ }); err != nil {
7272+ t.Fatalf("SysSetDecodePng(fn) = %v, want nil", err)
7373+ }
7474+7575+ // Clean up.
7676+ if err := SysSetDecodePng(nil); err != nil {
7777+ t.Fatalf("SysSetDecodePng(nil) = %v, want nil", err)
7878+ }
7979+}
+16
terminal.go
···398398 KittyKeyAll KittyKeyFlags = C.GHOSTTY_KITTY_KEY_ALL
399399)
400400401401+// KittyGraphics returns the Kitty graphics image storage for the
402402+// terminal's active screen. The returned handle is borrowed from
403403+// the terminal and remains valid until the next mutating call
404404+// (e.g. VTWrite or Reset).
405405+func (t *Terminal) KittyGraphics() (*KittyGraphics, error) {
406406+ var ptr C.GhosttyKittyGraphics
407407+ if err := resultError(C.ghostty_terminal_get(
408408+ t.ptr,
409409+ C.GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS,
410410+ unsafe.Pointer(&ptr),
411411+ )); err != nil {
412412+ return nil, err
413413+ }
414414+ return &KittyGraphics{ptr: ptr}, nil
415415+}
416416+401417// GridRef resolves a point in the terminal grid to a grid reference.
402418// The returned GridRef is only valid until the next terminal update.
403419//
+50
terminal_opt.go
···143143 ))
144144}
145145146146+// SetKittyImageStorageLimit sets the Kitty image storage limit in bytes.
147147+// Applied to all initialized screens (primary and alternate). A value of
148148+// zero disables the Kitty graphics protocol entirely, deleting all stored
149149+// images and placements. Pass nil to disable (equivalent to zero).
150150+func (t *Terminal) SetKittyImageStorageLimit(limit *uint64) error {
151151+ var val unsafe.Pointer
152152+ if limit != nil {
153153+ v := C.uint64_t(*limit)
154154+ val = unsafe.Pointer(&v)
155155+ }
156156+ return resultError(C.ghostty_terminal_set(
157157+ t.ptr,
158158+ C.GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT,
159159+ val,
160160+ ))
161161+}
162162+163163+// SetKittyImageMediumFile enables or disables Kitty image loading via the
164164+// file medium.
165165+func (t *Terminal) SetKittyImageMediumFile(enabled bool) error {
166166+ v := C.bool(enabled)
167167+ return resultError(C.ghostty_terminal_set(
168168+ t.ptr,
169169+ C.GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE,
170170+ unsafe.Pointer(&v),
171171+ ))
172172+}
173173+174174+// SetKittyImageMediumTempFile enables or disables Kitty image loading via
175175+// the temporary file medium.
176176+func (t *Terminal) SetKittyImageMediumTempFile(enabled bool) error {
177177+ v := C.bool(enabled)
178178+ return resultError(C.ghostty_terminal_set(
179179+ t.ptr,
180180+ C.GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE,
181181+ unsafe.Pointer(&v),
182182+ ))
183183+}
184184+185185+// SetKittyImageMediumSharedMem enables or disables Kitty image loading via
186186+// the shared memory medium.
187187+func (t *Terminal) SetKittyImageMediumSharedMem(enabled bool) error {
188188+ v := C.bool(enabled)
189189+ return resultError(C.ghostty_terminal_set(
190190+ t.ptr,
191191+ C.GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM,
192192+ unsafe.Pointer(&v),
193193+ ))
194194+}
195195+146196// SetTitle sets the terminal title manually. An empty string clears it.
147197func (t *Terminal) SetTitle(title string) error {
148198 s := C.GhosttyString{