Go bindings for libghostty-vt.
0
fork

Configure Feed

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

add kitty graphics protocol bindings

Add complete Go bindings for kitty_graphics.h, covering the full
image storage and placement iteration API.

New types: KittyGraphics, KittyGraphicsImage, and
KittyGraphicsPlacementIterator along with enums for placement
layers, image formats, and compression. Terminal.KittyGraphics()
provides the entry point by querying the active screen storage.

Image handles expose ID, dimensions, format, compression, and
borrowed pixel data. The placement iterator supports z-layer
filtering and provides per-placement getters for image ID,
placement ID, offsets, source rect fields, grid size, and z-index.
Rendering helpers compute pixel size, grid size, viewport position,
source rect, and bounding selection.

Terminal option setters added for SetKittyImageStorageLimit and the
three medium toggles (file, temp file, shared memory).

Also introduces the Selection type wrapping GhosttySelection since
the placement rect API returns one.

+1002 -1
+1 -1
TODO.md
··· 8 8 - [ ] SGR parser (`sgr.h`) 9 9 - [ ] Paste utilities (`paste.h`) 10 10 - [ ] Focus encoding (`focus.h`) 11 - - [ ] Kitty graphics (`kitty_graphics.h`) 11 + - [x] Kitty graphics (`kitty_graphics.h`) 12 12 - [ ] Allocator (`allocator.h` — `ghostty_alloc`, `ghostty_free`) 13 13 - [ ] Selection type (`selection.h`) 14 14
+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 + }
+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{