Go bindings for libghostty-vt.
0
fork

Configure Feed

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

sys: add decode PNG callback binding and built-in implementation

Bind the GHOSTTY_SYS_OPT_DECODE_PNG option from ghostty_sys_set(),
which lets embedders supply a PNG-to-RGBA decoder for Kitty Graphics
Protocol support.

SysSetDecodePng installs or clears a Go callback. The C trampoline
allocates the output pixel buffer through the library allocator via
ghostty_alloc so the library can own and free it. The Go callback
returns a SysImage struct; pixel data is copied into the
library-owned buffer before returning to C.

A ready-to-use SysDecodePng function is provided in sys_builtin.go
that uses Go standard library image/png. It returns NRGBA pixels
directly when the decoded image is already in that format and falls
back to a per-pixel conversion for other image types.

+262 -1
+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 + - [ ] 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()`
+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 + }