a73x

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()