1package chart23import (4 "bytes"5 "compress/zlib"6 "encoding/base64"7 "fmt"8 "image"9 "strings"1011 "github.com/charmbracelet/x/ansi"12 "github.com/charmbracelet/x/ansi/kitty"13)1415// Images reach the terminal through the kitty graphics protocol with16// Unicode placeholders: the image is sent once with a virtual placement,17// and the view draws placeholder characters where it should appear. The18// placeholders are ordinary text to Bubble Tea's renderer, so redraws never19// erase the image, and they survive tmux.2021// Transmit returns the sequences that send img as image id, with a virtual22// placement of cols by rows cells for placeholders to show. Sending an id23// again replaces the image. wrap, if not nil, wraps each chunk, e.g. for24// tmux passthrough.25func Transmit(id int, img *image.RGBA, cols, rows int, wrap func(string) string) string {26 b := img.Bounds()27 // Straight (not premultiplied) RGBA, as the protocol expects.28 raw := make([]byte, 0, b.Dx()*b.Dy()*4)29 for y := b.Min.Y; y < b.Max.Y; y++ {30 for x := b.Min.X; x < b.Max.X; x++ {31 i := img.PixOffset(x, y)32 r, g, bl, a := img.Pix[i], img.Pix[i+1], img.Pix[i+2], img.Pix[i+3]33 if a > 0 && a < 255 {34 un := func(c uint8) uint8 { return uint8(min(int(c)*255/int(a), 255)) }35 r, g, bl = un(r), un(g), un(bl)36 }37 raw = append(raw, r, g, bl, a)38 }39 }40 var z bytes.Buffer41 zw := zlib.NewWriter(&z)42 zw.Write(raw)43 zw.Close()44 payload := base64.StdEncoding.EncodeToString(z.Bytes())4546 if wrap == nil {47 wrap = func(s string) string { return s }48 }49 first := fmt.Sprintf("a=T,q=2,f=%d,o=z,s=%d,v=%d,i=%d,U=1,c=%d,r=%d", kitty.RGBA, b.Dx(), b.Dy(), id, cols, rows)50 var out strings.Builder51 for i := 0; i < len(payload) || i == 0; i += kitty.MaxChunkSize {52 chunk := payload[i:min(i+kitty.MaxChunkSize, len(payload))]53 more := "m=0"54 if i+kitty.MaxChunkSize < len(payload) {55 more = "m=1"56 }57 opts := "q=2," + more58 if i == 0 {59 opts = first + "," + more60 }61 out.WriteString(wrap(ansi.KittyGraphics([]byte(chunk), opts)))62 }63 return out.String()64}6566// Delete returns the sequence that frees image id and its placements.67func Delete(id int, wrap func(string) string) string {68 s := ansi.KittyGraphics(nil, fmt.Sprintf("a=d,d=I,i=%d,q=2", id))69 if wrap != nil {70 return wrap(s)71 }72 return s73}7475// Query is the probe for kitty graphics support: terminals that have it76// answer with an OK for image QueryID; others stay silent.77const QueryID = 317879// Query returns the probe.80func Query() string {81 return ansi.KittyGraphics([]byte("AAAA"), fmt.Sprintf("i=%d,s=1,v=1,a=q,t=d,f=24", QueryID))82}8384// Placeholder returns the text of the cell at row, col of an image's85// placement. The foreground color must name the image: the 256-color86// index of its id.87func Placeholder(row, col int) string {88 return string([]rune{kitty.Placeholder, kitty.Diacritic(row), kitty.Diacritic(col)})89}