Files
tmux/regress/image-kitty-layers.sh
2026-10-16 17:26:41 +01:00

136 lines
5.6 KiB
Bash

#!/bin/sh
# Image ordering and overlapping Kitty placements through SIXEL and fallback.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
command -v python3 >/dev/null || exit 101
python3 -B - "$TEST_TMUX" <<'PY'
import sys
from image_test import image_test, kitty, sixel
from image_terminal import Terminal
red, green, blue, white = (255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)
def upload(id, colour):
"""Upload a solid pixel under a Kitty image ID."""
return kitty('a=t,i=%d,f=32,s=1,v=1' % id, bytes((*colour, 255)))
def put(id, controls=''):
"""Place a stored image in two columns without moving the cursor."""
return kitty('a=p,i=%d,C=1,c=2,r=1' % id + controls)
def rectangle(colour, xoff=0, yoff=0, w=16, h=16):
"""Expected image pixels for a solid rectangle."""
return {(x + xoff, y + yoff): colour for y in range(h) for x in range(w)}
def check(test, client, terminal, expected):
"""Check incremental output and a full redraw of the same scene."""
terminal.feed(client.data)
terminal.check(expected)
test.redraw()
terminal = Terminal()
terminal.feed(client.data)
terminal.check(expected)
return terminal
with image_test(sys.argv[1]) as test:
test.run('set-option', '-g', 'scroll-on-clear', 'off')
client = test.attach()
fallback = test.attach(graphical=False)
terminal = Terminal()
# The PR's tie-breaker cases: upload order wins over placement and app IDs.
test.emit(upload(200, red) + upload(100, white) + put(100))
terminal = check(test, client, terminal, rectangle(white))
test.emit(put(200))
terminal = check(test, client, terminal, rectangle(white))
# Replacing the older source preserves its place in the drawing order.
test.emit(upload(200, green) + put(200))
terminal = check(test, client, terminal, rectangle(white))
test.emit(kitty('a=d,d=I,i=200') + upload(200, red) + put(200))
terminal = check(test, client, terminal, rectangle(red))
# A retransmit-and-place also inherits the old source's drawing order.
test.emit(b'\033[2J\033[H' + upload(200, red) + upload(100, white) +
put(200) + put(100) + kitty('a=T,i=100,f=32,s=1,v=1,C=1,c=2,r=1',
bytes((*green, 255))))
# 200 was recreated above, so it is newer than 100 despite retransmission.
terminal = check(test, client, terminal, rectangle(red))
# Placements of the same source use placement order, including after reflow.
pixels = bytes((*red, 255, *blue, 255))
test.emit(b'\033[2J\033[H' + kitty('a=t,i=300,f=32,s=2,v=1', pixels) +
put(300, ',p=99,x=1,w=1') + put(300, ',p=1,x=0,w=1'))
terminal = check(test, client, terminal, rectangle(red))
test.run('resize-window', '-x', '10')
test.redraw()
terminal = Terminal()
terminal.feed(client.data)
terminal.check(rectangle(red))
test.run('resize-window', '-x', '40')
# Replacing a named placement puts it after the other source placements.
test.emit(put(300, ',p=99,x=1,w=1'))
terminal = check(test, client, terminal, rectangle(blue))
test.emit(kitty('a=d,d=i,i=300,p=99'))
terminal = check(test, client, terminal, rectangle(red))
# Both protocols use source creation order, even when placed later.
graphics = sixel(16, 16, [(100, 100, 100)], [0] * 256)
test.emit(b'\033[2J\033[H' + upload(400, red) + graphics +
b'\033[H' + put(400))
terminal = check(test, client, Terminal(), rectangle(white))
# Replacing the older Kitty source leaves the newer SIXEL on top.
test.emit(upload(400, green) + put(400))
terminal = check(test, client, terminal, rectangle(white))
test.emit(b'\033[2J\033[H' + graphics + b'\033[H' +
upload(401, red) + put(401))
terminal = check(test, client, Terminal(), rectangle(red))
term = Terminal()
term.feed(fallback.data)
assert term.cells[0, 0][1:3] == (196, 196), term.cells[0, 0]
# Removing the newer Kitty placement reveals the retained SIXEL image.
test.emit(kitty('a=d,d=I,i=401'))
terminal = check(test, client, terminal, rectangle(white))
term = Terminal()
term.feed(fallback.data)
assert term.cells[0, 0][1:3] == (231, 231), term.cells[0, 0]
# Transparency preserves the lower image across redraws and deletion.
pixels = bytes(value for y in range(16) for x in range(16)
for value in (*blue, 255 if x < 8 else 0))
for base, colour in [(upload(500, red) + put(500), red), (graphics, white)]:
test.emit(b'\033[2J\033[H' + base + b'\033[H' +
kitty('a=T,i=501,f=32,s=16,v=16,C=1', pixels))
terminal = check(test, client, Terminal(),
rectangle(blue, w=8) | rectangle(colour, 8, w=8))
test.emit(kitty('a=d,d=I,i=501'))
terminal = check(test, client, terminal, rectangle(colour))
# Rejected layers cannot replace stored pixels or remove placements.
test.emit(b'\033[2J\033[H' + upload(600, red) + put(600))
terminal = check(test, client, Terminal(), rectangle(red))
for action in ('t', 'T', 'p', 'q', 'd'):
test.emit(kitty('a=%s,i=600,p=9,f=32,s=1,v=1,C=1,d=I,z=1' %
action, bytes((*green, 255))))
terminal = check(test, client, terminal, rectangle(red))
test.emit(kitty('a=p,i=600,p=9,C=1,c=2,r=1,z=0'))
terminal = check(test, client, terminal, rectangle(red))
# Images at the default layer cover text and explicit backgrounds.
test.emit(b'\033[2J\033[H\033[44mX \033[0m\033[H' + put(600))
check(test, client, Terminal(), rectangle(red))
term = Terminal()
term.feed(fallback.data)
assert term.cells[0, 0][1:3] == (196, 196), term.cells[0, 0]
PY