summaryrefslogtreecommitdiff
path: root/test/appearance_gallery.py
blob: 46b17dd9a3cec608f16e1ef4f9c66cd464d30853 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#!/usr/bin/env python3
"""Review native palettes, compact tags, sized fonts and pets in an owned SDL session."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import subprocess
import time

from fs import session
from ui_review import capture, command, panes, require, wait_for


THEMES = ('forge', 'lagoon', 'solarium', 'spectrum', 'harvest', 'clay')
NOTES = b'''Pardes / workspace review

New belongs to the column.
Tty opens a terminal.
Mode switches its input mode.

Font MartianMono-NrRg:18
TaglineSize 80

Native palettes
  forge       familiar charcoal
  lagoon      oceanic blue
  solarium    quiet solar tones
  spectrum    vivid syntax
  harvest     warm earth
  clay        soft rose and pine

Pet cat / Pet frog / Pet off
Companions stay in unused workspace-tag space.
'''
PREFERENCE_NOTES = b'''Pardes / contrast studies

Comments carry their weight.
Line numbers stay in the background.
Current line: bold quiet ink, no bright block.

Forge: neutral, near-black charcoal
  Theme forge
  Theme forge_black
  Theme forge_soft

Orchard: crisp woodland color
  Theme orchard
  Theme orchard_black

Dusk: intentionally soft and warm
  Theme dusk

Themes: Pardes themes come first.
'''


def run(args):
    root = args.output.resolve()
    root.mkdir(parents=True, mode=0o700, exist_ok=True)
    root.chmod(0o700)
    config = root / 'config/pardes'
    config.mkdir(parents=True, exist_ok=True)
    (config / 'init').write_text('Shell /bin/sh\nTheme forge\nFont MartianMono-NrRg:18\nTaglineSize 80\n')
    source_path = args.project.resolve() / 'gossip/src/crds_value.rs'
    original = source_path.read_bytes()
    name = 'appearance-gallery-' + str(os.getpid())
    launch = ['-c', 'stty cols 150 rows 48; exec "$@"', name,
              str(args.binary.resolve()), '--9p=' + name, str(source_path)]
    inherited = {'PARDES_TEST': '1', 'PARDES_TEST_CAPTURE_DIR': str(root),
                 'PARDES_TEST_COLS': '150', 'PARDES_TEST_ROWS': '48',
                 'SDL_VIDEODRIVER': 'x11'}
    if args.live_window:
        inherited = {'SDL_VIDEODRIVER': 'x11'}
    handles = {}
    report = {'themes': [], 'pets': [], 'os_windows': []}
    with session('/bin/sh', root, name, socket_name=name, tty=True,
                 launch=launch, inherited=inherited, terminal=handles) as (client, _):
        source = next(serial for serial, tag in panes(client).items() if str(source_path) in tag)
        before = set(panes(client))
        command(client, source, 'Newcol')  # empty, its tag with the keyboard
        right = int(client.read('/pane/new'))
        client.write(f'/pane/{right}/name', f'{root / "Review"}\n'.encode())
        client.write(f'/pane/{right}/body', PREFERENCE_NOTES if args.theme_selector else NOTES, truncate=True)
        client.write(f'/pane/{right}/dirty', b'0\n')
        before = set(panes(client))
        command(client, right, 'Tty')
        terminal = require(wait_for(lambda: next((s for s in panes(client) if s not in before), None)), 'terminal missing')
        client.write(f'/pane/{terminal}/pty/data', b"PS1='$ '; printf '\\033[2J\\033[H\\033[1;36mPardes workspace\\033[0m\\n\\n'; printf 'Agave source: read-only\\nTerminal: ready\\n\\n'; printf '\\033[32mSmall touches. Same workflow.\\033[0m\\n'\r")
        require(wait_for(lambda: b'Small touches. Same workflow.' in client.read(f'/pane/{terminal}/body')), 'terminal output missing')

        def config_report():
            before = set(panes(client))
            command(client, right, 'Config')
            pane = require(wait_for(lambda: next((s for s in panes(client) if s not in before), None)), 'Config output missing')
            text = client.read(f'/pane/{pane}/body').decode()
            client.write(f'/pane/{pane}/exec', b'Del\n')
            return text

        settings = config_report()
        require('Font effective size 18.00 pixels' in settings, 'configured font size not applied: ' + settings)
        require('TaglineSize 80' in settings, 'configured tag size missing')
        (root / 'effective-config.txt').write_text(settings)
        report['font_effective_pixels'] = 18
        report['tagline_percent'] = 80
        for serial, tag in panes(client).items():
            require(' New ' not in tag, 'pane still contains New')
            require('Newtty' not in tag, 'old terminal name remains')
        require('Mode' in panes(client)[terminal], 'terminal mode action missing')

        def owned_window():
            result = subprocess.run(['xdotool', 'search', '--onlyvisible', '--pid', str(handles['pid'])],
                                    text=True, capture_output=True)
            ids = result.stdout.split()
            if not ids:
                return None
            window = ids[0]
            actual_pid = subprocess.check_output(['xdotool', 'getwindowpid', window], text=True).strip()
            require(actual_pid == str(handles['pid']), 'OS capture window is not owned')
            return window

        window = require(wait_for(owned_window), 'owned SDL window missing')
        subprocess.run(['xdotool', 'windowsize', window, '1440', '960'], check=True)

        def take_frame(label):
            capture(client, root, label, not args.live_window)
            if not args.live_window:
                return
            window = require(owned_window(), 'owned SDL window disappeared')
            subprocess.run(['magick', 'import', '-window', window, str(root / (label + '.png'))], check=True)
            if window not in report['os_windows']:
                report['os_windows'].append(window)

        for theme in args.themes:
            command(client, right, 'Theme ' + theme)
            if args.theme_selector:
                # Keep both quiet active gutter ink and vivid comments in view.
                client.write(f'/pane/{source}/look', f'{source_path}:117\n'.encode())
            take_frame('classic-' + theme)
            if args.theme_selector:
                frame = client.screen()
                row = frame['cursor']['y'] * frame['cols']
                gutter = frame['styles'][frame['cells'][row + 2][1]]
                margin = frame['styles'][frame['cells'][row + 1][1]]
                require(gutter['bg'] == margin['bg'], 'active line number has a background block in ' + theme)
                require(gutter['bold'], 'active line number is not bold in ' + theme)
                previous = frame['styles'][frame['cells'][row - frame['cols'] + 2][1]]
                require(not previous['bold'], 'inactive line number is bold in ' + theme)
                active_tag = frame['styles'][frame['cells'][2 * frame['cols'] + 2][1]]
                inactive_tag = frame['styles'][frame['cells'][2 * frame['cols'] + frame['cols'] // 2 + 2][1]]
                def ink_polarity(style):
                    return sum(style['fg']['rgb']) - sum(style['bg']['rgb'])
                require(ink_polarity(active_tag) * ink_polarity(inactive_tag) > 0,
                        'focused tag inverts the inactive tag in ' + theme)
                report['active_gutter_bold_without_block'] = True
                report['focus_tags_without_inversion'] = True
            report['themes'].append(theme)
        command(client, right, 'Theme forge')
        if args.theme_selector:
            command(client, terminal, 'Collapse')
            before = set(panes(client))
            command(client, right, 'Themes')
            selector = require(wait_for(lambda: next((s for s in panes(client) if s not in before), None)),
                               'Themes missing')
            body = client.read(f'/pane/{selector}/body').decode()
            native, legacy = body.split('# Legacy and imported themes', 1)
            for theme in ('forge_black', 'forge_soft', 'orchard_black'):
                require('Theme ' + theme + '\n' in native, 'native contrast variant below legacy heading')
            require('Theme helix\n' in legacy, 'legacy theme section missing')
            require('Theme orchard' in ''.join(c[0] for c in client.screen()['cells']),
                    'Themes did not open at native first entry')
            take_frame('theme-selector')
            report['native_themes_first'] = True
            client.write(f'/pane/{selector}/exec', b'Del\n')
            command(client, terminal, 'Collapse')
        for pet in (() if args.skip_pets else ('cat', 'frog', 'off')):
            command(client, right, 'Pet ' + pet)
            settings = config_report()
            require('Pet: ' + pet + '\n' in settings, 'pet setting not applied')
            # Returning focus to the source closes all transient report state.
            client.write(f'/pane/{source}/look', f'{source_path}:1\n'.encode())
            take_frame('pet-' + pet)
            if pet != 'off':
                frame = client.screen()
                original_tag = frame['cells'][:frame['cols']]
                time.sleep(1)
                take_frame('pet-' + pet + '-later')
                frame = client.screen()
                require(original_tag == frame['cells'][:frame['cols']], 'pet animation changed tag text or styles')
                def band_digest(label):
                    pixels = subprocess.check_output(['magick', str(root / (label + '.png')),
                        '-crop', '1500x20+0+0', '-depth', '8', 'RGB:-'])
                    return hashlib.sha256(pixels).hexdigest()
                require(band_digest('pet-' + pet) != band_digest('pet-' + pet + '-later'),
                        'pet did not animate in workspace tag')
            report['pets'].append(pet)
        require(client.read(f'/pane/{source}/body') == original == source_path.read_bytes(), 'Agave source changed')
        report['source_unchanged'] = True
        client.write(f'/pane/{terminal}/pty/data', b'exit\r')
    (root / 'report.json').write_text(json.dumps(report, indent=2))
    print(json.dumps(report, indent=2))


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('binary', type=Path)
    parser.add_argument('output', type=Path)
    parser.add_argument('--project', required=True, type=Path)
    parser.add_argument('--live-window', action='store_true', help='capture the owned OS window without offscreen test mode')
    parser.add_argument('--themes', nargs='+', default=THEMES, help='themes to compare in order')
    parser.add_argument('--skip-pets', action='store_true', help='review palettes without repeating pet animation tests')
    parser.add_argument('--theme-selector', action='store_true', help='capture native-first picker and focused source gutter')
    run(parser.parse_args())