Go bindings for libghostty-vt.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #2 from mitchellh/push-uztmlvyqovyz

Kitty Graphics, PNG Decoding

authored by

Mitchell Hashimoto and committed by
GitHub
d7dbb854 90eeccdc

+1567 -1
+122
.agents/skills/discovering-upstream-apis/SKILL.md
··· 1 + --- 2 + name: discovering-upstream-apis 3 + description: >- 4 + Discovers new or requested C APIs in upstream libghostty-vt headers 5 + and adds Go bindings. Use when asked to add new APIs, add bindings, 6 + check for new upstream APIs, or bind a specific API like 'add the 7 + decode png api'. 8 + --- 9 + 10 + # Discovering Upstream APIs 11 + 12 + Finds new or requested C APIs in the upstream libghostty-vt headers 13 + and creates or updates Go bindings for them. 14 + 15 + ## Checking for upstream changes 16 + 17 + The pinned commit is in `CMakeLists.txt` under 18 + `FetchContent_Declare(ghostty ...)`. 19 + 20 + Run `scripts/latest-upstream-commit.sh` to compare the pinned commit 21 + against the latest upstream `main` branch. It prints the pinned SHA, 22 + the latest SHA, and whether an update is available. 23 + 24 + When an update is available: 25 + 26 + 1. Update the `GIT_TAG` in `CMakeLists.txt` to the latest commit. 27 + 2. Run `make clean && make build` to fetch the new source. 28 + 3. Run the discovery steps above. 29 + 4. Compare against the current Go bindings. 30 + 5. Present findings to the user. 31 + 32 + ## Workflow 33 + 34 + ### 1. Identify what to bind 35 + 36 + Determine the scope based on the user's request: 37 + 38 + - **Specific API** (e.g. "add the decode png api"): Search the 39 + upstream headers for matching functions/types. 40 + - **All new APIs** (e.g. "add the new apis"): Diff the upstream 41 + headers against existing Go bindings to find unbound APIs. 42 + 43 + ### 2. Locate upstream headers 44 + 45 + Headers live at: 46 + 47 + ``` 48 + build/_deps/ghostty-src/zig-out/include/ghostty/vt/ 49 + ``` 50 + 51 + The umbrella header is at 52 + `build/_deps/ghostty-src/zig-out/include/ghostty/vt.h` and includes 53 + all sub-headers. Individual headers are in the `vt/` subdirectory. 54 + 55 + If the build directory does not exist, run `make build` first. 56 + 57 + ### 3. Discover unbound APIs 58 + 59 + To find what's new or missing: 60 + 61 + 1. List all C functions in the upstream headers: 62 + 63 + ``` 64 + grep -rh '^[A-Za-z].*ghostty_' \ 65 + build/_deps/ghostty-src/zig-out/include/ghostty/vt/ \ 66 + | grep '(' 67 + ``` 68 + 69 + 2. List all C functions already referenced in Go files: 70 + 71 + ``` 72 + grep -rh 'C\.\(ghostty_[a-z_]*\)' *.go \ 73 + | grep -oE 'ghostty_[a-z_]+' | sort -u 74 + ``` 75 + 76 + 3. Cross-reference to find unbound functions. 77 + 4. Also check `TODO.md` for known missing items. 78 + 79 + For type/enum discovery: 80 + 81 + ``` 82 + grep -rh 'typedef\|^} Ghostty\|GHOSTTY_[A-Z_]*[, ]' \ 83 + build/_deps/ghostty-src/zig-out/include/ghostty/vt/*.h 84 + ``` 85 + 86 + ### 4. Confirm with the user 87 + 88 + Before writing code, present the list of new APIs found and ask 89 + which ones the user wants bound. Group them by header file. Include: 90 + 91 + - Function signatures 92 + - Related types/enums they depend on 93 + - Which header they come from 94 + 95 + ### 5. Write Go bindings 96 + 97 + Follow the conventions specified in AGENTS.md and patterns in 98 + existing code. Here are some examples: 99 + 100 + - **Simple getters** (`ghostty_terminal_get`): See 101 + `terminal_data.go` — call `ghostty_terminal_get` with the 102 + appropriate enum, cast result to Go type. 103 + - **New/Free lifecycle**: See `render_state.go` — `NewX()` returns 104 + `(*X, error)`, `Close()` frees. 105 + - **Effect callbacks**: See `terminal_effect.go` — use C trampolines 106 + with `//export` and `cgo.Handle` for userdata round-tripping. 107 + - **Tagged unions**: See `terminal.go` `ScrollViewport*` — set tag 108 + then poke the value union via `unsafe.Pointer`. 109 + - **Formatters/iterators**: See `formatter.go` — functional options 110 + pattern, alloc + copy + free for output buffers. 111 + 112 + ### 6. Write tests 113 + 114 + - Add tests in a `_test.go` file matching the source file name. 115 + - Follow existing test patterns (see `terminal_test.go`, 116 + `formatter_test.go`). 117 + - Run `make test` to verify. 118 + 119 + ### 7. Update TODO.md 120 + 121 + - Remove any items from `TODO.md` that have been bound. 122 + - Add any new partial items if applicable.
+34
.agents/skills/discovering-upstream-apis/scripts/latest-upstream-commit.sh
··· 1 + #!/usr/bin/env bash 2 + # Prints the latest commit SHA on the main branch of the upstream 3 + # ghostty repo. Compares it against the pinned commit in 4 + # CMakeLists.txt and reports whether an update is needed. 5 + 6 + set -euo pipefail 7 + 8 + REPO="ghostty-org/ghostty" 9 + BRANCH="main" 10 + CMAKE="CMakeLists.txt" 11 + 12 + # Get the currently pinned commit from CMakeLists.txt. 13 + pinned=$(grep -oP '(?<=GIT_TAG )[0-9a-f]+' "$CMAKE" 2>/dev/null || true) 14 + if [ -z "$pinned" ]; then 15 + echo "ERROR: could not find GIT_TAG in $CMAKE" >&2 16 + exit 1 17 + fi 18 + 19 + # Fetch the latest commit from GitHub. 20 + latest=$(git ls-remote "https://github.com/${REPO}.git" "refs/heads/${BRANCH}" \ 21 + | awk '{print $1}') 22 + if [ -z "$latest" ]; then 23 + echo "ERROR: could not fetch latest commit from ${REPO}" >&2 24 + exit 1 25 + fi 26 + 27 + echo "pinned: $pinned" 28 + echo "latest: $latest" 29 + 30 + if [ "$pinned" = "$latest" ]; then 31 + echo "status: up-to-date" 32 + else 33 + echo "status: update-available" 34 + fi
+148
.agents/skills/upstream-api-feedback/SKILL.md
··· 1 + --- 2 + name: upstream-api-feedback 3 + description: >- 4 + Reviews Go bindings against upstream libghostty-vt C headers and 5 + produces concrete upstream API improvement suggestions. Use when 6 + asked to review bindings for upstream feedback, suggest C API 7 + changes, find API friction, or improve the upstream API. 8 + --- 9 + 10 + # Upstream API Feedback 11 + 12 + Analyzes the Go bindings in this project against the upstream 13 + libghostty-vt C headers to identify concrete, actionable API 14 + changes that would make the bindings more performant, idiomatic, 15 + or easier to use. Output is written to directly to the chat. 16 + 17 + ## When to use 18 + 19 + - The user asks for upstream API feedback or suggestions. 20 + - The user asks to review bindings for performance or ergonomics. 21 + - After binding a new API, to check for friction points. 22 + 23 + ## Workflow 24 + 25 + ### 1. Identify scope 26 + 27 + Determine which bindings to review: 28 + 29 + - **Specific file** (e.g. "review kitty_graphics.go"): Focus on 30 + that file and its corresponding C header. 31 + - **All bindings** (e.g. "review everything"): Scan all `*.go` 32 + files (excluding `_test.go`) and cross-reference with headers. 33 + - **Recent changes**: Use `jj diff` or `git diff` to find what 34 + changed and focus on those files. 35 + 36 + ### 2. Read the Go bindings and C headers together 37 + 38 + For each file in scope: 39 + 40 + 1. Read the Go file to understand the binding patterns used. 41 + 2. Read the corresponding C header in 42 + `build/_deps/ghostty-src/zig-out/include/ghostty/vt/`. 43 + 3. Note every CGo crossing — each `C.ghostty_*()` call. 44 + 45 + ### 3. Apply the analysis checklist 46 + 47 + For each API surface, check for these patterns: 48 + 49 + #### Multi-call data fetching (highest impact) 50 + 51 + Look for Go functions that make multiple `C.ghostty_*_get()` calls 52 + to fetch logically related fields from the same object. Each CGo 53 + call has ~100ns overhead that compounds in hot paths. 54 + 55 + **Signal**: A Go type whose methods each call the same C 56 + `_get(handle, ENUM_VARIANT, &out)` function with different enum 57 + values — especially when callers typically need several fields 58 + together. 59 + 60 + **Suggestion**: A sized struct (like `GhosttyRenderStateColors`) 61 + that returns all fields in a single call. Reference the existing 62 + `GHOSTTY_INIT_SIZED` pattern. 63 + 64 + #### Pointer/length splits 65 + 66 + Look for cases where pointer and length are separate enum variants 67 + in the same `_get()` API (e.g. `DATA_PTR` + `DATA_LEN`). These 68 + are semantically one value split across two calls. 69 + 70 + **Signal**: The Go binding must make two sequential calls and has 71 + no atomicity guarantee between them. 72 + 73 + **Suggestion**: Either fold into the sized struct above, use 74 + `GhosttyString`-style `{ptr, len}` as a single variant, or add a 75 + dedicated function. 76 + 77 + #### Repeated parameter triples 78 + 79 + Look for multiple C functions that take the same parameter 80 + combination (e.g. `(iterator, image, terminal)`) and are typically 81 + called together per iteration step. 82 + 83 + **Signal**: The Go code calls 3-4 functions with identical 84 + arguments in sequence during a loop body. 85 + 86 + **Suggestion**: A combined function returning a struct with all 87 + results, cutting N CGo crossings to 1. 88 + 89 + #### Two-phase initialization 90 + 91 + Look for patterns where an object must be allocated, then populated 92 + via a separate call before it's usable (e.g. `_new()` then 93 + `_get(POPULATE, &handle)`). 94 + 95 + **Signal**: The Go binding wraps this in a helper but the C API 96 + still requires two calls where one would suffice. 97 + 98 + **Suggestion**: A combined constructor, or making the populate step 99 + part of `_new()`. 100 + 101 + #### Missing convenience variants 102 + 103 + Look for C APIs that could offer a simpler overload for the common 104 + case while keeping the flexible version. 105 + 106 + **Signal**: The Go binding wraps a complex C API with a simpler Go 107 + function that most callers use, hiding parameters that are almost 108 + always the same value. 109 + 110 + **Suggestion**: A C convenience function for the common case. 111 + 112 + ### 4. Assess impact 113 + 114 + Rate each suggestion: 115 + 116 + - **Hot path**: Called per-frame or per-placement in a render loop. 117 + CGo overhead is multiplied by iteration count. High impact. 118 + - **Setup path**: Called once during initialization or 119 + configuration. Low impact — ergonomics matter more than perf. 120 + - **Ergonomic**: Doesn't affect performance but makes the API 121 + harder to use correctly or requires awkward binding code. 122 + 123 + ### 5. Write the output 124 + 125 + Write findings to chat. Format: 126 + 127 + ```markdown 128 + # Upstream API Feedback 129 + 130 + Summary of the review scope and methodology. 131 + 132 + ## Suggestion Title 133 + 134 + **Impact**: Hot path / Setup path / Ergonomic 135 + **Files**: `header.h`, `binding.go` 136 + 137 + Description of the current pattern, why it's suboptimal, and the 138 + concrete C API change. Include a struct/function signature sketch. 139 + 140 + ## Next Suggestion 141 + 142 + ... 143 + ``` 144 + 145 + Group suggestions by impact (hot path first). Include concrete C 146 + type/function signatures — not vague ideas. Reference existing 147 + libghostty-vt patterns (like `GhosttyRenderStateColors`) as 148 + precedent when applicable.
+8 -1
TODO.md
··· 4 4 5 5 - [ ] Key encoding (`key.h`, `key/encoder.h`, `key/event.h`) 6 6 - [ ] Mouse encoding (`mouse.h`, `mouse/encoder.h`, `mouse/event.h`) 7 - - [ ] Render state (`render.h`) 8 7 - [ ] OSC parser (`osc.h`) 9 8 - [ ] SGR parser (`sgr.h`) 10 9 - [ ] Paste utilities (`paste.h`) 11 10 - [ ] Focus encoding (`focus.h`) 11 + - [x] Kitty graphics (`kitty_graphics.h`) 12 + - [ ] Allocator (`allocator.h` — `ghostty_alloc`, `ghostty_free`) 13 + - [ ] Selection type (`selection.h`) 12 14 13 15 ## Partially Bound 14 16 ··· 16 18 - [ ] `ghostty_size_report_encode()` 17 19 - [ ] `ghostty_type_json()` 18 20 - [ ] `ghostty_style_default()` 21 + - [ ] `ghostty_color_rgb_get()` 22 + - [ ] `ghostty_grid_ref_hyperlink_uri()` 23 + - [ ] `ghostty_terminal_point_from_grid_ref()` 24 + - [ ] `ghostty_formatter_format_buf()` 25 + - [ ] `ghostty_focus_encode()`
+536
kitty_graphics.go
··· 1 + package libghostty 2 + 3 + // Kitty graphics protocol bindings wrapping the C API from kitty_graphics.h. 4 + // Provides access to images and placements stored via the Kitty graphics 5 + // protocol. 6 + 7 + /* 8 + #include <ghostty/vt.h> 9 + 10 + // Helper to create a properly initialized GhosttySelection (sized struct). 11 + static inline GhosttySelection init_selection() { 12 + GhosttySelection s = GHOSTTY_INIT_SIZED(GhosttySelection); 13 + return s; 14 + } 15 + */ 16 + import "C" 17 + 18 + import "unsafe" 19 + 20 + // KittyGraphics is a handle to the Kitty graphics image storage 21 + // associated with a terminal's active screen. It is borrowed from 22 + // the terminal and remains valid until the next mutating terminal 23 + // call (e.g. VTWrite or Reset). 24 + // 25 + // C: GhosttyKittyGraphics 26 + type KittyGraphics struct { 27 + ptr C.GhosttyKittyGraphics 28 + } 29 + 30 + // KittyGraphicsImage is a handle to a single Kitty graphics image. 31 + // It is borrowed from the storage and remains valid until the next 32 + // mutating terminal call. 33 + // 34 + // C: GhosttyKittyGraphicsImage 35 + type KittyGraphicsImage struct { 36 + ptr C.GhosttyKittyGraphicsImage 37 + } 38 + 39 + // KittyGraphicsPlacementIterator iterates over placements in the 40 + // Kitty graphics storage. It is independently owned and must be 41 + // freed by calling Close, but the data it yields is only valid 42 + // while the underlying terminal is not mutated. 43 + // 44 + // C: GhosttyKittyGraphicsPlacementIterator 45 + type KittyGraphicsPlacementIterator struct { 46 + ptr C.GhosttyKittyGraphicsPlacementIterator 47 + } 48 + 49 + // KittyPlacementLayer classifies z-layer for kitty graphics placements. 50 + // Based on the kitty protocol z-index conventions. 51 + // 52 + // C: GhosttyKittyPlacementLayer 53 + type KittyPlacementLayer int 54 + 55 + const ( 56 + // KittyPlacementLayerAll disables layer filtering (all placements). 57 + KittyPlacementLayerAll KittyPlacementLayer = C.GHOSTTY_KITTY_PLACEMENT_LAYER_ALL 58 + 59 + // KittyPlacementLayerBelowBG matches placements below cell background 60 + // (z < INT32_MIN/2). 61 + KittyPlacementLayerBelowBG KittyPlacementLayer = C.GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_BG 62 + 63 + // KittyPlacementLayerBelowText matches placements above background but 64 + // below text (INT32_MIN/2 <= z < 0). 65 + KittyPlacementLayerBelowText KittyPlacementLayer = C.GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_TEXT 66 + 67 + // KittyPlacementLayerAboveText matches placements above text (z >= 0). 68 + KittyPlacementLayerAboveText KittyPlacementLayer = C.GHOSTTY_KITTY_PLACEMENT_LAYER_ABOVE_TEXT 69 + ) 70 + 71 + // KittyImageFormat describes the pixel format of a Kitty graphics image. 72 + // 73 + // C: GhosttyKittyImageFormat 74 + type KittyImageFormat int 75 + 76 + const ( 77 + // KittyImageFormatRGB is 24-bit RGB (3 bytes per pixel). 78 + KittyImageFormatRGB KittyImageFormat = C.GHOSTTY_KITTY_IMAGE_FORMAT_RGB 79 + 80 + // KittyImageFormatRGBA is 32-bit RGBA (4 bytes per pixel). 81 + KittyImageFormatRGBA KittyImageFormat = C.GHOSTTY_KITTY_IMAGE_FORMAT_RGBA 82 + 83 + // KittyImageFormatPNG is compressed PNG data. 84 + KittyImageFormatPNG KittyImageFormat = C.GHOSTTY_KITTY_IMAGE_FORMAT_PNG 85 + 86 + // KittyImageFormatGrayAlpha is 16-bit gray+alpha (2 bytes per pixel). 87 + KittyImageFormatGrayAlpha KittyImageFormat = C.GHOSTTY_KITTY_IMAGE_FORMAT_GRAY_ALPHA 88 + 89 + // KittyImageFormatGray is 8-bit grayscale (1 byte per pixel). 90 + KittyImageFormatGray KittyImageFormat = C.GHOSTTY_KITTY_IMAGE_FORMAT_GRAY 91 + ) 92 + 93 + // KittyImageCompression describes the compression of a Kitty graphics image. 94 + // 95 + // C: GhosttyKittyImageCompression 96 + type KittyImageCompression int 97 + 98 + const ( 99 + // KittyImageCompressionNone means no compression. 100 + KittyImageCompressionNone KittyImageCompression = C.GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE 101 + 102 + // KittyImageCompressionZlibDeflate means zlib/deflate compression. 103 + KittyImageCompressionZlibDeflate KittyImageCompression = C.GHOSTTY_KITTY_IMAGE_COMPRESSION_ZLIB_DEFLATE 104 + ) 105 + 106 + // Selection represents a grid selection range defined by two grid references. 107 + // 108 + // C: GhosttySelection 109 + type Selection struct { 110 + // Start is the start of the selection range (inclusive). 111 + Start GridRef 112 + 113 + // End is the end of the selection range (inclusive). 114 + End GridRef 115 + 116 + // Rectangle indicates whether the selection is rectangular (block) 117 + // rather than linear. 118 + Rectangle bool 119 + } 120 + 121 + // selectionFromC converts a C GhosttySelection to a Go Selection. 122 + func selectionFromC(cs C.GhosttySelection) Selection { 123 + return Selection{ 124 + Start: GridRef{ref: cs.start}, 125 + End: GridRef{ref: cs.end}, 126 + Rectangle: bool(cs.rectangle), 127 + } 128 + } 129 + 130 + // PlacementIterator populates the given iterator with placement data 131 + // from this storage. The iterator must have been created with 132 + // NewKittyGraphicsPlacementIterator. 133 + func (kg *KittyGraphics) PlacementIterator(iter *KittyGraphicsPlacementIterator) error { 134 + return resultError(C.ghostty_kitty_graphics_get( 135 + kg.ptr, 136 + C.GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR, 137 + unsafe.Pointer(&iter.ptr), 138 + )) 139 + } 140 + 141 + // Image looks up a Kitty graphics image by its image ID. Returns nil 142 + // if no image with the given ID exists. 143 + func (kg *KittyGraphics) Image(imageID uint32) *KittyGraphicsImage { 144 + ptr := C.ghostty_kitty_graphics_image(kg.ptr, C.uint32_t(imageID)) 145 + if ptr == nil { 146 + return nil 147 + } 148 + return &KittyGraphicsImage{ptr: ptr} 149 + } 150 + 151 + // ID returns the image ID. 152 + func (img *KittyGraphicsImage) ID() (uint32, error) { 153 + var v C.uint32_t 154 + if err := resultError(C.ghostty_kitty_graphics_image_get( 155 + img.ptr, 156 + C.GHOSTTY_KITTY_IMAGE_DATA_ID, 157 + unsafe.Pointer(&v), 158 + )); err != nil { 159 + return 0, err 160 + } 161 + return uint32(v), nil 162 + } 163 + 164 + // Number returns the image number. 165 + func (img *KittyGraphicsImage) Number() (uint32, error) { 166 + var v C.uint32_t 167 + if err := resultError(C.ghostty_kitty_graphics_image_get( 168 + img.ptr, 169 + C.GHOSTTY_KITTY_IMAGE_DATA_NUMBER, 170 + unsafe.Pointer(&v), 171 + )); err != nil { 172 + return 0, err 173 + } 174 + return uint32(v), nil 175 + } 176 + 177 + // Width returns the image width in pixels. 178 + func (img *KittyGraphicsImage) Width() (uint32, error) { 179 + var v C.uint32_t 180 + if err := resultError(C.ghostty_kitty_graphics_image_get( 181 + img.ptr, 182 + C.GHOSTTY_KITTY_IMAGE_DATA_WIDTH, 183 + unsafe.Pointer(&v), 184 + )); err != nil { 185 + return 0, err 186 + } 187 + return uint32(v), nil 188 + } 189 + 190 + // Height returns the image height in pixels. 191 + func (img *KittyGraphicsImage) Height() (uint32, error) { 192 + var v C.uint32_t 193 + if err := resultError(C.ghostty_kitty_graphics_image_get( 194 + img.ptr, 195 + C.GHOSTTY_KITTY_IMAGE_DATA_HEIGHT, 196 + unsafe.Pointer(&v), 197 + )); err != nil { 198 + return 0, err 199 + } 200 + return uint32(v), nil 201 + } 202 + 203 + // Format returns the pixel format of the image. 204 + func (img *KittyGraphicsImage) Format() (KittyImageFormat, error) { 205 + var v C.GhosttyKittyImageFormat 206 + if err := resultError(C.ghostty_kitty_graphics_image_get( 207 + img.ptr, 208 + C.GHOSTTY_KITTY_IMAGE_DATA_FORMAT, 209 + unsafe.Pointer(&v), 210 + )); err != nil { 211 + return 0, err 212 + } 213 + return KittyImageFormat(v), nil 214 + } 215 + 216 + // Compression returns the compression of the image. 217 + func (img *KittyGraphicsImage) Compression() (KittyImageCompression, error) { 218 + var v C.GhosttyKittyImageCompression 219 + if err := resultError(C.ghostty_kitty_graphics_image_get( 220 + img.ptr, 221 + C.GHOSTTY_KITTY_IMAGE_DATA_COMPRESSION, 222 + unsafe.Pointer(&v), 223 + )); err != nil { 224 + return 0, err 225 + } 226 + return KittyImageCompression(v), nil 227 + } 228 + 229 + // Data returns a borrowed slice of the raw pixel data. The slice is 230 + // only valid until the next mutating terminal call. 231 + func (img *KittyGraphicsImage) Data() ([]byte, error) { 232 + var ptr *C.uint8_t 233 + if err := resultError(C.ghostty_kitty_graphics_image_get( 234 + img.ptr, 235 + C.GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR, 236 + unsafe.Pointer(&ptr), 237 + )); err != nil { 238 + return nil, err 239 + } 240 + 241 + var length C.size_t 242 + if err := resultError(C.ghostty_kitty_graphics_image_get( 243 + img.ptr, 244 + C.GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN, 245 + unsafe.Pointer(&length), 246 + )); err != nil { 247 + return nil, err 248 + } 249 + 250 + if ptr == nil || length == 0 { 251 + return nil, nil 252 + } 253 + 254 + return unsafe.Slice((*byte)(unsafe.Pointer(ptr)), int(length)), nil 255 + } 256 + 257 + // NewKittyGraphicsPlacementIterator creates a new placement iterator. 258 + // Call KittyGraphics.PlacementIterator to populate it with data, then 259 + // iterate with Next and read fields with the getter methods. 260 + // The iterator must be freed by calling Close. 261 + func NewKittyGraphicsPlacementIterator() (*KittyGraphicsPlacementIterator, error) { 262 + var ptr C.GhosttyKittyGraphicsPlacementIterator 263 + if err := resultError(C.ghostty_kitty_graphics_placement_iterator_new(nil, &ptr)); err != nil { 264 + return nil, err 265 + } 266 + return &KittyGraphicsPlacementIterator{ptr: ptr}, nil 267 + } 268 + 269 + // Close frees the placement iterator. After this call, the iterator 270 + // must not be used. 271 + func (it *KittyGraphicsPlacementIterator) Close() { 272 + C.ghostty_kitty_graphics_placement_iterator_free(it.ptr) 273 + } 274 + 275 + // SetLayer sets the z-layer filter for the iterator. Only placements 276 + // matching the given layer will be returned by Next. The default is 277 + // KittyPlacementLayerAll (no filtering). 278 + func (it *KittyGraphicsPlacementIterator) SetLayer(layer KittyPlacementLayer) error { 279 + v := C.GhosttyKittyPlacementLayer(layer) 280 + return resultError(C.ghostty_kitty_graphics_placement_iterator_set( 281 + it.ptr, 282 + C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_LAYER, 283 + unsafe.Pointer(&v), 284 + )) 285 + } 286 + 287 + // Next advances the iterator to the next placement. Returns true if 288 + // a placement is available, false when iteration is complete. 289 + func (it *KittyGraphicsPlacementIterator) Next() bool { 290 + return bool(C.ghostty_kitty_graphics_placement_next(it.ptr)) 291 + } 292 + 293 + // ImageID returns the image ID of the current placement. 294 + func (it *KittyGraphicsPlacementIterator) ImageID() (uint32, error) { 295 + var v C.uint32_t 296 + if err := resultError(C.ghostty_kitty_graphics_placement_get( 297 + it.ptr, 298 + C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID, 299 + unsafe.Pointer(&v), 300 + )); err != nil { 301 + return 0, err 302 + } 303 + return uint32(v), nil 304 + } 305 + 306 + // PlacementID returns the placement ID of the current placement. 307 + func (it *KittyGraphicsPlacementIterator) PlacementID() (uint32, error) { 308 + var v C.uint32_t 309 + if err := resultError(C.ghostty_kitty_graphics_placement_get( 310 + it.ptr, 311 + C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID, 312 + unsafe.Pointer(&v), 313 + )); err != nil { 314 + return 0, err 315 + } 316 + return uint32(v), nil 317 + } 318 + 319 + // IsVirtual reports whether the current placement is a virtual 320 + // (unicode placeholder) placement. 321 + func (it *KittyGraphicsPlacementIterator) IsVirtual() (bool, error) { 322 + var v C.bool 323 + if err := resultError(C.ghostty_kitty_graphics_placement_get( 324 + it.ptr, 325 + C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL, 326 + unsafe.Pointer(&v), 327 + )); err != nil { 328 + return false, err 329 + } 330 + return bool(v), nil 331 + } 332 + 333 + // XOffset returns the pixel offset from the left edge of the cell. 334 + func (it *KittyGraphicsPlacementIterator) XOffset() (uint32, error) { 335 + var v C.uint32_t 336 + if err := resultError(C.ghostty_kitty_graphics_placement_get( 337 + it.ptr, 338 + C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET, 339 + unsafe.Pointer(&v), 340 + )); err != nil { 341 + return 0, err 342 + } 343 + return uint32(v), nil 344 + } 345 + 346 + // YOffset returns the pixel offset from the top edge of the cell. 347 + func (it *KittyGraphicsPlacementIterator) YOffset() (uint32, error) { 348 + var v C.uint32_t 349 + if err := resultError(C.ghostty_kitty_graphics_placement_get( 350 + it.ptr, 351 + C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET, 352 + unsafe.Pointer(&v), 353 + )); err != nil { 354 + return 0, err 355 + } 356 + return uint32(v), nil 357 + } 358 + 359 + // SourceX returns the source rectangle x origin in pixels. 360 + func (it *KittyGraphicsPlacementIterator) SourceX() (uint32, error) { 361 + var v C.uint32_t 362 + if err := resultError(C.ghostty_kitty_graphics_placement_get( 363 + it.ptr, 364 + C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_X, 365 + unsafe.Pointer(&v), 366 + )); err != nil { 367 + return 0, err 368 + } 369 + return uint32(v), nil 370 + } 371 + 372 + // SourceY returns the source rectangle y origin in pixels. 373 + func (it *KittyGraphicsPlacementIterator) SourceY() (uint32, error) { 374 + var v C.uint32_t 375 + if err := resultError(C.ghostty_kitty_graphics_placement_get( 376 + it.ptr, 377 + C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_Y, 378 + unsafe.Pointer(&v), 379 + )); err != nil { 380 + return 0, err 381 + } 382 + return uint32(v), nil 383 + } 384 + 385 + // SourceWidth returns the source rectangle width in pixels 386 + // (0 = full image width). 387 + func (it *KittyGraphicsPlacementIterator) SourceWidth() (uint32, error) { 388 + var v C.uint32_t 389 + if err := resultError(C.ghostty_kitty_graphics_placement_get( 390 + it.ptr, 391 + C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_WIDTH, 392 + unsafe.Pointer(&v), 393 + )); err != nil { 394 + return 0, err 395 + } 396 + return uint32(v), nil 397 + } 398 + 399 + // SourceHeight returns the source rectangle height in pixels 400 + // (0 = full image height). 401 + func (it *KittyGraphicsPlacementIterator) SourceHeight() (uint32, error) { 402 + var v C.uint32_t 403 + if err := resultError(C.ghostty_kitty_graphics_placement_get( 404 + it.ptr, 405 + C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_HEIGHT, 406 + unsafe.Pointer(&v), 407 + )); err != nil { 408 + return 0, err 409 + } 410 + return uint32(v), nil 411 + } 412 + 413 + // Columns returns the number of columns this placement occupies. 414 + func (it *KittyGraphicsPlacementIterator) Columns() (uint32, error) { 415 + var v C.uint32_t 416 + if err := resultError(C.ghostty_kitty_graphics_placement_get( 417 + it.ptr, 418 + C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS, 419 + unsafe.Pointer(&v), 420 + )); err != nil { 421 + return 0, err 422 + } 423 + return uint32(v), nil 424 + } 425 + 426 + // Rows returns the number of rows this placement occupies. 427 + func (it *KittyGraphicsPlacementIterator) Rows() (uint32, error) { 428 + var v C.uint32_t 429 + if err := resultError(C.ghostty_kitty_graphics_placement_get( 430 + it.ptr, 431 + C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_ROWS, 432 + unsafe.Pointer(&v), 433 + )); err != nil { 434 + return 0, err 435 + } 436 + return uint32(v), nil 437 + } 438 + 439 + // Z returns the z-index of the current placement. 440 + func (it *KittyGraphicsPlacementIterator) Z() (int32, error) { 441 + var v C.int32_t 442 + if err := resultError(C.ghostty_kitty_graphics_placement_get( 443 + it.ptr, 444 + C.GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z, 445 + unsafe.Pointer(&v), 446 + )); err != nil { 447 + return 0, err 448 + } 449 + return int32(v), nil 450 + } 451 + 452 + // Rect computes the grid rectangle occupied by the current placement. 453 + // Virtual placements (unicode placeholders) return an error with 454 + // ResultNoValue. 455 + func (it *KittyGraphicsPlacementIterator) Rect(img *KittyGraphicsImage, t *Terminal) (*Selection, error) { 456 + cs := C.init_selection() 457 + if err := resultError(C.ghostty_kitty_graphics_placement_rect( 458 + it.ptr, 459 + img.ptr, 460 + t.ptr, 461 + &cs, 462 + )); err != nil { 463 + return nil, err 464 + } 465 + sel := selectionFromC(cs) 466 + return &sel, nil 467 + } 468 + 469 + // PixelSize computes the rendered pixel dimensions of the current 470 + // placement, accounting for the source rectangle, specified 471 + // columns/rows, and aspect ratio. 472 + func (it *KittyGraphicsPlacementIterator) PixelSize(img *KittyGraphicsImage, t *Terminal) (width, height uint32, err error) { 473 + var w, h C.uint32_t 474 + if err := resultError(C.ghostty_kitty_graphics_placement_pixel_size( 475 + it.ptr, 476 + img.ptr, 477 + t.ptr, 478 + &w, 479 + &h, 480 + )); err != nil { 481 + return 0, 0, err 482 + } 483 + return uint32(w), uint32(h), nil 484 + } 485 + 486 + // GridSize computes the number of grid columns and rows the current 487 + // placement occupies. 488 + func (it *KittyGraphicsPlacementIterator) GridSize(img *KittyGraphicsImage, t *Terminal) (cols, rows uint32, err error) { 489 + var c, r C.uint32_t 490 + if err := resultError(C.ghostty_kitty_graphics_placement_grid_size( 491 + it.ptr, 492 + img.ptr, 493 + t.ptr, 494 + &c, 495 + &r, 496 + )); err != nil { 497 + return 0, 0, err 498 + } 499 + return uint32(c), uint32(r), nil 500 + } 501 + 502 + // ViewportPos returns the viewport-relative grid position of the 503 + // current placement. The row can be negative for partially visible 504 + // placements. Returns an error with ResultNoValue when fully 505 + // off-screen or for virtual placements. 506 + func (it *KittyGraphicsPlacementIterator) ViewportPos(img *KittyGraphicsImage, t *Terminal) (col, row int32, err error) { 507 + var c, r C.int32_t 508 + if err := resultError(C.ghostty_kitty_graphics_placement_viewport_pos( 509 + it.ptr, 510 + img.ptr, 511 + t.ptr, 512 + &c, 513 + &r, 514 + )); err != nil { 515 + return 0, 0, err 516 + } 517 + return int32(c), int32(r), nil 518 + } 519 + 520 + // SourceRect returns the resolved source rectangle for the current 521 + // placement in pixels, clamped to the actual image bounds. A width 522 + // or height of 0 in the placement means "use the full image dimension". 523 + func (it *KittyGraphicsPlacementIterator) SourceRect(img *KittyGraphicsImage) (x, y, width, height uint32, err error) { 524 + var cx, cy, cw, ch C.uint32_t 525 + if err := resultError(C.ghostty_kitty_graphics_placement_source_rect( 526 + it.ptr, 527 + img.ptr, 528 + &cx, 529 + &cy, 530 + &cw, 531 + &ch, 532 + )); err != nil { 533 + return 0, 0, 0, 0, err 534 + } 535 + return uint32(cx), uint32(cy), uint32(cw), uint32(ch), nil 536 + }
+399
kitty_graphics_test.go
··· 1 + package libghostty 2 + 3 + import ( 4 + "testing" 5 + ) 6 + 7 + // newKittyTerminal creates a terminal with Kitty graphics enabled 8 + // (PNG decode callback, WritePty handler, storage limit, and cell 9 + // pixel dimensions), ready for Kitty graphics protocol testing. 10 + func newKittyTerminal(t *testing.T) *Terminal { 11 + t.Helper() 12 + 13 + // Install the PNG decoder. 14 + if err := SysSetDecodePng(SysDecodePng); err != nil { 15 + t.Fatal(err) 16 + } 17 + 18 + term, err := NewTerminal( 19 + WithSize(80, 24), 20 + // Install a WritePty handler so the terminal can send 21 + // protocol responses (required for kitty graphics). 22 + WithWritePty(func(data []byte) {}), 23 + ) 24 + if err != nil { 25 + t.Fatal(err) 26 + } 27 + 28 + // Set cell pixel dimensions (required for image placement calculations). 29 + if err := term.Resize(80, 24, 8, 16); err != nil { 30 + t.Fatal(err) 31 + } 32 + 33 + // Enable Kitty graphics with a generous storage limit. 34 + limit := uint64(64 * 1024 * 1024) 35 + if err := term.SetKittyImageStorageLimit(&limit); err != nil { 36 + t.Fatal(err) 37 + } 38 + 39 + return term 40 + } 41 + 42 + // sendKittyImage sends a 1x1 PNG image to the terminal using the Kitty 43 + // graphics protocol. Uses the same image as the upstream C example. 44 + // The terminal auto-assigns the image ID. 45 + func sendKittyImage(t *testing.T, term *Terminal) { 46 + t.Helper() 47 + 48 + // Kitty graphics protocol: transmit+display, PNG format (f=100), 49 + // direct transmission (t=d, implicit), request response (q=1). 50 + // Uses the same 1x1 red PNG as the upstream C example. 51 + cmd := "\x1b_Ga=T,f=100,q=1;" + 52 + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA" + 53 + "DUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==" + 54 + "\x1b\\" 55 + term.VTWrite([]byte(cmd)) 56 + } 57 + 58 + func TestKittyGraphicsStorageLimit(t *testing.T) { 59 + term, err := NewTerminal(WithSize(80, 24)) 60 + if err != nil { 61 + t.Fatal(err) 62 + } 63 + defer term.Close() 64 + 65 + // Set a storage limit. 66 + limit := uint64(1024 * 1024) 67 + if err := term.SetKittyImageStorageLimit(&limit); err != nil { 68 + t.Fatal(err) 69 + } 70 + 71 + // Disable by passing nil. 72 + if err := term.SetKittyImageStorageLimit(nil); err != nil { 73 + t.Fatal(err) 74 + } 75 + } 76 + 77 + func TestKittyGraphicsMediumSetters(t *testing.T) { 78 + term, err := NewTerminal(WithSize(80, 24)) 79 + if err != nil { 80 + t.Fatal(err) 81 + } 82 + defer term.Close() 83 + 84 + if err := term.SetKittyImageMediumFile(true); err != nil { 85 + t.Fatal(err) 86 + } 87 + if err := term.SetKittyImageMediumFile(false); err != nil { 88 + t.Fatal(err) 89 + } 90 + if err := term.SetKittyImageMediumTempFile(true); err != nil { 91 + t.Fatal(err) 92 + } 93 + if err := term.SetKittyImageMediumSharedMem(true); err != nil { 94 + t.Fatal(err) 95 + } 96 + } 97 + 98 + func TestKittyGraphicsHandle(t *testing.T) { 99 + term := newKittyTerminal(t) 100 + defer term.Close() 101 + 102 + kg, err := term.KittyGraphics() 103 + if err != nil { 104 + t.Fatal(err) 105 + } 106 + if kg == nil { 107 + t.Fatal("expected non-nil KittyGraphics handle") 108 + } 109 + } 110 + 111 + func TestKittyGraphicsPlacementIteratorEmpty(t *testing.T) { 112 + term := newKittyTerminal(t) 113 + defer term.Close() 114 + 115 + kg, err := term.KittyGraphics() 116 + if err != nil { 117 + t.Fatal(err) 118 + } 119 + 120 + iter, err := NewKittyGraphicsPlacementIterator() 121 + if err != nil { 122 + t.Fatal(err) 123 + } 124 + defer iter.Close() 125 + 126 + if err := kg.PlacementIterator(iter); err != nil { 127 + t.Fatal(err) 128 + } 129 + 130 + // No images sent; iterator should be empty. 131 + if iter.Next() { 132 + t.Fatal("expected no placements in empty terminal") 133 + } 134 + } 135 + 136 + func TestKittyGraphicsImageLookupMiss(t *testing.T) { 137 + term := newKittyTerminal(t) 138 + defer term.Close() 139 + 140 + kg, err := term.KittyGraphics() 141 + if err != nil { 142 + t.Fatal(err) 143 + } 144 + 145 + // No image with ID 999 should exist. 146 + img := kg.Image(999) 147 + if img != nil { 148 + t.Fatal("expected nil for non-existent image ID") 149 + } 150 + } 151 + 152 + func TestKittyGraphicsImageSendAndLookup(t *testing.T) { 153 + term := newKittyTerminal(t) 154 + defer term.Close() 155 + 156 + sendKittyImage(t, term) 157 + 158 + kg, err := term.KittyGraphics() 159 + if err != nil { 160 + t.Fatal(err) 161 + } 162 + 163 + // Find the image ID by iterating placements (terminal auto-assigns IDs). 164 + iter, err := NewKittyGraphicsPlacementIterator() 165 + if err != nil { 166 + t.Fatal(err) 167 + } 168 + defer iter.Close() 169 + 170 + if err := kg.PlacementIterator(iter); err != nil { 171 + t.Fatal(err) 172 + } 173 + 174 + if !iter.Next() { 175 + t.Fatal("expected at least one placement after sending image") 176 + } 177 + 178 + imageID, err := iter.ImageID() 179 + if err != nil { 180 + t.Fatal(err) 181 + } 182 + 183 + // Look up the image by its ID. 184 + img := kg.Image(imageID) 185 + if img == nil { 186 + t.Fatal("expected non-nil image for placement's image ID") 187 + } 188 + 189 + // Verify image properties. 190 + id, err := img.ID() 191 + if err != nil { 192 + t.Fatal(err) 193 + } 194 + if id != imageID { 195 + t.Fatalf("expected image ID %d, got %d", imageID, id) 196 + } 197 + 198 + w, err := img.Width() 199 + if err != nil { 200 + t.Fatal(err) 201 + } 202 + h, err := img.Height() 203 + if err != nil { 204 + t.Fatal(err) 205 + } 206 + if w != 1 || h != 1 { 207 + t.Fatalf("expected 1x1 image, got %dx%d", w, h) 208 + } 209 + 210 + // Check format — after PNG decoding it should be RGBA. 211 + format, err := img.Format() 212 + if err != nil { 213 + t.Fatal(err) 214 + } 215 + if format != KittyImageFormatRGBA { 216 + t.Fatalf("expected RGBA format, got %d", format) 217 + } 218 + 219 + // Check compression. 220 + compression, err := img.Compression() 221 + if err != nil { 222 + t.Fatal(err) 223 + } 224 + if compression != KittyImageCompressionNone { 225 + t.Fatalf("expected no compression after decode, got %d", compression) 226 + } 227 + 228 + // Check data is accessible. 229 + data, err := img.Data() 230 + if err != nil { 231 + t.Fatal(err) 232 + } 233 + if len(data) == 0 { 234 + t.Fatal("expected non-empty pixel data") 235 + } 236 + // 1x1 RGBA = 4 bytes. 237 + if len(data) != 4 { 238 + t.Fatalf("expected 4 bytes of pixel data, got %d", len(data)) 239 + } 240 + } 241 + 242 + func TestKittyGraphicsPlacementIteration(t *testing.T) { 243 + term := newKittyTerminal(t) 244 + defer term.Close() 245 + 246 + sendKittyImage(t, term) 247 + 248 + kg, err := term.KittyGraphics() 249 + if err != nil { 250 + t.Fatal(err) 251 + } 252 + 253 + iter, err := NewKittyGraphicsPlacementIterator() 254 + if err != nil { 255 + t.Fatal(err) 256 + } 257 + defer iter.Close() 258 + 259 + if err := kg.PlacementIterator(iter); err != nil { 260 + t.Fatal(err) 261 + } 262 + 263 + if !iter.Next() { 264 + t.Fatal("expected at least one placement") 265 + } 266 + 267 + // Verify we can read placement fields. 268 + _, err = iter.PlacementID() 269 + if err != nil { 270 + t.Fatal(err) 271 + } 272 + 273 + isVirtual, err := iter.IsVirtual() 274 + if err != nil { 275 + t.Fatal(err) 276 + } 277 + if isVirtual { 278 + t.Fatal("expected non-virtual placement for direct display") 279 + } 280 + 281 + _, err = iter.Z() 282 + if err != nil { 283 + t.Fatal(err) 284 + } 285 + 286 + // Look up the image for rendering helpers. 287 + imageID, err := iter.ImageID() 288 + if err != nil { 289 + t.Fatal(err) 290 + } 291 + img := kg.Image(imageID) 292 + if img == nil { 293 + t.Fatal("expected image lookup to succeed") 294 + } 295 + 296 + // PixelSize should return valid dimensions. 297 + pw, ph, err := iter.PixelSize(img, term) 298 + if err != nil { 299 + t.Fatal(err) 300 + } 301 + if pw == 0 || ph == 0 { 302 + t.Fatalf("expected non-zero pixel size, got %dx%d", pw, ph) 303 + } 304 + 305 + // GridSize should return valid dimensions. 306 + gc, gr, err := iter.GridSize(img, term) 307 + if err != nil { 308 + t.Fatal(err) 309 + } 310 + if gc == 0 || gr == 0 { 311 + t.Fatalf("expected non-zero grid size, got %dx%d", gc, gr) 312 + } 313 + 314 + // SourceRect should succeed. 315 + _, _, sw, sh, err := iter.SourceRect(img) 316 + if err != nil { 317 + t.Fatal(err) 318 + } 319 + if sw == 0 || sh == 0 { 320 + t.Fatalf("expected non-zero source rect size, got %dx%d", sw, sh) 321 + } 322 + } 323 + 324 + func TestKittyGraphicsPlacementLayerFilter(t *testing.T) { 325 + term := newKittyTerminal(t) 326 + defer term.Close() 327 + 328 + sendKittyImage(t, term) 329 + 330 + kg, err := term.KittyGraphics() 331 + if err != nil { 332 + t.Fatal(err) 333 + } 334 + 335 + iter, err := NewKittyGraphicsPlacementIterator() 336 + if err != nil { 337 + t.Fatal(err) 338 + } 339 + defer iter.Close() 340 + 341 + if err := kg.PlacementIterator(iter); err != nil { 342 + t.Fatal(err) 343 + } 344 + 345 + // Set layer filter to ABOVE_TEXT (default z=0 should match). 346 + if err := iter.SetLayer(KittyPlacementLayerAboveText); err != nil { 347 + t.Fatal(err) 348 + } 349 + 350 + // Should still find the placement (z=0 is above text). 351 + if !iter.Next() { 352 + t.Fatal("expected placement with ABOVE_TEXT layer filter") 353 + } 354 + } 355 + 356 + func TestKittyGraphicsPlacementViewportPos(t *testing.T) { 357 + term := newKittyTerminal(t) 358 + defer term.Close() 359 + 360 + sendKittyImage(t, term) 361 + 362 + kg, err := term.KittyGraphics() 363 + if err != nil { 364 + t.Fatal(err) 365 + } 366 + 367 + iter, err := NewKittyGraphicsPlacementIterator() 368 + if err != nil { 369 + t.Fatal(err) 370 + } 371 + defer iter.Close() 372 + 373 + if err := kg.PlacementIterator(iter); err != nil { 374 + t.Fatal(err) 375 + } 376 + 377 + if !iter.Next() { 378 + t.Fatal("expected at least one placement") 379 + } 380 + 381 + imageID, err := iter.ImageID() 382 + if err != nil { 383 + t.Fatal(err) 384 + } 385 + img := kg.Image(imageID) 386 + if img == nil { 387 + t.Fatal("expected image lookup to succeed") 388 + } 389 + 390 + // The image was just placed, so it should be visible in the viewport. 391 + col, row, err := iter.ViewportPos(img, term) 392 + if err != nil { 393 + t.Fatal(err) 394 + } 395 + // Position should be non-negative for a freshly placed image. 396 + if col < 0 || row < 0 { 397 + t.Fatalf("expected non-negative viewport position, got col=%d row=%d", col, row) 398 + } 399 + }
+102
sys.go
··· 18 18 uint8_t* message, 19 19 size_t message_len); 20 20 21 + // Forward declaration for the Go decode-PNG trampoline. 22 + // Uses compatible types (no const) to match what cgo generates for 23 + // the //export function. 24 + extern _Bool goSysDecodePngTrampoline( 25 + void* userdata, 26 + GhosttyAllocator* allocator, 27 + uint8_t* data, 28 + size_t data_len, 29 + GhosttySysImage* out); 30 + 21 31 // Helper to install the Go log trampoline via ghostty_sys_set. 22 32 // We need this because cgo cannot take the address of a Go-exported 23 33 // function directly as a C function pointer. ··· 34 44 static inline GhosttyResult sys_clear_log(void) { 35 45 return ghostty_sys_set(GHOSTTY_SYS_OPT_LOG, NULL); 36 46 } 47 + 48 + // Helper to install the Go decode-PNG trampoline via ghostty_sys_set. 49 + static inline GhosttyResult sys_set_decode_png_go(void) { 50 + return ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, (const void*)goSysDecodePngTrampoline); 51 + } 52 + 53 + // Helper to clear the decode-PNG callback. 54 + static inline GhosttyResult sys_clear_decode_png(void) { 55 + return ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, NULL); 56 + } 37 57 */ 38 58 import "C" 39 59 40 60 import "unsafe" 41 61 62 + // SysImage holds the result of decoding an image (e.g. PNG) into raw 63 + // RGBA pixel data. Returned by the user-supplied decode callback. 64 + // C: GhosttySysImage 65 + type SysImage struct { 66 + // Width of the decoded image in pixels. 67 + Width uint32 68 + 69 + // Height of the decoded image in pixels. 70 + Height uint32 71 + 72 + // Data is the decoded RGBA pixel data (4 bytes per pixel). 73 + Data []byte 74 + } 75 + 76 + // SysDecodePngFn is the Go callback type for PNG decoding. It receives 77 + // raw PNG data and must return a decoded SysImage. The returned pixel 78 + // data will be copied into library-managed memory; the caller does not 79 + // need to keep the slice alive after returning. 80 + // 81 + // Return a non-nil error to indicate decode failure. 82 + // C: GhosttySysDecodePngFn 83 + type SysDecodePngFn func(data []byte) (*SysImage, error) 84 + 85 + // sysDecodePngFn is the currently installed Go decode-PNG callback. 86 + var sysDecodePngFn SysDecodePngFn 87 + 88 + // SysSetDecodePng installs a Go callback that decodes PNG image data 89 + // into RGBA pixels. This enables PNG support in the Kitty Graphics 90 + // Protocol. Pass nil to clear the callback and disable PNG decoding. 91 + // 92 + // This function is not safe for concurrent use. Callers must ensure 93 + // that decode configuration is not modified while terminals may 94 + // process image data (e.g. configure at startup before creating 95 + // terminals). 96 + func SysSetDecodePng(fn SysDecodePngFn) error { 97 + sysDecodePngFn = fn 98 + if fn == nil { 99 + return resultError(C.sys_clear_decode_png()) 100 + } 101 + return resultError(C.sys_set_decode_png_go()) 102 + } 103 + 42 104 // SysLogLevel represents the severity level of a log message from the 43 105 // library. Maps directly to the C enum values. 44 106 // C: GhosttySysLogLevel ··· 138 200 139 201 fn(SysLogLevel(level), scope, message) 140 202 } 203 + 204 + //export goSysDecodePngTrampoline 205 + func goSysDecodePngTrampoline( 206 + _ unsafe.Pointer, 207 + allocator *C.GhosttyAllocator, 208 + dataPtr *C.uint8_t, 209 + dataLen C.size_t, 210 + out *C.GhosttySysImage, 211 + ) C.bool { 212 + fn := sysDecodePngFn 213 + if fn == nil { 214 + return false 215 + } 216 + 217 + // Build a Go slice over the input PNG data without copying. 218 + data := unsafe.Slice((*byte)(unsafe.Pointer(dataPtr)), int(dataLen)) 219 + 220 + img, err := fn(data) 221 + if err != nil || img == nil { 222 + return false 223 + } 224 + 225 + // Allocate output pixel buffer through the library's allocator so 226 + // the library can free it later. 227 + pixelLen := C.size_t(len(img.Data)) 228 + buf := C.ghostty_alloc(allocator, pixelLen) 229 + if buf == nil { 230 + return false 231 + } 232 + 233 + // Copy decoded pixels into the library-owned buffer. 234 + copy(unsafe.Slice((*byte)(unsafe.Pointer(buf)), int(pixelLen)), img.Data) 235 + 236 + out.width = C.uint32_t(img.Width) 237 + out.height = C.uint32_t(img.Height) 238 + out.data = buf 239 + out.data_len = pixelLen 240 + 241 + return true 242 + }
+55
sys_builtin.go
··· 1 + package libghostty 2 + 3 + // Built-in implementations for system callbacks using Go standard library 4 + // packages. These are optional convenience functions that can be passed 5 + // directly to their corresponding SysSet* installers. 6 + 7 + import ( 8 + "bytes" 9 + "fmt" 10 + "image" 11 + "image/png" 12 + ) 13 + 14 + // SysDecodePng is a ready-to-use [SysDecodePngFn] implementation that 15 + // decodes PNG data using Go's standard [image/png] package. It converts 16 + // any decoded image format to NRGBA (non-premultiplied alpha) before 17 + // returning the raw pixel bytes. 18 + // 19 + // Usage: 20 + // 21 + // libghostty.SysSetDecodePng(libghostty.SysDecodePng) 22 + func SysDecodePng(data []byte) (*SysImage, error) { 23 + img, err := png.Decode(bytes.NewReader(data)) 24 + if err != nil { 25 + return nil, fmt.Errorf("png decode: %w", err) 26 + } 27 + 28 + bounds := img.Bounds() 29 + w := bounds.Dx() 30 + h := bounds.Dy() 31 + 32 + // Fast path: if the image is already NRGBA we can use the pixels 33 + // directly without a per-pixel conversion. 34 + if nrgba, ok := img.(*image.NRGBA); ok { 35 + return &SysImage{ 36 + Width: uint32(w), 37 + Height: uint32(h), 38 + Data: nrgba.Pix, 39 + }, nil 40 + } 41 + 42 + // Slow path: convert arbitrary image types to NRGBA. 43 + dst := image.NewNRGBA(bounds) 44 + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 45 + for x := bounds.Min.X; x < bounds.Max.X; x++ { 46 + dst.Set(x, y, img.At(x, y)) 47 + } 48 + } 49 + 50 + return &SysImage{ 51 + Width: uint32(w), 52 + Height: uint32(h), 53 + Data: dst.Pix, 54 + }, nil 55 + }
+76
sys_builtin_test.go
··· 1 + package libghostty 2 + 3 + import ( 4 + "bytes" 5 + "image" 6 + "image/color" 7 + "image/png" 8 + "testing" 9 + ) 10 + 11 + func TestSysDecodePng(t *testing.T) { 12 + // Encode a small 2x2 NRGBA PNG in-memory. 13 + src := image.NewNRGBA(image.Rect(0, 0, 2, 2)) 14 + src.SetNRGBA(0, 0, color.NRGBA{R: 255, A: 255}) 15 + src.SetNRGBA(1, 0, color.NRGBA{G: 255, A: 255}) 16 + src.SetNRGBA(0, 1, color.NRGBA{B: 255, A: 255}) 17 + src.SetNRGBA(1, 1, color.NRGBA{R: 255, G: 255, B: 255, A: 255}) 18 + 19 + var buf bytes.Buffer 20 + if err := png.Encode(&buf, src); err != nil { 21 + t.Fatalf("png.Encode: %v", err) 22 + } 23 + 24 + img, err := SysDecodePng(buf.Bytes()) 25 + if err != nil { 26 + t.Fatalf("SysDecodePng: %v", err) 27 + } 28 + 29 + if img.Width != 2 || img.Height != 2 { 30 + t.Fatalf("dimensions = %dx%d, want 2x2", img.Width, img.Height) 31 + } 32 + 33 + // 2x2 NRGBA = 16 bytes (4 bytes per pixel). 34 + if len(img.Data) != 16 { 35 + t.Fatalf("len(Data) = %d, want 16", len(img.Data)) 36 + } 37 + 38 + // Spot-check top-left pixel: red, fully opaque. 39 + if img.Data[0] != 255 || img.Data[1] != 0 || img.Data[2] != 0 || img.Data[3] != 255 { 40 + t.Errorf("pixel(0,0) = %v, want [255 0 0 255]", img.Data[0:4]) 41 + } 42 + } 43 + 44 + func TestSysDecodePngInvalid(t *testing.T) { 45 + _, err := SysDecodePng([]byte("not a png")) 46 + if err == nil { 47 + t.Fatal("SysDecodePng(invalid) = nil error, want error") 48 + } 49 + } 50 + 51 + func TestSysDecodePngRGBA(t *testing.T) { 52 + // Use an RGBA image (premultiplied alpha) to exercise the slow path 53 + // conversion to NRGBA. 54 + src := image.NewRGBA(image.Rect(0, 0, 1, 1)) 55 + src.SetRGBA(0, 0, color.RGBA{R: 128, G: 0, B: 0, A: 128}) 56 + 57 + var buf bytes.Buffer 58 + if err := png.Encode(&buf, src); err != nil { 59 + t.Fatalf("png.Encode: %v", err) 60 + } 61 + 62 + img, err := SysDecodePng(buf.Bytes()) 63 + if err != nil { 64 + t.Fatalf("SysDecodePng: %v", err) 65 + } 66 + 67 + if img.Width != 1 || img.Height != 1 { 68 + t.Fatalf("dimensions = %dx%d, want 1x1", img.Width, img.Height) 69 + } 70 + 71 + // The premultiplied (128,0,0,128) should convert to non-premultiplied 72 + // (255,0,0,128). 73 + if img.Data[0] != 255 || img.Data[1] != 0 || img.Data[2] != 0 || img.Data[3] != 128 { 74 + t.Errorf("pixel(0,0) = %v, want [255 0 0 128]", img.Data[0:4]) 75 + } 76 + }
+21
sys_test.go
··· 56 56 t.Fatalf("SysSetLog(nil) = %v, want nil", err) 57 57 } 58 58 } 59 + 60 + func TestSysSetDecodePngNil(t *testing.T) { 61 + // Clearing with nil should always succeed. 62 + if err := SysSetDecodePng(nil); err != nil { 63 + t.Fatalf("SysSetDecodePng(nil) = %v, want nil", err) 64 + } 65 + } 66 + 67 + func TestSysSetDecodePngCallback(t *testing.T) { 68 + // Install a Go decode-PNG callback and verify it can be set and cleared. 69 + if err := SysSetDecodePng(func(data []byte) (*SysImage, error) { 70 + return &SysImage{Width: 1, Height: 1, Data: []byte{0, 0, 0, 255}}, nil 71 + }); err != nil { 72 + t.Fatalf("SysSetDecodePng(fn) = %v, want nil", err) 73 + } 74 + 75 + // Clean up. 76 + if err := SysSetDecodePng(nil); err != nil { 77 + t.Fatalf("SysSetDecodePng(nil) = %v, want nil", err) 78 + } 79 + }
+16
terminal.go
··· 398 398 KittyKeyAll KittyKeyFlags = C.GHOSTTY_KITTY_KEY_ALL 399 399 ) 400 400 401 + // KittyGraphics returns the Kitty graphics image storage for the 402 + // terminal's active screen. The returned handle is borrowed from 403 + // the terminal and remains valid until the next mutating call 404 + // (e.g. VTWrite or Reset). 405 + func (t *Terminal) KittyGraphics() (*KittyGraphics, error) { 406 + var ptr C.GhosttyKittyGraphics 407 + if err := resultError(C.ghostty_terminal_get( 408 + t.ptr, 409 + C.GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS, 410 + unsafe.Pointer(&ptr), 411 + )); err != nil { 412 + return nil, err 413 + } 414 + return &KittyGraphics{ptr: ptr}, nil 415 + } 416 + 401 417 // GridRef resolves a point in the terminal grid to a grid reference. 402 418 // The returned GridRef is only valid until the next terminal update. 403 419 //
+50
terminal_opt.go
··· 143 143 )) 144 144 } 145 145 146 + // SetKittyImageStorageLimit sets the Kitty image storage limit in bytes. 147 + // Applied to all initialized screens (primary and alternate). A value of 148 + // zero disables the Kitty graphics protocol entirely, deleting all stored 149 + // images and placements. Pass nil to disable (equivalent to zero). 150 + func (t *Terminal) SetKittyImageStorageLimit(limit *uint64) error { 151 + var val unsafe.Pointer 152 + if limit != nil { 153 + v := C.uint64_t(*limit) 154 + val = unsafe.Pointer(&v) 155 + } 156 + return resultError(C.ghostty_terminal_set( 157 + t.ptr, 158 + C.GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT, 159 + val, 160 + )) 161 + } 162 + 163 + // SetKittyImageMediumFile enables or disables Kitty image loading via the 164 + // file medium. 165 + func (t *Terminal) SetKittyImageMediumFile(enabled bool) error { 166 + v := C.bool(enabled) 167 + return resultError(C.ghostty_terminal_set( 168 + t.ptr, 169 + C.GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE, 170 + unsafe.Pointer(&v), 171 + )) 172 + } 173 + 174 + // SetKittyImageMediumTempFile enables or disables Kitty image loading via 175 + // the temporary file medium. 176 + func (t *Terminal) SetKittyImageMediumTempFile(enabled bool) error { 177 + v := C.bool(enabled) 178 + return resultError(C.ghostty_terminal_set( 179 + t.ptr, 180 + C.GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE, 181 + unsafe.Pointer(&v), 182 + )) 183 + } 184 + 185 + // SetKittyImageMediumSharedMem enables or disables Kitty image loading via 186 + // the shared memory medium. 187 + func (t *Terminal) SetKittyImageMediumSharedMem(enabled bool) error { 188 + v := C.bool(enabled) 189 + return resultError(C.ghostty_terminal_set( 190 + t.ptr, 191 + C.GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM, 192 + unsafe.Pointer(&v), 193 + )) 194 + } 195 + 146 196 // SetTitle sets the terminal title manually. An empty string clears it. 147 197 func (t *Terminal) SetTitle(title string) error { 148 198 s := C.GhosttyString{