Real-image posters: a new size is a new image id, never a changed placement

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 00:29:16 +02:00
co-authored by Claude Sonnet 5.5
parent 2ba47cb4ee
commit cf07aaf8fb
5 changed files with 82 additions and 112 deletions
+29 -47
View File
@@ -28,22 +28,29 @@ const placeholderRune = '\U0010EEEE'
const kittyChunk = 4096 // bytes of base64 per escape sequence, the protocol maximum
// kittyGraphics uploads images to the terminal and keeps track of them.
//
// Every footprint of a poster (the part of it that is shown and the number of
// cells it covers) is uploaded as its own image with its own id and placed once.
// An existing placement is never changed: terminals do not reliably replace the
// size of a placement that is already there (Ghostty kept showing the old, small
// one after the window was enlarged), whereas a new image id always starts clean.
type kittyGraphics struct {
write func([]byte) error // writes to the terminal, in step with the screen
nextID uint32
ids map[cropKey]uint32
latest map[*image.RGBA]cropKey // the crop of each image that is currently uploaded
placed map[uint32][2]int // columns and rows each image is currently placed with
ids map[footprint]uint32
latest map[*image.RGBA]footprint // the footprint of each poster that is currently uploaded
}
// cropKey identifies an uploaded image: a poster and the part of it that was sent.
type cropKey struct {
img *image.RGBA
rect image.Rectangle
// footprint identifies an uploaded image: a poster, the part of it that was
// sent, and the cells it covers.
type footprint struct {
img *image.RGBA
rect image.Rectangle
cols, rows int
}
func newKittyGraphics(write func([]byte) error) *kittyGraphics {
return &kittyGraphics{write: write, nextID: 1, ids: map[cropKey]uint32{}, latest: map[*image.RGBA]cropKey{}, placed: map[uint32][2]int{}}
return &kittyGraphics{write: write, nextID: 1, ids: map[footprint]uint32{}, latest: map[*image.RGBA]footprint{}}
}
// kittyAPC builds one graphics command: ESC _ G <keys> ; <payload> ESC \.
@@ -59,11 +66,12 @@ func kittyAPC(keys string, payload []byte) []byte {
return b.Bytes()
}
// imageID returns the terminal's id for the part rect of the image,
// uploading it on first use. When the same image is needed with another crop
// (after a resize) the earlier upload is removed.
func (k *kittyGraphics) imageID(img *image.RGBA, rect image.Rectangle) (uint32, error) {
key := cropKey{img, rect}
// show makes sure the part rect of the image is uploaded and placed over
// cols×rows cells, and returns the id the placeholder cells must carry. The same
// footprint is reused; a different one for the same poster replaces the old
// upload (which is removed from the terminal).
func (k *kittyGraphics) show(img *image.RGBA, rect image.Rectangle, cols, rows int) (uint32, error) {
key := footprint{img, rect, cols, rows}
if id, ok := k.ids[key]; ok {
return id, nil
}
@@ -91,11 +99,14 @@ func (k *kittyGraphics) imageID(img *image.RGBA, rect image.Rectangle) (uint32,
}
rest = rest[n:]
}
// U=1: a virtual placement, shown wherever placeholder cells refer to the id.
if err := k.write(kittyAPC(fmt.Sprintf("a=p,U=1,i=%d,c=%d,r=%d,q=2", id, cols, rows), nil)); err != nil {
return 0, err
}
k.nextID++
if old, ok := k.latest[img]; ok {
if oldID, ok := k.ids[old]; ok {
_ = k.write(kittyAPC(fmt.Sprintf("a=d,d=I,i=%d,q=2", oldID), nil))
delete(k.placed, oldID)
delete(k.ids, old)
}
}
@@ -104,43 +115,14 @@ func (k *kittyGraphics) imageID(img *image.RGBA, rect image.Rectangle) (uint32,
return id, nil
}
// place makes the image cover cols×rows cells for the placeholders. A new size
// replaces the previous placement.
func (k *kittyGraphics) place(id uint32, cols, rows int) error {
if p, ok := k.placed[id]; ok {
if p == [2]int{cols, rows} {
return nil
}
// Remove the old placement (the uploaded data stays) before making a new one.
if err := k.write(kittyAPC(fmt.Sprintf("a=d,d=i,i=%d,q=2", id), nil)); err != nil {
return err
}
}
// U=1: a virtual placement, shown wherever placeholder cells refer to it.
if err := k.write(kittyAPC(fmt.Sprintf("a=p,U=1,i=%d,c=%d,r=%d,q=2", id, cols, rows), nil)); err != nil {
return err
}
k.placed[id] = [2]int{cols, rows}
return nil
}
// invalidate drops every placement (the uploaded pictures stay) so the next
// draw places them again, e.g. after the terminal was resized.
func (k *kittyGraphics) invalidate() {
for id := range k.placed {
_ = k.write(kittyAPC(fmt.Sprintf("a=d,d=i,i=%d,q=2", id), nil))
}
k.placed = map[uint32][2]int{}
}
// deleteAll removes every image uploaded so far from the terminal.
// deleteAll removes every image uploaded so far from the terminal. The next
// draw uploads what it needs again, which is also how a resize is handled.
func (k *kittyGraphics) deleteAll() {
for _, id := range k.ids {
_ = k.write(kittyAPC(fmt.Sprintf("a=d,d=I,i=%d,q=2", id), nil))
}
k.ids = map[cropKey]uint32{}
k.latest = map[*image.RGBA]cropKey{}
k.placed = map[uint32][2]int{}
k.ids = map[footprint]uint32{}
k.latest = map[*image.RGBA]footprint{}
}
// placeholderCell returns what to draw in the cell at (row, col) of the image