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
+48 -57
View File
@@ -51,16 +51,21 @@ func TestKittyUploadChunksAndEncodes(t *testing.T) {
k := newKittyGraphics(rec.write)
img := noisy(200, 300) // incompressible: far more than one 4096-byte chunk of base64
id, err := k.imageID(img, img.Bounds())
id, err := k.show(img, img.Bounds(), 20, 15)
if err != nil || id != 1 {
t.Fatalf("id=%d err=%v", id, err)
}
if len(rec.cmds) < 3 {
if len(rec.cmds) < 4 {
t.Fatalf("only %d commands for a large image", len(rec.cmds))
}
// The upload is followed by the placement, once.
if last := rec.cmds[len(rec.cmds)-1]; last != "\x1b_Ga=p,U=1,i=1,c=20,r=15,q=2\x1b\\" {
t.Errorf("last command = %q", last)
}
uploads := rec.cmds[:len(rec.cmds)-1]
re := regexp.MustCompile(`^\x1b_G([^;]*);(.*)\x1b\\$`)
var payload strings.Builder
for i, c := range rec.cmds {
for i, c := range uploads {
m := re.FindStringSubmatch(c)
if m == nil {
t.Fatalf("command %d is not a graphics command: %q", i, c[:min(40, len(c))])
@@ -69,7 +74,7 @@ func TestKittyUploadChunksAndEncodes(t *testing.T) {
if len(data) > kittyChunk {
t.Errorf("chunk %d has %d bytes", i, len(data))
}
last := i == len(rec.cmds)-1
last := i == len(uploads)-1
switch {
case i == 0 && !strings.HasPrefix(keys, "a=t,f=100,t=d,i=1,q=2,m=1"):
t.Errorf("first command keys = %q", keys)
@@ -94,43 +99,51 @@ func TestKittyUploadChunksAndEncodes(t *testing.T) {
t.Error("pixel data changed")
}
// The same image is not uploaded twice; another one gets the next id.
// The same footprint is not uploaded twice; another poster gets the next id.
n := len(rec.cmds)
if again, _ := k.imageID(img, img.Bounds()); again != id || len(rec.cmds) != n {
t.Error("an uploaded image must be reused")
if again, _ := k.show(img, img.Bounds(), 20, 15); again != id || len(rec.cmds) != n {
t.Error("an uploaded footprint must be reused")
}
if other, _ := k.imageID(noisy(4, 4), image.Rect(0, 0, 4, 4)); other != 2 {
small := noisy(4, 4)
if other, _ := k.show(small, small.Bounds(), 3, 2); other != 2 {
t.Errorf("second image id = %d", other)
}
}
func TestKittyPlacement(t *testing.T) {
func TestKittyNewSizeIsANewImage(t *testing.T) {
rec := &recorder{}
k := newKittyGraphics(rec.write)
if err := k.place(7, 20, 15); err != nil {
t.Fatal(err)
}
if len(rec.cmds) != 1 || rec.cmds[0] != "\x1b_Ga=p,U=1,i=7,c=20,r=15,q=2\x1b\\" {
t.Fatalf("first placement: %q", rec.cmds)
}
k.place(7, 20, 15) // unchanged: nothing to send
if len(rec.cmds) != 1 {
t.Errorf("unchanged placement sent %d commands", len(rec.cmds))
}
k.place(7, 10, 8) // a new size replaces the placement
if len(rec.cmds) != 3 || rec.cmds[1] != "\x1b_Ga=d,d=i,i=7,q=2\x1b\\" || rec.cmds[2] != "\x1b_Ga=p,U=1,i=7,c=10,r=8,q=2\x1b\\" {
t.Errorf("resize: %q", rec.cmds[1:])
img := noisy(10, 15)
id1, _ := k.show(img, img.Bounds(), 20, 15)
rec.cmds = nil
if again, _ := k.show(img, img.Bounds(), 20, 15); again != id1 || len(rec.cmds) != 0 {
t.Fatalf("same footprint: id %d, %d commands", again, len(rec.cmds))
}
rec.cmds = nil
small := noisy(2, 2)
id, _ := k.imageID(small, small.Bounds())
k.deleteAll()
if last := rec.cmds[len(rec.cmds)-1]; last != "\x1b_Ga=d,d=I,i="+itoa(int(id))+",q=2\x1b\\" {
t.Errorf("deleteAll sent %q", last)
// A different size is never done by changing the placement (a terminal can keep the
// old one): the poster is uploaded under a new id, placed, and the old id removed.
id2, _ := k.show(img, img.Bounds(), 10, 8)
if id2 == id1 {
t.Fatal("a new footprint must get a new id")
}
if len(k.ids) != 0 || len(k.placed) != 0 {
t.Error("deleteAll must forget everything")
all := strings.Join(rec.cmds, "")
if !strings.Contains(all, "a=t,f=100,t=d,i=2") || !strings.Contains(all, "a=p,U=1,i=2,c=10,r=8") || !strings.Contains(all, "a=d,d=I,i=1") {
t.Errorf("resize sent %q", rec.cmds)
}
if strings.Contains(all, "a=p,U=1,i=1") {
t.Error("the old placement must not be touched")
}
// After deleteAll (what a window resize triggers) everything is uploaded afresh.
rec.cmds = nil
k.deleteAll()
id3, _ := k.show(img, img.Bounds(), 10, 8)
if id3 == id2 || !strings.Contains(strings.Join(rec.cmds, ""), "a=d,d=I,i=2") || !strings.Contains(strings.Join(rec.cmds, ""), "a=t,f=100,t=d,i=3") {
t.Errorf("after deleteAll: id %d, %q", id3, rec.cmds)
}
if len(k.ids) != 1 || len(k.latest) != 1 {
t.Errorf("bookkeeping: %d ids, %d latest", len(k.ids), len(k.latest))
}
}
@@ -243,15 +256,11 @@ func TestCoverViewGraphicsDrawsPlaceholders(t *testing.T) {
// A cell aspect of 2.5 (taller cells) changes the footprint of the same box.
c.cellAspect = func() float64 { return 2.5 }
c.Draw(screen)
// The box is now 17×10 cells, a different shape: a new crop is uploaded, the
// old upload is removed, and the image is placed afresh.
var placements []string
for _, cmd := range rec.cmds[2:] {
placements = append(placements, cmd[:min(len(cmd), 30)])
}
last := rec.cmds[len(rec.cmds)-1]
if !strings.Contains(last, "c=17,r=10") || !strings.Contains(strings.Join(rec.cmds[2:], ""), "a=d,d=I,i=1") {
t.Errorf("after the cell aspect changed: %q", placements)
// The box is now 17×10 cells, a different shape: a new image id is uploaded and
// placed, the old upload is removed.
all := strings.Join(rec.cmds[2:], "")
if !strings.Contains(all, "a=p,U=1,i=2,c=17,r=10") || !strings.Contains(all, "a=d,d=I,i=1") {
t.Errorf("after the cell aspect changed: %q", rec.cmds[2:])
}
}
@@ -280,24 +289,6 @@ func TestCropToBox(t *testing.T) {
}
}
func TestInvalidateReplacesPlacements(t *testing.T) {
rec := &recorder{}
k := newKittyGraphics(rec.write)
k.place(3, 17, 13)
k.place(4, 10, 8)
rec.cmds = nil
k.invalidate()
if len(rec.cmds) != 2 || !strings.Contains(strings.Join(rec.cmds, ""), "a=d,d=i,i=3") || !strings.Contains(strings.Join(rec.cmds, ""), "a=d,d=i,i=4") {
t.Fatalf("invalidate sent %q", rec.cmds)
}
// The same size is placed again afterwards, since the placement is gone.
rec.cmds = nil
k.place(3, 17, 13)
if len(rec.cmds) != 1 || !strings.Contains(rec.cmds[0], "a=p,U=1,i=3,c=17,r=13") {
t.Errorf("after invalidate: %q", rec.cmds)
}
}
func TestCellAspectFromWindow(t *testing.T) {
// 140×50 cells of 9×20 px plus 4 and 11 px of padding: plain division
// would say 1264/140=9.03 and 1011/50=20.22 (aspect 2.24), the real shape is 20/9.