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, 'DumpConfig')
pane = require(wait_for(lambda: next((s for s in panes(client) if s not in before), None)), 'DumpConfig 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(' 18.00 pixels\n' 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())
|