diff --git a/TODO.md b/TODO.md index 8bd7c23..3541402 100644 --- a/TODO.md +++ b/TODO.md @@ -4,11 +4,13 @@ - [ ] Key encoding (`key.h`, `key/encoder.h`, `key/event.h`) - [ ] Mouse encoding (`mouse.h`, `mouse/encoder.h`, `mouse/event.h`) -- [ ] Render state (`render.h`) - [ ] OSC parser (`osc.h`) - [ ] SGR parser (`sgr.h`) - [ ] Paste utilities (`paste.h`) - [ ] Focus encoding (`focus.h`) +- [ ] Kitty graphics (`kitty_graphics.h`) +- [ ] Allocator (`allocator.h` — `ghostty_alloc`, `ghostty_free`) +- [ ] Selection type (`selection.h`) ## Partially Bound @@ -16,3 +18,8 @@ - [ ] `ghostty_size_report_encode()` - [ ] `ghostty_type_json()` - [ ] `ghostty_style_default()` +- [ ] `ghostty_color_rgb_get()` +- [ ] `ghostty_grid_ref_hyperlink_uri()` +- [ ] `ghostty_terminal_point_from_grid_ref()` +- [ ] `ghostty_formatter_format_buf()` +- [ ] `ghostty_focus_encode()` diff --git a/sys.go b/sys.go index 32736ad..28a4492 100644 --- a/sys.go +++ b/sys.go @@ -18,6 +18,16 @@ extern void goSysLogTrampoline( uint8_t* message, size_t message_len); +// Forward declaration for the Go decode-PNG trampoline. +// Uses compatible types (no const) to match what cgo generates for +// the //export function. +extern _Bool goSysDecodePngTrampoline( + void* userdata, + GhosttyAllocator* allocator, + uint8_t* data, + size_t data_len, + GhosttySysImage* out); + // Helper to install the Go log trampoline via ghostty_sys_set. // We need this because cgo cannot take the address of a Go-exported // function directly as a C function pointer. @@ -34,11 +44,63 @@ static inline GhosttyResult sys_set_log_stderr(void) { static inline GhosttyResult sys_clear_log(void) { return ghostty_sys_set(GHOSTTY_SYS_OPT_LOG, NULL); } + +// Helper to install the Go decode-PNG trampoline via ghostty_sys_set. +static inline GhosttyResult sys_set_decode_png_go(void) { + return ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, (const void*)goSysDecodePngTrampoline); +} + +// Helper to clear the decode-PNG callback. +static inline GhosttyResult sys_clear_decode_png(void) { + return ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, NULL); +} */ import "C" import "unsafe" +// SysImage holds the result of decoding an image (e.g. PNG) into raw +// RGBA pixel data. Returned by the user-supplied decode callback. +// C: GhosttySysImage +type SysImage struct { + // Width of the decoded image in pixels. + Width uint32 + + // Height of the decoded image in pixels. + Height uint32 + + // Data is the decoded RGBA pixel data (4 bytes per pixel). + Data []byte +} + +// SysDecodePngFn is the Go callback type for PNG decoding. It receives +// raw PNG data and must return a decoded SysImage. The returned pixel +// data will be copied into library-managed memory; the caller does not +// need to keep the slice alive after returning. +// +// Return a non-nil error to indicate decode failure. +// C: GhosttySysDecodePngFn +type SysDecodePngFn func(data []byte) (*SysImage, error) + +// sysDecodePngFn is the currently installed Go decode-PNG callback. +var sysDecodePngFn SysDecodePngFn + +// SysSetDecodePng installs a Go callback that decodes PNG image data +// into RGBA pixels. This enables PNG support in the Kitty Graphics +// Protocol. Pass nil to clear the callback and disable PNG decoding. +// +// This function is not safe for concurrent use. Callers must ensure +// that decode configuration is not modified while terminals may +// process image data (e.g. configure at startup before creating +// terminals). +func SysSetDecodePng(fn SysDecodePngFn) error { + sysDecodePngFn = fn + if fn == nil { + return resultError(C.sys_clear_decode_png()) + } + return resultError(C.sys_set_decode_png_go()) +} + // SysLogLevel represents the severity level of a log message from the // library. Maps directly to the C enum values. // C: GhosttySysLogLevel @@ -138,3 +200,43 @@ func goSysLogTrampoline( fn(SysLogLevel(level), scope, message) } + +//export goSysDecodePngTrampoline +func goSysDecodePngTrampoline( + _ unsafe.Pointer, + allocator *C.GhosttyAllocator, + dataPtr *C.uint8_t, + dataLen C.size_t, + out *C.GhosttySysImage, +) C.bool { + fn := sysDecodePngFn + if fn == nil { + return false + } + + // Build a Go slice over the input PNG data without copying. + data := unsafe.Slice((*byte)(unsafe.Pointer(dataPtr)), int(dataLen)) + + img, err := fn(data) + if err != nil || img == nil { + return false + } + + // Allocate output pixel buffer through the library's allocator so + // the library can free it later. + pixelLen := C.size_t(len(img.Data)) + buf := C.ghostty_alloc(allocator, pixelLen) + if buf == nil { + return false + } + + // Copy decoded pixels into the library-owned buffer. + copy(unsafe.Slice((*byte)(unsafe.Pointer(buf)), int(pixelLen)), img.Data) + + out.width = C.uint32_t(img.Width) + out.height = C.uint32_t(img.Height) + out.data = buf + out.data_len = pixelLen + + return true +} diff --git a/sys_builtin.go b/sys_builtin.go new file mode 100644 index 0000000..d8446ba --- /dev/null +++ b/sys_builtin.go @@ -0,0 +1,55 @@ +package libghostty + +// Built-in implementations for system callbacks using Go standard library +// packages. These are optional convenience functions that can be passed +// directly to their corresponding SysSet* installers. + +import ( + "bytes" + "fmt" + "image" + "image/png" +) + +// SysDecodePng is a ready-to-use [SysDecodePngFn] implementation that +// decodes PNG data using Go's standard [image/png] package. It converts +// any decoded image format to NRGBA (non-premultiplied alpha) before +// returning the raw pixel bytes. +// +// Usage: +// +// libghostty.SysSetDecodePng(libghostty.SysDecodePng) +func SysDecodePng(data []byte) (*SysImage, error) { + img, err := png.Decode(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("png decode: %w", err) + } + + bounds := img.Bounds() + w := bounds.Dx() + h := bounds.Dy() + + // Fast path: if the image is already NRGBA we can use the pixels + // directly without a per-pixel conversion. + if nrgba, ok := img.(*image.NRGBA); ok { + return &SysImage{ + Width: uint32(w), + Height: uint32(h), + Data: nrgba.Pix, + }, nil + } + + // Slow path: convert arbitrary image types to NRGBA. + dst := image.NewNRGBA(bounds) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + dst.Set(x, y, img.At(x, y)) + } + } + + return &SysImage{ + Width: uint32(w), + Height: uint32(h), + Data: dst.Pix, + }, nil +} diff --git a/sys_builtin_test.go b/sys_builtin_test.go new file mode 100644 index 0000000..11ec33a --- /dev/null +++ b/sys_builtin_test.go @@ -0,0 +1,76 @@ +package libghostty + +import ( + "bytes" + "image" + "image/color" + "image/png" + "testing" +) + +func TestSysDecodePng(t *testing.T) { + // Encode a small 2x2 NRGBA PNG in-memory. + src := image.NewNRGBA(image.Rect(0, 0, 2, 2)) + src.SetNRGBA(0, 0, color.NRGBA{R: 255, A: 255}) + src.SetNRGBA(1, 0, color.NRGBA{G: 255, A: 255}) + src.SetNRGBA(0, 1, color.NRGBA{B: 255, A: 255}) + src.SetNRGBA(1, 1, color.NRGBA{R: 255, G: 255, B: 255, A: 255}) + + var buf bytes.Buffer + if err := png.Encode(&buf, src); err != nil { + t.Fatalf("png.Encode: %v", err) + } + + img, err := SysDecodePng(buf.Bytes()) + if err != nil { + t.Fatalf("SysDecodePng: %v", err) + } + + if img.Width != 2 || img.Height != 2 { + t.Fatalf("dimensions = %dx%d, want 2x2", img.Width, img.Height) + } + + // 2x2 NRGBA = 16 bytes (4 bytes per pixel). + if len(img.Data) != 16 { + t.Fatalf("len(Data) = %d, want 16", len(img.Data)) + } + + // Spot-check top-left pixel: red, fully opaque. + if img.Data[0] != 255 || img.Data[1] != 0 || img.Data[2] != 0 || img.Data[3] != 255 { + t.Errorf("pixel(0,0) = %v, want [255 0 0 255]", img.Data[0:4]) + } +} + +func TestSysDecodePngInvalid(t *testing.T) { + _, err := SysDecodePng([]byte("not a png")) + if err == nil { + t.Fatal("SysDecodePng(invalid) = nil error, want error") + } +} + +func TestSysDecodePngRGBA(t *testing.T) { + // Use an RGBA image (premultiplied alpha) to exercise the slow path + // conversion to NRGBA. + src := image.NewRGBA(image.Rect(0, 0, 1, 1)) + src.SetRGBA(0, 0, color.RGBA{R: 128, G: 0, B: 0, A: 128}) + + var buf bytes.Buffer + if err := png.Encode(&buf, src); err != nil { + t.Fatalf("png.Encode: %v", err) + } + + img, err := SysDecodePng(buf.Bytes()) + if err != nil { + t.Fatalf("SysDecodePng: %v", err) + } + + if img.Width != 1 || img.Height != 1 { + t.Fatalf("dimensions = %dx%d, want 1x1", img.Width, img.Height) + } + + // The premultiplied (128,0,0,128) should convert to non-premultiplied + // (255,0,0,128). + if img.Data[0] != 255 || img.Data[1] != 0 || img.Data[2] != 0 || img.Data[3] != 128 { + t.Errorf("pixel(0,0) = %v, want [255 0 0 128]", img.Data[0:4]) + } +} diff --git a/sys_test.go b/sys_test.go index 9f59463..96b71b1 100644 --- a/sys_test.go +++ b/sys_test.go @@ -56,3 +56,24 @@ func TestSysSetLogCallback(t *testing.T) { t.Fatalf("SysSetLog(nil) = %v, want nil", err) } } + +func TestSysSetDecodePngNil(t *testing.T) { + // Clearing with nil should always succeed. + if err := SysSetDecodePng(nil); err != nil { + t.Fatalf("SysSetDecodePng(nil) = %v, want nil", err) + } +} + +func TestSysSetDecodePngCallback(t *testing.T) { + // Install a Go decode-PNG callback and verify it can be set and cleared. + if err := SysSetDecodePng(func(data []byte) (*SysImage, error) { + return &SysImage{Width: 1, Height: 1, Data: []byte{0, 0, 0, 255}}, nil + }); err != nil { + t.Fatalf("SysSetDecodePng(fn) = %v, want nil", err) + } + + // Clean up. + if err := SysSetDecodePng(nil); err != nil { + t.Fatalf("SysSetDecodePng(nil) = %v, want nil", err) + } +}