mirror of
https://github.com/tmux/tmux.git
synced 2026-10-11 13:04:22 +00:00
Match Kitty image sizing and layer order, preserve trailing backgrounds, and add regression coverage.
This commit is contained in:
136
regress/image-kitty-layers.sh
Normal file
136
regress/image-kitty-layers.sh
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Kitty output layers must not depend on upload, placement or redraw order.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
export TERM
|
||||
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
export TEST_TMUX
|
||||
command -v python3 >/dev/null || exit 0
|
||||
|
||||
python3 - <<'PY'
|
||||
import base64
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
binary = str(Path(os.environ['TEST_TMUX']).resolve())
|
||||
inner = [binary, '-Lkitty-layers-inner' + str(os.getpid()), '-f/dev/null']
|
||||
outer = [binary, '-Lkitty-layers-outer' + str(os.getpid()), '-f/dev/null']
|
||||
|
||||
|
||||
# Run one command against the selected test server.
|
||||
def run(server, *args):
|
||||
result = subprocess.check_output(server + list(args), text=True).strip()
|
||||
return result
|
||||
|
||||
|
||||
# Build a graphics command that leaves the cursor unchanged.
|
||||
def graphics(control, payload=''):
|
||||
return '\033_G' + control + (';' + payload if payload else '') + '\033\\'
|
||||
|
||||
|
||||
# Replay uploads and placements using Kitty's internal-ID tie breaker.
|
||||
def scene(data):
|
||||
images, placements = {}, {}
|
||||
loading = bytearray()
|
||||
upload = None
|
||||
for match in re.finditer(rb'\033_G([^\033]*)\033\\', data):
|
||||
control, _, payload = match[1].partition(b';')
|
||||
fields = dict(item.split(b'=', 1) for item in control.split(b','))
|
||||
action = fields.get(b'a', b't')
|
||||
image_id = int(fields.get(b'i', 0))
|
||||
if action == b't':
|
||||
if image_id:
|
||||
upload = image_id
|
||||
loading.clear()
|
||||
loading.extend(base64.b64decode(payload))
|
||||
if fields.get(b'm', b'0') == b'0':
|
||||
# The one-pixel fixture has a duplicated border on each side.
|
||||
assert len(loading) == 36, len(loading)
|
||||
images[upload] = (len(images), bytes(loading[16:20]))
|
||||
elif action == b'p':
|
||||
placements[image_id, int(fields[b'p'])] = int(fields.get(b'z', 0))
|
||||
elif action == b'd':
|
||||
if b'p' in fields:
|
||||
placements.pop((image_id, int(fields[b'p'])), None)
|
||||
else:
|
||||
placements = {key: z for key, z in placements.items()
|
||||
if key[0] != image_id}
|
||||
if not placements:
|
||||
return None, len(images)
|
||||
top = max(placements, key=lambda key: (placements[key], images[key[0]][0]))
|
||||
return images[top[0]][1], len(images)
|
||||
|
||||
|
||||
# Wait for a rendered scene, rather than only the application's input write.
|
||||
def expect(name, colour, uploads):
|
||||
deadline = time.monotonic() + 3
|
||||
while time.monotonic() < deadline:
|
||||
actual, count = scene(capture.read_bytes())
|
||||
if actual == colour and count >= uploads:
|
||||
return
|
||||
time.sleep(0.05)
|
||||
raise AssertionError((name, actual, colour, count, uploads))
|
||||
|
||||
|
||||
red = bytes((255, 0, 0, 255))
|
||||
white = bytes((255, 255, 255, 255))
|
||||
older = graphics('a=t,q=2,i=200,f=32,s=1,v=1', base64.b64encode(red).decode())
|
||||
newer = graphics('a=t,q=2,i=100,f=32,s=1,v=1', base64.b64encode(white).decode())
|
||||
put_older = graphics('a=p,q=2,C=1,i=200,c=1,r=1')
|
||||
put_newer = graphics('a=p,q=2,C=1,i=100,c=1,r=1')
|
||||
commands = [older + newer + put_newer, put_older,
|
||||
older + put_older,
|
||||
graphics('a=d,d=I,q=2,i=200') + older + put_older,
|
||||
put_older.replace('C=1', 'C=1,z=2147483646') +
|
||||
put_newer.replace('C=1', 'C=1,z=2147483647')]
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='tmux-kitty-layers-') as tmp:
|
||||
directory = Path(tmp)
|
||||
capture = directory / 'output'
|
||||
capture.touch()
|
||||
helper = directory / 'emit.py'
|
||||
helper.write_text('import os, sys, time\n'
|
||||
'commands = ' + repr(commands) + '\n'
|
||||
'for index, command in enumerate(commands):\n'
|
||||
' if index: sys.stdin.readline()\n'
|
||||
' os.write(1, command.encode())\n'
|
||||
'time.sleep(30)\n')
|
||||
try:
|
||||
run(inner, 'new-session', '-d', '-x', '40', '-y', '12',
|
||||
shlex.join(['python3', str(helper)]))
|
||||
if run(inner, 'display-message', '-p', '#{image_support}') == '0':
|
||||
raise SystemExit(0)
|
||||
run(inner, 'set', '-g', 'status', 'off')
|
||||
run(inner, 'set', '-as', 'terminal-features', ',*:kitty')
|
||||
pane = run(outer, 'new-session', '-dP', '-F', '#{pane_id}',
|
||||
'-x', '40', '-y', '12', 'exec sh')
|
||||
run(outer, 'set', '-g', 'status', 'off')
|
||||
run(outer, 'pipe-pane', '-t', pane, '-O',
|
||||
'cat >' + shlex.quote(str(capture)))
|
||||
run(outer, 'send-keys', '-t', pane, '-l', shlex.join(inner + ['attach']))
|
||||
run(outer, 'send-keys', '-t', pane, 'Enter')
|
||||
expect('newer-placed-first', white, 1)
|
||||
run(inner, 'send-keys', 'Enter')
|
||||
expect('older-uploaded-last', white, 2)
|
||||
run(inner, 'refresh-client')
|
||||
time.sleep(0.2)
|
||||
expect('full-redraw', white, 2)
|
||||
run(inner, 'send-keys', 'Enter')
|
||||
expect('older-retransmitted', white, 3)
|
||||
run(inner, 'send-keys', 'Enter')
|
||||
expect('older-recreated', red, 4)
|
||||
run(inner, 'send-keys', 'Enter')
|
||||
expect('maximum-z-order', white, 6)
|
||||
finally:
|
||||
for server in (outer, inner):
|
||||
subprocess.run(server + ['kill-server'], stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
PY
|
||||
@@ -327,14 +327,52 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp:
|
||||
check('placeholder-erase', virtual + colours + ph + '\033[H\033[2K', '0,0',
|
||||
text='', render=r'^\s*$')
|
||||
white_pixel = '/////w=='
|
||||
check('letterbox', graphics('a=T,q=2,f=32,s=1,v=1,c=2,r=2,C=1', white_pixel),
|
||||
'0,0', render='▄▄')
|
||||
check('pillarbox', graphics('a=T,q=2,f=32,s=1,v=1,c=4,r=1,C=1', white_pixel),
|
||||
'0,0', render=(r'^(?:\x1b\[[0-9;]+m)* '
|
||||
r'(?:\x1b\[[0-9;]+m)*\x1b\[48;2;255;255;255m {2}'))
|
||||
# Ordinary placements stretch; placeholders retain aspect ratio.
|
||||
check('stretch-tall', graphics('a=T,q=2,f=32,s=1,v=1,c=2,r=2,C=1',
|
||||
white_pixel),
|
||||
'0,0', render=r'48;2;255;255;255m {2}')
|
||||
check('stretch-wide', graphics('a=T,q=2,f=32,s=1,v=1,c=4,r=1,C=1',
|
||||
white_pixel),
|
||||
'0,0', render=r'48;2;255;255;255m {4}')
|
||||
check('stretch-crop', graphics('a=T,q=2,f=32,s=24,v=32,x=4,y=8,w=8,h=8,'
|
||||
'c=2,r=2,C=1', white),
|
||||
'0,0', render=r'48;2;255;255;255m {2}')
|
||||
check('virtual-letterbox', graphics('a=T,q=2,U=1,i=7,p=1,f=32,s=1,v=1,'
|
||||
'c=2,r=2', white_pixel) + colours +
|
||||
ph + '\u0305\u0305' + ph + '\u0305\u030d',
|
||||
'2,0', render='▄▄')
|
||||
check('virtual-pillarbox', graphics('a=T,q=2,U=1,i=7,p=1,f=32,s=1,v=1,'
|
||||
'c=4,r=1', white_pixel) + colours +
|
||||
ph + '\u0305\u0305' + ph + '\u0305\u030d',
|
||||
'2,0', render=(r'^(?:\x1b\[[0-9;]+m)* '
|
||||
r'(?:\x1b\[[0-9;]+m)*\x1b\[48;2;255;255;255m '))
|
||||
# Equal-z images follow source creation order, even with descending IDs.
|
||||
older = graphics('a=t,q=2,i=200,f=32,s=1,v=1', pixel)
|
||||
newer = graphics('a=t,q=2,i=100,f=32,s=1,v=1', white_pixel)
|
||||
put_older = graphics('a=p,q=2,C=1,i=200,c=1,r=1')
|
||||
put_newer = graphics('a=p,q=2,C=1,i=100,c=1,r=1')
|
||||
check('same-z-descending-ids', older + newer + put_older + put_newer,
|
||||
'0,0', render=r'48;2;255;255;255m ')
|
||||
check('same-z-reversed-placements',
|
||||
older + newer + put_newer + put_older,
|
||||
'0,0', render=r'48;2;255;255;255m ')
|
||||
check('same-z-retransmit',
|
||||
older + newer + put_newer + older + put_older,
|
||||
'0,0', render=r'48;2;255;255;255m ')
|
||||
check('same-z-recreate', older + newer + put_newer +
|
||||
graphics('a=d,d=I,q=2,i=200') + older + put_older,
|
||||
'0,0', render=r'48;2;255;0;0m ')
|
||||
# An empty line tail must not discard the preceding background run.
|
||||
background = '\033[11GX\033[17G\033[42m \033[0m'
|
||||
check('background-tail', background, '20,0', render=r'42m {4}')
|
||||
check('background-tail-image',
|
||||
graphics('a=T,q=2,C=1,f=32,s=1,v=1,c=20,r=1,z=-1073741825',
|
||||
white_pixel) +
|
||||
background, '20,0', render=r'42m {4}')
|
||||
offset_source = base64.b64encode(b'\xff' * (8 * 32 * 4)).decode()
|
||||
check('scaled-offset', graphics('a=T,q=2,f=32,s=8,v=32,c=1,r=1,X=4,C=1',
|
||||
offset_source), '0,0', render=r'12[78];12[78];12[78]m')
|
||||
offset_source),
|
||||
'0,0', render=r'191;191;191m')
|
||||
square = base64.b64encode(b'\xff' * (8 * 8 * 4)).decode()
|
||||
check('vertical-offset', graphics('a=T,q=2,f=32,s=8,v=8,c=1,r=1,Y=8,C=1',
|
||||
square), '0,0', render='▄')
|
||||
|
||||
Reference in New Issue
Block a user