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:
+29
-47
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user