5KIKDA72HM6JFIPKOWGLM2EO7D5PTSK7WEVYV3YZWGMG3M34PJXQC GQNMVJQBC6DRV5XGK3K5L7YWG2GJUXR7EQE3OHNW72XK6BFY3AHQC KNZ624PA7NXVLCB656OUDYTSMLZFAC6RVRYAIVD6IUISP4FFNGCAC IFVRAERTCCDICNTYTG3TX2WASB6RXQQEJWWXQMQZJSQDQ3HLE5OQC ZFMOUTHEMHYYEAGXRQ3L427FVZPNBC7CX6QATVXU2KPDJOVEH2EQC 3JA7HYRMHV57SIMGMGPDOMKQ3NBQS2SKOX3EKDHRBQRP7ZPZGFTQC BMC55JAM6W7ELGY7GL7TU7NERFNJHH4S4OKNDFMEEJI7F4ARF6TAC U6JEEU5O477ZOJ5UMRMOJSGPEJEU6Q7KMPKUSDF56CYVUJWL7QBQC TLLVARZXOP2M3B5VTLF4SYDGMIBPHABE6LJFG77IU53QTSYGTKWAC E56RSY75LHUPKXPOPPGFDHPSACMRWPHOAD2GREJN3OFPA6QFM64QC DBOROCRFD6A5SJBMFYFEJI5S5M77X4EFEK6KDQWA5QDMQJKIHRWQC EW7VBNMGWFBC73ZUDLB4LIK2HWFKA74ZUTUDG4J575ZQHEFHW4UQC 3XZAHT6PTML33YSBVF5OSL7KLCRARTHRYPT3N7GUYJKSZKWHBIDQC D4EL6RSTSZ3S3IDSETRNGLJHZKGZEE2V2OZIOKQK6LRLHQNS77JQC package utilsimport ("image""image/color""strings""testing")func TestWriteKittyImage_SmallImage(t *testing.T) {// 2x2 image produces small base64 payload — single chunk, no m= keyimg := image.NewGray(image.Rect(0, 0, 2, 2))img.SetGray(0, 0, color.Gray{Y: 128})var buf strings.Builderif err := WriteKittyImage(img, &buf); err != nil {t.Fatalf("WriteKittyImage: %v", err)}out := buf.String()if !strings.HasPrefix(out, "\x1b_Gf=100,a=T;") {t.Error("expected single-chunk header with f=100,a=T")}if strings.Contains(out, "m=") {t.Error("small image should not use chunked m= key")}if !strings.HasSuffix(out, "\x1b\\") {t.Error("expected escape sequence terminator")}}func TestWriteKittyImage_LargeImage_Chunked(t *testing.T) {}}var buf strings.Builderif err := WriteKittyImage(img, &buf); err != nil {t.Fatalf("WriteKittyImage: %v", err)}out := buf.String()// Should have multiple escape sequenceschunks := strings.Split(out, "\x1b\\")// Last element is empty after final terminatorchunks = chunks[:len(chunks)-1]if len(chunks) < 2 {t.Fatalf("expected multiple chunks, got %d", len(chunks))}// First chunk should have f=100,a=T,m=1if !strings.Contains(chunks[0], "f=100,a=T,m=1") {t.Errorf("first chunk missing f=100,a=T,m=1: %s", chunks[0][:min(80, len(chunks[0]))])}// Last chunk should have m=0last := chunks[len(chunks)-1]if !strings.Contains(last, "\x1b_Gm=0;") {t.Errorf("last chunk missing m=0: %s", last[:min(80, len(last))])}// Middle chunks should have m=1for i := 1; i < len(chunks)-1; i++ {if !strings.Contains(chunks[i], "\x1b_Gm=1;") {t.Errorf("middle chunk %d missing m=1", i)}}}func TestClearKittyImages(t *testing.T) {var buf strings.BuilderClearKittyImages(&buf)expected := "\x1b_Ga=d\x1b\\"if buf.String() != expected {t.Errorf("got %q, want %q", buf.String(), expected)}}// 128x128 random noise image is incompressible — produces >4096 bytes of base64 even with proper LZ77rng := rand.New(rand.NewSource(42))img := image.NewGray(image.Rect(0, 0, 128, 128))for y := range 128 {for x := range 128 {img.SetGray(x, y, color.Gray{Y: uint8(rng.Intn(256))})"math/rand"
package utilsimport ("image""image/color""io")// WriteKittyImage writes an image to the writer using the Kitty graphics protocol.func WriteKittyImage(img image.Image, w io.Writer) error {}// CreateGrayscaleImage creates an image.Image from a 2D uint8 array.// The array is organized as [rows][cols] where rows = frequency bins.func CreateGrayscaleImage(data [][]uint8) image.Image {if len(data) == 0 || len(data[0]) == 0 {return nil}height := len(data)width := len(data[0])img := image.NewGray(image.Rect(0, 0, width, height))for y := range height {for x := range width {img.SetGray(x, y, color.Gray{Y: data[y][x]})}}return img}// ResizeImage resizes an image using nearest-neighbor interpolation.// For higher quality, use golang.org/x/image/draw, but this keeps dependencies minimal.func ResizeImage(img image.Image, newWidth, newHeight int) image.Image {bounds := img.Bounds()srcWidth := bounds.Dx()srcHeight := bounds.Dy()// Detect if image is grayscale or RGB_, isGray := img.(*image.Gray)scaleX := float64(srcWidth) / float64(newWidth)scaleY := float64(srcHeight) / float64(newHeight)result := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))for y := range newHeight {for x := range newWidth {srcX := int(float64(x) * scaleX)srcY := int(float64(y) * scaleY)if srcX >= srcWidth {srcX = srcWidth - 1}if srcY >= srcHeight {srcY = srcHeight - 1}c := img.At(srcX+bounds.Min.X, srcY+bounds.Min.Y)r, g, b, _ := c.RGBA()result.SetRGBA(x, y, color.RGBA{R: uint8(r >> 8),G: uint8(g >> 8),B: uint8(b >> 8),A: 255,})}}return result}if isGray {result := image.NewGray(image.Rect(0, 0, newWidth, newHeight))for y := range newHeight {for x := range newWidth {srcX := int(float64(x) * scaleX)srcY := int(float64(y) * scaleY)if srcX >= srcWidth {srcX = srcWidth - 1}if srcY >= srcHeight {srcY = srcHeight - 1}c := img.At(srcX+bounds.Min.X, srcY+bounds.Min.Y)gray := color.GrayModel.Convert(c).(color.Gray)result.SetGray(x, y, gray)}}return result}}}return img}// CreateRGBImage creates an image.Image from a 2D RGBPixel array.// The array is organized as [rows][cols] where rows = frequency bins.func CreateRGBImage(data [][]RGBPixel) image.Image {if len(data) == 0 || len(data[0]) == 0 {return nil}height := len(data)width := len(data[0])img := image.NewRGBA(image.Rect(0, 0, width, height))for y := range height {for x := range width {img.SetRGBA(x, y, color.RGBA{R: data[y][x].R,G: data[y][x].G,B: data[y][x].B,A: 255,})return kitty.EncodeGraphics(w, img, &kitty.Options{Format: kitty.PNG,Action: kitty.TransmitAndPut,Transmission: kitty.Direct,Chunk: true,})// The image is encoded as PNG, base64'd, and sent via chunked Kitty escape sequences.// ClearKittyImages clears all previously displayed Kitty imagesfunc ClearKittyImages(w io.Writer) error {}_, err := io.WriteString(w, ansi.KittyGraphics(nil, "a=d"))return err// ClampImageSize clamps a dimension to [224, 448].func ClampImageSize(size int) int {return max(224, min(448, size))}// 448px suits Retina/HiDPI screens (224 logical pixels at 2x).const SpectrogramDisplaySize = 448// SpectrogramDisplaySize is the default pixel dimension for spectrogram images."github.com/charmbracelet/x/ansi""github.com/charmbracelet/x/ansi/kitty"
package utilsimport ("image""image/color""math/rand""strings""testing")func TestWriteKittyImage_SmallImage(t *testing.T) {// 2x2 image produces small base64 payload — single chunk, no m= keyimg := image.NewGray(image.Rect(0, 0, 2, 2))img.SetGray(0, 0, color.Gray{Y: 128})var buf strings.Builderif err := WriteKittyImage(img, &buf); err != nil {t.Fatalf("WriteKittyImage: %v", err)}out := buf.String()if !strings.HasPrefix(out, "\x1b_Gf=100,a=T;") {t.Error("expected single-chunk header with f=100,a=T")}if strings.Contains(out, "m=") {t.Error("small image should not use chunked m= key")}if !strings.HasSuffix(out, "\x1b\\") {t.Error("expected escape sequence terminator")}}func TestWriteKittyImage_LargeImage_Chunked(t *testing.T) {// 128x128 random noise image is incompressible — produces >4096 bytes of base64 even with proper LZ77rng := rand.New(rand.NewSource(42))img := image.NewGray(image.Rect(0, 0, 128, 128))for y := range 128 {for x := range 128 {img.SetGray(x, y, color.Gray{Y: uint8(rng.Intn(256))})}}var buf strings.Builderif err := WriteKittyImage(img, &buf); err != nil {t.Fatalf("WriteKittyImage: %v", err)}out := buf.String()// Should have multiple escape sequenceschunks := strings.Split(out, "\x1b\\")// Last element is empty after final terminatorchunks = chunks[:len(chunks)-1]if len(chunks) < 2 {t.Fatalf("expected multiple chunks, got %d", len(chunks))}// First chunk should have f=100,a=T,m=1if !strings.Contains(chunks[0], "f=100,a=T,m=1") {t.Errorf("first chunk missing f=100,a=T,m=1: %s", chunks[0][:min(80, len(chunks[0]))])}// Last chunk should have m=0last := chunks[len(chunks)-1]if !strings.Contains(last, "\x1b_Gm=0;") {t.Errorf("last chunk missing m=0: %s", last[:min(80, len(last))])}// Middle chunks should have m=1for i := 1; i < len(chunks)-1; i++ {if !strings.Contains(chunks[i], "\x1b_Gm=1;") {t.Errorf("middle chunk %d missing m=1", i)}}}func TestClearKittyImages(t *testing.T) {var buf strings.BuilderClearKittyImages(&buf)expected := "\x1b_Ga=d\x1b\\"if buf.String() != expected {t.Errorf("got %q, want %q", buf.String(), expected)}}func TestWriteSixelImage(t *testing.T) {img := image.NewGray(image.Rect(0, 0, 4, 6))for y := range 6 {for x := range 4 {img.SetGray(x, y, color.Gray{Y: uint8((x + y) * 40)})}}var buf strings.Builderif err := WriteSixelImage(img, &buf); err != nil {t.Fatalf("WriteSixelImage: %v", err)}out := buf.String()// Sixel DCS introducerif !strings.HasPrefix(out, "\x1bP") {t.Error("expected DCS prefix \\x1bP")}// String terminatorif !strings.HasSuffix(out, "\x1b\\") {t.Error("expected ST suffix \\x1b\\\\")}// Should contain 'q' after DCS parametersif !strings.Contains(out, "q") {t.Error("expected 'q' in DCS sequence")}}func TestClearImages_Kitty(t *testing.T) {var buf strings.BuilderClearImages(&buf, ProtocolKitty)if buf.String() != "\x1b_Ga=d\x1b\\" {t.Errorf("got %q, want kitty clear sequence", buf.String())}}func TestClearImages_Sixel(t *testing.T) {var buf strings.BuilderClearImages(&buf, ProtocolSixel)if buf.String() != "" {t.Errorf("expected no output for sixel clear, got %q", buf.String())}}func TestWriteImage_Kitty(t *testing.T) {img := image.NewGray(image.Rect(0, 0, 2, 2))var buf strings.Builderif err := WriteImage(img, &buf, ProtocolKitty); err != nil {t.Fatalf("WriteImage kitty: %v", err)}if !strings.HasPrefix(buf.String(), "\x1b_G") {t.Error("expected kitty escape prefix")}}func TestWriteImage_Sixel(t *testing.T) {img := image.NewGray(image.Rect(0, 0, 4, 6))var buf strings.Builderif err := WriteImage(img, &buf, ProtocolSixel); err != nil {t.Fatalf("WriteImage sixel: %v", err)}if !strings.HasPrefix(buf.String(), "\x1bP") {t.Error("expected sixel DCS prefix")}}
package utilsimport ("bytes""image""image/color""io""github.com/charmbracelet/x/ansi""github.com/charmbracelet/x/ansi/kitty""github.com/charmbracelet/x/ansi/sixel")// ImageProtocol selects the terminal graphics protocol.type ImageProtocol intconst (ProtocolKitty ImageProtocol = iotaProtocolSixel)// SpectrogramDisplaySize is the default pixel dimension for spectrogram images.// 448px suits Retina/HiDPI screens (224 logical pixels at 2x).const SpectrogramDisplaySize = 448// ClampImageSize clamps a dimension to [224, 448].func ClampImageSize(size int) int {return max(224, min(448, size))}// WriteImage writes an image using the specified terminal graphics protocol.func WriteImage(img image.Image, w io.Writer, protocol ImageProtocol) error {switch protocol {case ProtocolSixel:return WriteSixelImage(img, w)default:return WriteKittyImage(img, w)}}// ClearImages clears previously displayed images. No-op for sixel (inline text).func ClearImages(w io.Writer, protocol ImageProtocol) error {switch protocol {case ProtocolSixel:return nildefault:return ClearKittyImages(w)}}// ClearKittyImages clears all previously displayed Kitty imagesfunc ClearKittyImages(w io.Writer) error {_, err := io.WriteString(w, ansi.KittyGraphics(nil, "a=d"))return err}// WriteKittyImage writes an image to the writer using the Kitty graphics protocol.// The image is encoded as PNG, base64'd, and sent via chunked Kitty escape sequences.func WriteKittyImage(img image.Image, w io.Writer) error {return kitty.EncodeGraphics(w, img, &kitty.Options{Format: kitty.PNG,Action: kitty.TransmitAndPut,Transmission: kitty.Direct,Chunk: true,})}// WriteSixelImage writes an image using the Sixel graphics protocol.func WriteSixelImage(img image.Image, w io.Writer) error {var buf bytes.Bufferenc := &sixel.Encoder{}if err := enc.Encode(&buf, img); err != nil {return err}_, err := io.WriteString(w, ansi.SixelGraphics(0, 1, 0, buf.Bytes()))return err}// CreateGrayscaleImage creates an image.Image from a 2D uint8 array.// The array is organized as [rows][cols] where rows = frequency bins.func CreateGrayscaleImage(data [][]uint8) image.Image {if len(data) == 0 || len(data[0]) == 0 {return nil}height := len(data)width := len(data[0])img := image.NewGray(image.Rect(0, 0, width, height))for y := range height {for x := range width {img.SetGray(x, y, color.Gray{Y: data[y][x]})}}return img}// CreateRGBImage creates an image.Image from a 2D RGBPixel array.// The array is organized as [rows][cols] where rows = frequency bins.func CreateRGBImage(data [][]RGBPixel) image.Image {if len(data) == 0 || len(data[0]) == 0 {return nil}height := len(data)width := len(data[0])img := image.NewRGBA(image.Rect(0, 0, width, height))for y := range height {for x := range width {img.SetRGBA(x, y, color.RGBA{R: data[y][x].R,G: data[y][x].G,B: data[y][x].B,A: 255,})}}return img}// ResizeImage resizes an image using nearest-neighbor interpolation.// For higher quality, use golang.org/x/image/draw, but this keeps dependencies minimal.func ResizeImage(img image.Image, newWidth, newHeight int) image.Image {bounds := img.Bounds()srcWidth := bounds.Dx()srcHeight := bounds.Dy()// Detect if image is grayscale or RGB_, isGray := img.(*image.Gray)scaleX := float64(srcWidth) / float64(newWidth)scaleY := float64(srcHeight) / float64(newHeight)if isGray {result := image.NewGray(image.Rect(0, 0, newWidth, newHeight))for y := range newHeight {for x := range newWidth {srcX := int(float64(x) * scaleX)srcY := int(float64(y) * scaleY)if srcX >= srcWidth {srcX = srcWidth - 1}if srcY >= srcHeight {srcY = srcHeight - 1}c := img.At(srcX+bounds.Min.X, srcY+bounds.Min.Y)gray := color.GrayModel.Convert(c).(color.Gray)result.SetGray(x, y, gray)}}return result}result := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))for y := range newHeight {for x := range newWidth {srcX := int(float64(x) * scaleX)srcY := int(float64(y) * scaleY)if srcX >= srcWidth {srcX = srcWidth - 1}if srcY >= srcHeight {srcY = srcHeight - 1}c := img.At(srcX+bounds.Min.X, srcY+bounds.Min.Y)r, g, b, _ := c.RGBA()result.SetRGBA(x, y, color.RGBA{R: uint8(r >> 8),G: uint8(g >> 8),B: uint8(b >> 8),A: 255,})}}return result}
// Output via Kitty protocol (images cleared at start of View)utils.WriteKittyImage(resized, b)
// Output via terminal graphics protocol (images cleared at start of View)utils.WriteImage(resized, b, m.protocol())
// Write to stdout via Kitty protocolif err := utils.WriteKittyImage(resized, os.Stdout); err != nil {
// Write to stdout via terminal graphics protocolif err := utils.WriteImage(resized, os.Stdout, protocol); err != nil {