test/native_theme.py
Ref: Size: 6.8 KiB History
#!/usr/bin/env python3
"""Assert the explicitly selected trial appearance through real terminal output and painted pixels."""
import shlex
import sys
sys.dont_write_bytecode = True
from native_resize import by_id, pty_sizes
from native_tiling import eventually, require
# These are acceptance values, deliberately independent of the painter source.
BG = '211b1a'
FG = 'f3e8d0'
FOCUS = '2f6f70'
INACTIVE = '342824'
DIVIDER = '76594b'
MODAL = '2b211f'
SELECTED = '496f68'
BELL = '9a5c24'
CURSOR = 'd8a84e'
ANSI = ('211b1a', 'd46a5a', '8ebf7a', 'd8a84e',
'7aa2c8', 'b28abf', '62b8a9', 'f3e8d0',
'665954', 'ef8b70', 'b4d98f', 'f0c878',
'a8cbe8', 'd1a9d1', '8cddd0', 'fff4df')
def pixel(capture, x, y):
width, height, data = capture
x, y = int(x), int(y)
require(0 <= x < width and 0 <= y < height, 'sample outside framebuffer')
offset = (y * width + x) * 3
return data[offset:offset + 3].hex()
def check_samples(rig, samples, label):
last = []
def painted():
capture = rig.last_pixels()
last[:] = [(name, expected, pixel(capture, x, y))
for name, x, y, expected in samples
if pixel(capture, x, y) != expected]
return not last
try:
eventually(painted, label)
except AssertionError:
raise AssertionError(f'{label}: {last}') from None
def edge_sample(name, rect, expected):
return name, rect['x'] + rect['w'] - 2, rect['y'] + 2, expected
def paint_specimen(rig, pane_id, sock, session):
rig.focus(pane_id)
state = rig.state()
pane = by_id(state)[pane_id]
require(pane['cols'] >= 32 and pane['rows'] >= 12, 'theme fixture too small')
marker = 'THEME-READY-' + session
text = '\033[0m\033[2J\033[HWarm native appearance'
for index in range(16):
row, col = 3 + index // 8, 1 + 4 * (index % 8)
text += f'\033[{row};{col}H\033[48;5;{index}m \033[0m'
text += ('\033[6;1H\033[48;5;17m \033[0m'
'\033[6;5H\033[48;5;232m \033[0m'
'\033[6;9H\033[48;5;255m \033[0m'
'\033[7;1H\033[7m \033[0m'
'\033[8;1H\033[48;2;18;52;86m \033[0m'
'\033[10;1H' + marker + '\033[12;2H')
# Shell printf consumes the escaped bytes; PS1 remains blank at the fixed cursor.
rig.shell("export PS1=''; printf '%b' " + shlex.quote(text.replace('\033', '\\033')))
rig.wait_state(lambda s: marker in by_id(s)[pane_id]['painted_text'])
def terminal_samples(state, pane):
content, cw, ch = pane['content'], state['cell_w'], state['cell_h']
def cell(label, col, row, colour):
return (f"pane {pane['id']} {label}", content['x'] + (col + .5) * cw,
content['y'] + (row + .5) * ch, colour)
samples = [edge_sample('blank content', content, BG)]
for index, colour in enumerate(ANSI):
samples.append(cell(f'palette {index}', 4 * (index % 8), 2 + index // 8, colour))
for col, colour in [(0, '00005f'), (4, '080808'), (8, 'eeeeee')]:
samples.append(cell('extended palette', col, 5, colour))
samples += [cell('inverse defaults', 0, 6, FG), cell('explicit RGB', 0, 7, '123456')]
if pane['id'] == state['focus']:
samples.append(cell('cursor', 1, 11, CURSOR))
return samples
def modal_samples(rig, kind):
state = rig.wait_state(lambda s: s.get(kind))
modal = state[kind]
samples = [edge_sample(kind + ' background', modal['rect'], MODAL)]
selected = modal.get('selected', 0)
# Recovery hooks have no selected index; the initial action is Cancel/Retry.
for index, row in enumerate(modal['rows']):
if row['rect']['h']:
samples.append(edge_sample(f'{kind} row {index}', row['rect'],
SELECTED if index == selected else MODAL))
check_samples(rig, samples, kind + ' uses themed rows')
def main():
from native_lifecycle import LifecycleRig, start_workspace
require(len(sys.argv) == 3, 'usage: native_theme.py MUX MUXG')
rig = LifecycleRig(*sys.argv[1:])
rig.float_size, rig.float_scale = (1000, 650), 2
rig.env['MUXG_TEST_THEME'] = 'trial'
try:
sessions = start_workspace(rig)
identities = [(p['id'], p['generation']) for p in rig.state()['panes']]
for pane_id, (sock, session) in sessions.items():
paint_specimen(rig, pane_id, sock, session)
for focused in sessions:
rig.focus(focused)
state = rig.state()
samples = []
for pane in state['panes']:
samples += terminal_samples(state, pane)
samples.append(edge_sample('header', pane['header'],
FOCUS if pane['id'] == focused else INACTIVE))
for rail in state['dividers']:
rect = rail['rect']
samples.append(('divider', rect['x'], rect['y'], DIVIDER))
check_samples(rig, samples, 'terminal, headers, cursor and dividers')
rig.ok('three panes: all ANSI colours, extended palette, inverse, RGB, focus and cursor')
rig.chord('enter')
rig.picker('hosts')
modal_samples(rig, 'picker')
rig.key('down')
modal_samples(rig, 'picker')
rig.key('escape')
rig.key('escape')
rig.chord('p')
modal_samples(rig, 'recovery')
rig.key('escape')
rig.ok('host picker and recovery menu use themed backgrounds and selection')
state = rig.state()
header = by_id(state)[state['focus']]['header']
rig.shell("printf '\\007'")
check_samples(rig, [edge_sample('bell', header, BELL)], 'bell highlight')
check_samples(rig, [edge_sample('focus after bell', header, FOCUS)], 'bell expires')
pty_sizes(rig, sessions, identities)
rig.ok('bell theme expires back to focus; every PTY keeps its dimensions')
for remaining in (2, 1, 0):
rig.chord('d')
rig.wait_state(lambda s: len(s['panes']) == remaining)
state = rig.wait_state(lambda s: not s['picker'] and not s['recovery'])
# The empty menu is centred and its Add pane row is the second of four.
x, y, ch = state['width'] / 2, state['height'] / 2, state['cell_h']
check_samples(rig, [('empty background', x, y - 1.5 * ch + 2, MODAL),
('Add pane highlight', x, y - .5 * ch + 2, SELECTED),
('empty surroundings', 1, 1, DIVIDER)], 'empty workspace theme')
for sock, session in sessions.values():
require('THEME-READY-' + session in rig.dump(sock, session),
'detaching removed the daemon session')
rig.ok('empty workspace and Add pane are themed; detached sessions survive')
rig.quit()
print('PASS: native appearance', flush=True)
finally:
rig.close()
if __name__ == '__main__':
main()