mirror of
https://github.com/tmux/tmux.git
synced 2026-10-16 16:44:30 +00:00
194 lines
7.9 KiB
Bash
194 lines
7.9 KiB
Bash
#!/bin/sh
|
|
|
|
# Kitty ordinary placement geometry, cursor movement and replacement.
|
|
|
|
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 base64
|
|
import sys
|
|
from image_test import image_test
|
|
from image_terminal import Terminal
|
|
|
|
|
|
def graphics(control, data=b''):
|
|
"""Build a Kitty graphics APC with replies suppressed."""
|
|
payload = b';' + base64.b64encode(data) if data else b''
|
|
return b'\033_Gq=2,' + control.encode() + payload + b'\033\\'
|
|
|
|
|
|
colours = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)]
|
|
raw = bytes(value for y in range(32) for x in range(16)
|
|
for value in (*colours[2 * (y >= 16) + (x >= 8)], 255))
|
|
upload = graphics('a=t,i=7,f=32,s=16,v=32', raw)
|
|
put = 'a=p,i=7,C=1'
|
|
|
|
|
|
def pattern(w=16, h=32, xoff=0, yoff=0):
|
|
"""Expected positions and colours for a scaled four-quadrant image."""
|
|
return {(x + xoff, y + yoff): colours[2 * (y >= h // 2) + (x >= w // 2)]
|
|
for y in range(h) for x in range(w)}
|
|
|
|
|
|
def solid(w, h, colour, xoff=0, yoff=0):
|
|
"""Expected positions for a solid rectangle."""
|
|
return {(x + xoff, y + yoff): colour for y in range(h) for x in range(w)}
|
|
|
|
|
|
with image_test(sys.argv[1]) as test:
|
|
client = test.attach()
|
|
fallback = test.attach(graphical=False)
|
|
|
|
def check(command, expected, cursor='0,0'):
|
|
"""Check the displayed image, its redraw and the pane cursor."""
|
|
test.emit(b'\033[2J\033[H' + command)
|
|
actual = test.run('display-message', '-p', '#{cursor_x},#{cursor_y}')
|
|
assert actual == cursor, ('cursor', actual, cursor)
|
|
term = Terminal()
|
|
term.feed(client.data)
|
|
term.check(expected)
|
|
test.redraw()
|
|
term = Terminal()
|
|
term.feed(client.data)
|
|
term.check(expected)
|
|
assert not fallback.images(), 'graphics sent to fallback client'
|
|
|
|
# Uploading alone draws nothing; placement reuses the stored pixels.
|
|
check(upload, {})
|
|
check(graphics(put), pattern())
|
|
term = Terminal()
|
|
term.feed(fallback.data)
|
|
assert term.cells[0, 0][1:3] == (196, 196), term.cells[0, 0]
|
|
check(graphics('a=T,i=7,f=32,s=16,v=32', raw), pattern(), cursor='2,1')
|
|
check(graphics(put + ',c=4'), pattern(32, 64))
|
|
check(graphics(put + ',r=4'), pattern(32, 64))
|
|
check(graphics(put + ',c=4,r=1'), pattern(32, 16))
|
|
|
|
# Crop first, then scale; oversized crop dimensions stop at the source edge.
|
|
check(graphics(put + ',y=16,w=8,h=16,c=2,r=1'), solid(16, 16, colours[2]))
|
|
check(graphics(put + ',x=8,y=16,w=999,h=999,c=1,r=1'),
|
|
solid(8, 16, colours[3]))
|
|
|
|
# Natural and explicit sizes both honour offsets within the first cell.
|
|
check(graphics(put + ',X=4,Y=8'), pattern(xoff=4, yoff=8))
|
|
check(graphics(put + ',c=3,r=3,X=4,Y=8'), pattern(20, 40, 4, 8))
|
|
check(graphics(put + ',c=1,r=1,X=4'), pattern(4, 16, 4))
|
|
check(graphics(put + ',X=4294967295'), pattern(xoff=7))
|
|
check(b'\033[1;40H' + graphics(put),
|
|
{(312 + x, y): colours[2 * (y >= 16)]
|
|
for y in range(32) for x in range(8)}, cursor='39,0')
|
|
|
|
# Stationary images clip at the bottom; moving images scroll by one row.
|
|
check(b'\033[16;1H' + graphics(put),
|
|
{(x, 240 + y): colours[x >= 8]
|
|
for y in range(16) for x in range(16)}, cursor='0,15')
|
|
check(b'\033[16;1H' + graphics('a=p,i=7'), pattern(yoff=224),
|
|
cursor='2,15')
|
|
|
|
# Named placement replacement removes the earlier display.
|
|
first = graphics(put + ',p=9')
|
|
second = b'\033[4;5H' + graphics(put + ',p=9,c=1,r=1')
|
|
check(first + second, pattern(8, 16, 32, 48), cursor='4,3')
|
|
|
|
# Placement zero creates independent displays; IDs are local to an image.
|
|
check(graphics(put) + b'\033[4;5H' + graphics(put),
|
|
pattern() | pattern(xoff=32, yoff=48), cursor='4,3')
|
|
blue = graphics('a=t,i=8,f=32,s=1,v=1', b'\0\0\xff\xff')
|
|
other = b'\033[4;5H' + graphics('a=p,i=8,p=9,C=1,c=1,r=1')
|
|
check(first + blue + other, pattern() | solid(8, 16, colours[2], 32, 48),
|
|
cursor='4,3')
|
|
|
|
# Failed placement validation preserves the old source and display.
|
|
bad = graphics('a=T,i=7,p=9,f=32,s=1,v=1,x=1,C=1', b'\0\xff\0\xff')
|
|
check(first + bad + b'\033[4;5H' + graphics(put + ',c=1,r=1'),
|
|
pattern() | pattern(8, 16, 32, 48), cursor='4,3')
|
|
check(first + graphics(put + ',p=9,x=16'), pattern())
|
|
|
|
# Successful source replacement removes all its old placements.
|
|
green = graphics('a=t,i=7,f=32,s=1,v=1', b'\0\xff\0\xff')
|
|
check(first + b'\033[4;5H' + graphics(put) + green, {}, cursor='4,3')
|
|
check(graphics(put + ',c=1,r=1'), solid(8, 16, colours[1]))
|
|
|
|
# Replacement also removes displays saved by the alternate screen.
|
|
check(upload + first + b'\033[?1049h' + green + b'\033[?1049l', {})
|
|
check(upload + first + b'\033[?1049h' + second + b'\033[?1049l', {})
|
|
|
|
# Anonymous upload-and-place does not require a stored source mapping.
|
|
check(graphics('a=T,f=32,s=1,v=1,p=9,C=1,c=1,r=1', b'\xff' * 4),
|
|
solid(8, 16, colours[3]))
|
|
|
|
# A placement keeps shared pixels after its anonymous source is released.
|
|
check(graphics('a=T,f=32,s=16,v=32,X=4,Y=8,C=1', raw),
|
|
pattern(xoff=4, yoff=8))
|
|
|
|
# Clients sample the original crop independently, including partial spans.
|
|
with image_test(sys.argv[1]) as test:
|
|
client = test.attach()
|
|
source_width, source_height = 32, 32
|
|
source = [(255, 0, 0) if (x + y) % 2 else (0, 0, 255)
|
|
for y in range(source_height) for x in range(source_width)]
|
|
pixels = bytes(value for colour in source for value in (*colour, 255))
|
|
# Shrink horizontally and enlarge vertically with transparent offsets.
|
|
command = graphics('a=T,f=32,s=32,v=32,x=4,y=4,w=24,h=24,'
|
|
'c=2,r=4,X=4,Y=8,C=1', pixels)
|
|
test.emit(command)
|
|
large = test.attach(cw=24, ch=24)
|
|
|
|
def expected(cw, ch):
|
|
"""Map output coordinates through the full placement to its crop."""
|
|
return {(x, y): source[(4 + (y * 16 - 8 * ch) * 24 // (ch * 56)) * 32 +
|
|
4 + (x * 8 - 4 * cw) * 24 // (cw * 12)]
|
|
for y in range(ch // 2, ch * 4) for x in range(cw // 2, cw * 2)}
|
|
|
|
def check_clients(clipped=False):
|
|
"""Check shared-source scaling and alignment on both clients."""
|
|
test.redraw()
|
|
terminals = []
|
|
for attached, cw, ch in [(client, 8, 16), (large, 24, 24)]:
|
|
term = Terminal(cw=cw, ch=ch)
|
|
term.feed(attached.data)
|
|
wanted = expected(cw, ch)
|
|
if clipped:
|
|
wanted = {point: colour for point, colour in wanted.items()
|
|
if point[0] < cw}
|
|
term.check(wanted)
|
|
terminals.append((attached, cw, ch, term))
|
|
return terminals
|
|
|
|
terminals = check_clients()
|
|
test.emit(b'\033[2;1H\033[X')
|
|
for attached, cw, ch, term in terminals:
|
|
term.feed(attached.data)
|
|
term.check(expected(cw, ch))
|
|
check_clients()
|
|
# Clip the right-hand span without changing either client's cell pixels.
|
|
test.run('resize-window', '-x', '1')
|
|
check_clients(clipped=True)
|
|
# Changing cell pixels must retain the same partial-span source origin.
|
|
test.resize(client, 1, 16, cw=24, ch=24)
|
|
test.redraw()
|
|
term = Terminal(cw=24, ch=24)
|
|
term.feed(client.data)
|
|
wanted = {point: colour for point, colour in expected(24, 24).items()
|
|
if point[0] < 24}
|
|
term.check(wanted)
|
|
|
|
# Offsets and source crops contribute fractional coverage to text fallback.
|
|
with image_test(sys.argv[1]) as test:
|
|
test.run('set-option', '-as', 'terminal-features', 'screen-256color:RGB')
|
|
client = test.attach(graphical=False)
|
|
pixels = b'\xff\0\0\xff' * 16 + b'\0\0\xff\xff' * 16
|
|
test.emit(graphics('a=T,f=32,s=8,v=4,x=1,w=6,c=1,r=1,X=4,Y=4,C=1',
|
|
pixels))
|
|
test.redraw()
|
|
term = Terminal()
|
|
term.feed(client.data)
|
|
char, fg, bg, bold = term.cells[0, 0]
|
|
assert char == '\u2584' and not bold, (char, bold)
|
|
assert fg == (31, 0, 95) and bg == (63, 0, 0), (fg, bg)
|
|
PY
|