summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 12:42:48 -0300
committerGabriel Schneider <[email protected]>2026-10-01 13:04:28 -0300
commitf6a511c61d7ef2ff680f70077b236559a595be69 (patch)
tree496144bde72ee3d8cad0d84ab1ff110f65b0308d /test
parent0892e3aa5cd46e43b0bb0ac10f47d2ee4f500409 (diff)
downloadpardes-main.tar.gz
pardes-main.zip
Clicks in the site's clips stop, then click in place with a ring; the hero opens on all three panes; new clips show the PDF and image panes; the Themes gallery has a shot of every themeHEADmain
record_demos.py: a click glides to its target, stops, and presses and releases with the pointer still (a ring and the button's name on the press, a pulse after), pausing before anything moves. The hero's README, main.c and output pane are all up from its first frame. pdf-navigation and pdf-tint-fit page the Intel SDM vol. 2 (--pdf, read where it is): PdfSections to CPUID, a search stepped with n, PdfTint's three looks, PdfFit both ways and a page address. image-pane opens a Mandelbrot PNG the script draws, then Petscii. `record_demos.py themes` shoots one scene in each theme of src/themes, in the Themes order, into docs/site/media/themes with themes.toml (ready). Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'test')
-rw-r--r--test/record_demos.py407
1 files changed, 351 insertions, 56 deletions
diff --git a/test/record_demos.py b/test/record_demos.py
index 655ec475..71bf1055 100644
--- a/test/record_demos.py
+++ b/test/record_demos.py
@@ -1,16 +1,23 @@
#!/usr/bin/env python3
-"""Record the documentation site's landing-page clips from a hidden window.
+"""Record the documentation site's clips and theme shots from a hidden window.
zig build -Dplatform=gui -Doptimize=ReleaseSafe --prefix <scratch dir>
-python3 -B test/record_demos.py <scratch dir>/bin/pardes-gui [clip ...]
+python3 -B test/record_demos.py <scratch dir>/bin/pardes-gui [clip ... | themes]
Each clip drives a throwaway session (PARDES_TEST: an offscreen capture,
the virtual clock) through its stdin's window-pixel mouse events, its keys
and its 9P files, keeps the settled frame after every step, and lays the
-frames out on a timeline with a drawn pointer. ffmpeg encodes the timeline
-to docs/site/media/<clip>.webm and .mp4, with <clip>.png as its poster and
-captions.txt beside them. Needs a Wayland display for the hidden window,
-gcc and git for the demo project, and 9ns (cloud9) for the scripting clip.
+frames out on a timeline with a drawn pointer: a click glides there,
+stops, and presses and releases still, a ring and the button's name drawn.
+ffmpeg encodes the timeline to docs/site/media/<clip>.webm and .mp4, with
+<clip>.png as its poster and captions.txt beside them (clips.toml, which
+the site reads, is kept by hand). `themes` instead writes
+docs/site/media/themes/<theme>.png for each theme of src/themes, in the
+Themes builtin's order, and themes.toml.
+
+Needs a Wayland display for the hidden window, gcc and git for the demo
+project, 9ns (cloud9) for the scripting clip, Pillow and numpy, and the
+Intel SDM vol. 2 for the PDF clips (--pdf; read where it is, never copied).
Frames and the intermediate video go under --work (default
../.scratch/rec/work, beside the checkout). The demo project lives at
@@ -241,6 +248,19 @@ class Session:
cw, ch = w / self.cols, h / self.rows
return int(start * cw + (col - start + offset) * TAG_W), int((row + .55) * ch)
+ def tag_word(self, serial, word):
+ """The pixel over the middle of `word` in pane `serial`'s tag, which
+ may run past the grid's columns (/screen cuts a tag at its width,
+ the window wraps it at its own): its rows hold (width - 3 cells) /
+ TAG_W characters each."""
+ tag = self.tag(serial)
+ w, h = self.size()
+ cw, ch = w / self.cols, h / self.rows
+ per_row = int((w - 3 * cw) // TAG_W)
+ top = self.find(tag[:min(len(tag), self.cols - 4)], 0)[1]
+ at = tag.index(word) + len(word) // 2
+ return int(3 * cw + (at % per_row + .5) * TAG_W), int((top + at // per_row + .55) * ch)
+
def size(self):
with open(self.caps / 'latest.ppm', 'rb') as f:
f.readline()
@@ -280,37 +300,43 @@ class Session:
# the timeline
- def shot(self, seconds, poster=False):
+ def shot(self, seconds, poster=False, ring=None, ring_button=None):
"""Hold the settled frame for `seconds`, the pointer where it is.
- `poster` marks the frame drawn as the clip's still."""
+ `poster` marks the frame drawn as the clip's still; `ring` draws a
+ click there ('press' while held, 'pulse' just after)."""
data = self.settle()
path = self.frames / f'{len(self.shots):04d}.ppm'
path.write_bytes(data)
self.shots.append(dict(frame=path, secs=seconds, start=self.pointer, end=self.pointer,
- button=self.button, badge=self.badge, poster=poster))
+ button=self.button, badge=self.badge, poster=poster, ring=ring,
+ ring_button=ring_button or self.button))
def glide(self, x, y, seconds=.6):
"""Glide the drawn pointer to (x, y) over the frame on screen, then
move the window's pointer there (dragging any button held)."""
if self.shots and self.pointer is not None:
last = self.shots[-1]
- self.shots.append(dict(last, secs=seconds, start=self.pointer, end=(x, y), poster=False))
+ self.shots.append(dict(last, secs=seconds, start=self.pointer, end=(x, y), poster=False, ring=None))
self.pointer = (x, y)
self.mouse('motion', self.button or 0, x, y)
- def sweep(self, button, start, end, hold=.5):
- """Press `button` (SDL: 1 left, 2 middle, 3 right) at `start`,
- drag it to `end` and release: the sweep acme's B2 and B3 take."""
- if self.pointer != start:
- self.glide(*start)
- self.mouse('down', button, *start)
+ def click(self, button, x, y, done=None, what='the click', rest=.4, hold=.35, after=1.0):
+ """Glide to (x, y) and stop; a beat later press `button` (SDL: 1
+ left, 2 middle, 3 right) and release it with the pointer still, a
+ ring and the button's name drawn on the press; once `done()` says
+ the click took, hold the result `after` seconds, the ring pulsing
+ out, before anything moves again."""
+ if self.pointer != (x, y):
+ self.glide(x, y)
+ self.shot(rest)
+ self.mouse('down', button, x, y)
self.button = button
- self.shot(.15)
- if end != start:
- self.glide(*end, seconds=.3)
- self.shot(hold)
- self.mouse('up', button, *self.pointer)
+ self.shot(hold, ring='press')
+ self.mouse('up', button, x, y)
self.button = None
+ if done is not None:
+ self.wait(done, what)
+ self.shot(after, ring='pulse', ring_button=button)
def press(self, keys, per_key=.4):
"""Send each (key bytes, badge) of `keys`, a frame a key with the
@@ -360,21 +386,31 @@ class Painter:
self.font = ImageFont.truetype(str(BADGE_FONT), 24)
self.size = size
- def paint(self, img, pointer, button, badge):
+ def paint(self, img, pointer, badge, ring=None, button=None, since=0.0):
+ """`img` with the badge, the pointer, and a click: `ring` 'press'
+ (a ring and the button's name) or 'pulse' (`since` seconds after the
+ release: the ring widening and fading, the name kept a moment)."""
img = img.copy()
w, h = self.size
if badge:
draw_badge(img, badge, (w - 16, h - 16), self.font, anchor='rb')
if pointer is not None:
x, y = pointer
- if button:
+ if ring and button:
label, colour = BUTTONS[button]
d = ImageDraw.Draw(img, 'RGBA')
- r = 16
- d.ellipse((x - r, y - r, x + r, y + r), outline=colour + (255,), width=4)
- # The name sits below the pointer, kept inside the frame.
- lx = min(x + 22, w - 16 - self.font.getlength(label))
- draw_badge(img, label, (lx, y + 40), self.font, fill=colour)
+ if ring == 'press':
+ r, alpha, named = 16, 255, True
+ else:
+ t = min(since / .45, 1)
+ r, alpha, named = 16 + 22 * t, int(255 * (1 - t)), since < .6
+ if alpha > 0:
+ d.ellipse((x - r, y - r, x + r, y + r), outline=colour + (alpha,), width=4)
+ if named:
+ # The name sits below the pointer, kept inside the frame.
+ lx = min(x + 22, w - 16 - self.font.getlength(label))
+ ly = y + 40 if y + 70 < h else y - 60
+ draw_badge(img, label, (lx, ly), self.font, fill=colour)
img.paste(self.sprite, (int(x) - 1, int(y) - 1), self.sprite)
return img
@@ -409,22 +445,23 @@ def compose(shots, out):
n = round(clock * FPS) - emitted
for i in range(n):
t = ease((i + 1) / n)
- frame = painter.paint(img, lerp(shot['start'], shot['end'], t), shot['button'], shot['badge'])
+ frame = painter.paint(img, lerp(shot['start'], shot['end'], t), shot['badge'], shot['ring'],
+ shot['ring_button'], i / FPS)
ff.stdin.write(frame.tobytes())
if shot['poster'] and poster is None:
- poster = painter.paint(img, shot['end'], shot['button'], None)
+ poster = painter.paint(img, shot['end'], None)
emitted += n
last = shots[-1]
n = round(.7 * FPS)
for i in range(n):
t = ease((i + 1) / n)
frame = Image.blend(loaded[1], first, t)
- ff.stdin.write(painter.paint(frame, lerp(last['end'], shots[0]['start'], t), None, None).tobytes())
+ ff.stdin.write(painter.paint(frame, lerp(last['end'], shots[0]['start'], t), None).tobytes())
emitted += n
ff.stdin.close()
if ff.wait() != 0:
raise RuntimeError('ffmpeg (lossless) failed')
- return emitted / FPS, poster or painter.paint(first, None, None, None)
+ return emitted / FPS, poster or painter.paint(first, None, None)
def encode(lossless, name, poster, crf_webm=30, crf_mp4=20):
@@ -447,20 +484,29 @@ def encode(lossless, name, poster, crf_webm=30, crf_mp4=20):
# the clips
-def text_is_the_interface(s):
- """B2 on `make` in a tag runs it; B3 on the error opens the file there."""
+def hero_setup(s):
+ """README, main.c under it and an output pane under that, all on
+ screen from the first frame: the output pane is a finished `clear`,
+ which `make` will reuse."""
+ s.write('/pane/1/look', f'{PROJECT}/src/main.c\n')
+ s.wait(lambda: len(s.panes()) == 2, 'main.c')
+ s.write('/pane/1/exec', 'clear\n')
+ s.wait(lambda: any(kind == 'cmd' and 'exit' in s.tag(n) for n, kind in s.panes().items()), 'the output pane')
s.write('/pane/1/tag', ' make')
+ s.mouse('motion', 0, *s.cell_px(36, 9))
+
+
+def text_is_the_interface(s):
+ """B2 on `make` in a tag runs it; B3 on the error opens main.c there."""
+ output = next(n for n, kind in s.panes().items() if kind == 'cmd')
s.pointer = s.cell_px(36, 9)
- s.shot(1.0)
+ s.shot(1.2)
col, row = s.find('make', 0)
- s.sweep(2, s.tag_px(col, row, offset=.15), s.tag_px(col + 3, row, offset=.85))
- s.wait(lambda: any('exit' in s.tag(n) for n, kind in s.panes().items() if kind == 'cmd'), 'make to finish')
- s.shot(1.1)
+ s.click(2, *s.tag_px(col + 1, row), done=lambda: '(make) exit' in s.tag(output), what='make')
col, row = s.find('src/main.c:12:5', 0)
- s.sweep(3, s.cell_px(col, row), s.cell_px(col + 14, row))
- s.wait(lambda: len(s.panes()) == 3, 'main.c to open')
- s.glide(*s.cell_px(40, 9), seconds=.5)
- s.shot(2.6, poster=True)
+ s.click(3, *s.cell_px(col + 4, row), done=lambda: any(line.startswith(' 12 ') for line in s.screen()[6:12]),
+ what='main.c at line 12', after=2.8)
+ s.shots[-1]['poster'] = True
def shell_setup(s):
@@ -543,32 +589,273 @@ def diff_review(s):
s.wait_text('+ printf')
s.shot(1.4)
col, row = s.find('+ printf', 0)
- s.sweep(3, s.cell_px(col, row), s.cell_px(col, row))
- s.wait(lambda: len(s.panes()) == 3, 'main.c to open')
- s.glide(*s.cell_px(42, 9), seconds=.5)
- s.shot(2.6, poster=True)
+ s.click(3, *s.cell_px(col, row), done=lambda: len(s.panes()) == 3, what='main.c to open', after=2.6)
+ s.shots[-1]['poster'] = True
+
+
+def mandelbrot(path, width=1200, height=800):
+ """The classic view of the Mandelbrot set, smoothly coloured."""
+ import numpy as np
+ x = np.linspace(-2.35, 0.85, width)
+ y = np.linspace(-1.0667, 1.0667, height)
+ c = x[None, :] + 1j * y[:, None]
+ z = np.zeros_like(c)
+ count = np.zeros(c.shape)
+ alive = np.ones(c.shape, bool)
+ limit = 200
+ for n in range(limit):
+ z[alive] = z[alive] ** 2 + c[alive]
+ escaped = alive & (np.abs(z) > 4)
+ # The smooth (normalised) iteration count, so the bands blend.
+ count[escaped] = n + 1 - np.log2(np.log2(np.abs(z[escaped])))
+ alive &= ~escaped
+ t = np.sqrt(np.clip(count / 40, 0, 1))
+ # A deep blue through gold to white, the inside black.
+ stops = np.array([[0, 7, 100], [32, 107, 203], [237, 255, 255], [255, 170, 0], [80, 2, 0]], float)
+ pos = np.linspace(0, 1, len(stops))
+ rgb = np.stack([np.interp(t, pos, stops[:, k]) for k in range(3)], axis=-1)
+ rgb[alive] = 0
+ Image.fromarray(rgb.astype(np.uint8)).save(path)
+
+
+def image_setup(s):
+ """A lone terminal at the project, which holds a picture."""
+ mandelbrot(PROJECT / 'mandelbrot.png')
+ shell_setup(s)
+ s.write('/pane/1/exec', 'Del\n')
+ s.wait(lambda: list(s.panes()) == [2], 'the README to close')
+
+
+def image_pane(s):
+ """ls in a shell; B3 on the PNG's name opens it in a pane."""
+ s.pointer = s.cell_px(40, 8)
+ s.shot(1.0)
+ s.type('ls', lambda typed: at_prompt(s, typed)())
+ s.keys('\r')
+ s.wait(lambda: any('mandelbrot.png' in line for line in s.terminal_rows()), 'ls')
+ s.shot(.9)
+ col, row = s.find('mandelbrot.png', 0)
+ s.click(3, *s.cell_px(col + 5, row), done=lambda: len(s.panes()) == 2, what='the picture', after=2.2)
+ s.shots[-1]['poster'] = True
+ image = next(n for n, kind in s.panes().items() if kind == 'image')
+ # Petscii, a word typed into the picture's tag, redraws it as a C64's
+ # character art would.
+ s.write(f'/pane/{image}/tag', ' Petscii')
+ s.click(2, *s.tag_word(image, 'Petscii'), done=lambda: 'petscii:on' in s.tag(image), what='petscii',
+ after=2.6)
+
+
+# The Intel 64 and IA-32 SDM, vol. 2 (2,515 pages), opened where it is.
+PDF = [Path('/home/goblin/01-resources/intel-devref-ii.pdf')]
+
+
+def pdf_args():
+ return [PDF[0]]
+
+
+def pdf_setup(s):
+ s.mouse('motion', 0, *s.cell_px(COLS - 2, ROWS - 1))
+
+
+def pdf_tag(s, word, pane=None):
+ """The pixel over `word` in the PDF pane's tag."""
+ pane = pane or next(n for n, kind in s.panes().items() if kind == 'pdf')
+ return s.tag_word(pane, word)
+
+
+def page_of(s, pane):
+ tag = s.tag(pane)
+ return int(tag[tag.index('[') + 1:tag.index('/', tag.index('['))])
+
+
+def pdf_navigation(s):
+ """Page through the Intel SDM, jump to CPUID from its sections, search
+ a word and step through the hits."""
+ pdf = next(n for n, kind in s.panes().items() if kind == 'pdf')
+ s.pointer = s.cell_px(44, 12)
+ s.shot(1.2)
+ s.press([('\x06', 'Ctrl-f'), ('\x06', 'Ctrl-f Ctrl-f')], per_key=.7)
+ s.click(2, *pdf_tag(s, 'PdfSections'), done=lambda: len(s.panes()) == 2, what='the sections', after=.8)
+ listing = next(n for n in s.panes() if n != pdf)
+ top = s.find('+PdfSections', 0)[1]
+ s.write('/focus', f'{listing}\n')
+ s.badge = '200G'
+ for key in '200G':
+ s.keys(key)
+ s.wait(lambda: any('CPUID' in line for line in s.screen()[top:]), 'the CPUID row')
+ s.shot(1.0)
+ s.badge = None
+ # A row's file.pdf:PAGE:SECTION is the place to look at.
+ col, row = s.find('pdf:317:200', after=top + 1)
+ s.click(3, *s.cell_px(col + 4, row), done=lambda: page_of(s, pdf) == 317, what='page 317', after=1.2)
+ s.click(2, *s.tag_word(listing, 'Del'), done=lambda: list(s.panes()) == [pdf], what='the listing to close',
+ after=.8)
+ s.write('/focus', f'{pdf}\n')
+ s.badge = '/'
+ s.keys('/')
+ s.shot(.25)
+ # Its first hits are on the pages after CPUID's first.
+ s.type('apic id', lambda typed: True, per_key=.08)
+ s.badge = '/apic id Enter'
+ s.keys('\r')
+ s.wait(lambda: any(kind == 'text' for kind in s.panes().values()), 'the hits')
+ s.shot(1.0)
+ s.badge = None
+ hits = next(n for n, kind in s.panes().items() if kind == 'text')
+ # The hit list folds to its tag, and the page has the column again.
+ s.click(2, *s.tag_word(hits, 'Collapse'), done=lambda: 'collapsed' in s.read(f'/pane/{hits}/ctl'),
+ what='the hits to fold', after=.8)
+ s.write('/focus', f'{pdf}\n')
+ for k in range(1, 4):
+ s.badge = ' '.join(['n'] * k)
+ s.keys('n')
+ s.shot(.8)
+ s.badge = None
+ s.shots[-1]['secs'] = 1.6
+ s.shots[-1]['poster'] = True
+
+
+def pdf_tint_setup(s):
+ """CPUID's page, and two words of the user's in the tag: PdfFit and a
+ page address (MOV's)."""
+ s.write('/pane/1/look', f'{PDF[0]}:317\n')
+ s.wait(lambda: page_of(s, 1) == 317, 'page 317')
+ s.write('/pane/1/tag', f' PdfFit {PDF[0].name}:762')
+ pdf_setup(s)
+
+
+def pdf_tint_fit(s):
+ """PdfTint through its three looks, PdfFit both ways, then a look at
+ a page address."""
+ s.pointer = s.cell_px(44, 12)
+ s.shot(1.2)
+ for _ in range(3):
+ before = s.read('/pane/1/ctl').split()[-1]
+ s.click(2, *pdf_tag(s, 'PdfTint'), done=lambda: s.read('/pane/1/ctl').split()[-1] != before,
+ what='the tint', after=1.0)
+ for fit in ('height', 'width'):
+ s.click(2, *pdf_tag(s, 'PdfFit'), done=lambda: f'fit:{fit}' in s.read('/pane/1/ctl'), what='the fit',
+ after=1.3)
+ s.click(3, *pdf_tag(s, f'{PDF[0].name}:762'), done=lambda: page_of(s, 1) == 762, what='page 762', after=2.0)
+ s.shots[-1]['poster'] = True
+
+
+THEMES = MEDIA / 'themes'
+THEME_WIDTH = 640
+THEME_MAX_BYTES = 120_000
+
+
+def theme_scene(s):
+ """One column: a C file with its syntax colours and a selection, a
+ shell that ran make (gcc's colours), and git diff paged into +Pager."""
+ env = dict(clean_env(), HOME=str(HOME), GIT_AUTHOR_NAME='demo', GIT_AUTHOR_EMAIL='[email protected]',
+ GIT_COMMITTER_NAME='demo', GIT_COMMITTER_EMAIL='[email protected]')
+ for args in (['init', '-q'], ['add', '.'], ['commit', '-qm', 'hello'], ['config', 'diff.context', '1']):
+ subprocess.run(['git', '-C', str(PROJECT), *args], check=True, env=env, capture_output=True)
+ main = PROJECT / 'src/main.c'
+ main.write_text(main.read_text().replace('i <= 4', 'i <= 5'))
+ s.write('/pane/1/exec', 'Tty\n')
+ s.wait(lambda: 2 in s.panes(), 'the terminal')
+ s.write('/pane/2/pty/data', f"cd {PROJECT}; PS1='$ '; clear; make; git diff\r")
+ s.wait(lambda: len(s.panes()) == 3, 'the pager')
+ s.wait(lambda: 'Error 1' in s.read('/pane/2/body'), 'make')
+ s.write('/pane/1/dot', '12\n')
+ # The pager shows its hunk, the end of the diff.
+ pager = next(n for n, kind in s.panes().items() if n not in (1, 2))
+ s.write(f'/pane/{pager}/dot', '$\n')
+
+
+def theme_order(s):
+ """The themes as the Themes builtin lists them, up to the imported
+ ones: pardes's own, the ports and the legacy pair (src/themes)."""
+ s.write('/ctl', 'Themes\n')
+ s.wait(lambda: any(s.read(f'/pane/{n}/name').strip().endswith('+Themes') for n in s.panes()), '+Themes')
+ pane = next(n for n in s.panes() if s.read(f'/pane/{n}/name').strip().endswith('+Themes'))
+ names = []
+ for line in s.read(f'/pane/{pane}/body').splitlines():
+ if line.startswith('# Imported'):
+ break
+ if line.startswith('Theme '):
+ names.append(line.split()[1])
+ s.write(f'/pane/{pane}/ctl', 'Del\n')
+ s.wait(lambda: pane not in s.panes(), '+Themes to close')
+ return names
+
+
+def save_theme_png(data, path):
+ """The capture at THEME_WIDTH, as small a PNG as reads: full colour if it
+ fits THEME_MAX_BYTES, else 256 colours. Answers light or dark, from the
+ colour most of the window is."""
+ import io
+ img = Image.open(io.BytesIO(data)).convert('RGB')
+ ground = max(img.getcolors(1 << 24), key=lambda c: c[0])[1]
+ lum = sum(w * (v / 255) ** 2.2 for w, v in zip((.2126, .7152, .0722), ground))
+ small = img.resize((THEME_WIDTH, round(img.height * THEME_WIDTH / img.width)), Image.LANCZOS)
+ small.save(path, optimize=True)
+ if path.stat().st_size > THEME_MAX_BYTES:
+ small.quantize(256, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE).save(path, optimize=True)
+ return 'light' if lum > .18 else 'dark'
+
+
+def record_themes(binary, work):
+ """docs/site/media/themes/<theme>.png for every theme of src/themes, in
+ the Themes builtin's order, and themes.toml listing them."""
+ make_project()
+ s = Session(binary, work / 'themes', 'rec-themes', [PROJECT / 'src/main.c'], rows=22)
+ try:
+ theme_scene(s)
+ names = theme_order(s)
+ THEMES.mkdir(parents=True, exist_ok=True)
+ rows = []
+ for name in names:
+ s.write('/ctl', f'Theme {name}\n')
+ # A pointer move puts away the word's flash that a command
+ # leaves up until the pointer moves.
+ s.mouse('motion', 0, *s.cell_px(COLS - 2, 1))
+ s.mouse('motion', 0, *s.cell_px(COLS - 3, 1))
+ kind = save_theme_png(s.settle(), THEMES / f'{name}.png')
+ rows.append((name, kind))
+ print(f'{name}.png {kind} {(THEMES / f"{name}.png").stat().st_size / 1e3:.0f} KB', flush=True)
+ finally:
+ s.close()
+ toml = ['# The Themes page gallery, in the order the Themes builtin walks them:',
+ '# media/themes/<file> for each, made by test/record_demos.py themes.',
+ 'ready = true', '']
+ for name, kind in rows:
+ toml += ['[[theme]]', f'name = "{name}"', f'file = "{name}.png"', f'kind = "{kind}"', '']
+ (THEMES / 'themes.toml').write_text('\n'.join(toml))
CLIPS = {
'text-is-the-interface': dict(
- run=text_is_the_interface, args=[PROJECT / 'README'],
- caption='Middle-click runs a word; right-click an error opens the file.'),
+ setup=hero_setup, run=text_is_the_interface, args=[PROJECT / 'README'],
+ caption='Middle-click runs a word; right-click an error opens its line.'),
'terminals-are-panes': dict(
setup=shell_setup, run=terminals_are_panes, args=[PROJECT / 'src/main.c'],
- caption='A shell is a pane: Ctrl-b, then move over its output.'),
+ caption='A terminal is a pane: Ctrl-b, then move over its output.'),
'scripting-over-9p': dict(
setup=mounted_shell_setup, run=scripting_over_9p, args=[PROJECT / 'README'],
- caption='The session is files: a shell script reads and writes its panes.'),
+ caption='A script drives the session through its virtual filesystem (9P).'),
'diff-review': dict(
setup=diff_setup, run=diff_review, args=[PROJECT / 'README'],
- caption='git diff pages into a pane; right-click a change to open it.'),
+ caption='Right-click a diff line to open that line of the file.'),
+ 'pdf-navigation': dict(
+ setup=pdf_setup, run=pdf_navigation, args=pdf_args,
+ caption='A 2,515-page PDF: page, jump to CPUID, search, step hits.'),
+ 'pdf-tint-fit': dict(
+ setup=pdf_tint_setup, run=pdf_tint_fit, args=pdf_args,
+ caption='PdfTint tints the page, PdfFit fits it, a page address jumps.'),
+ 'image-pane': dict(
+ setup=image_setup, run=image_pane, args=[PROJECT / 'README'],
+ caption="Right-click a picture's name to open it; Petscii redraws it."),
}
def record(binary, work, name):
clip = CLIPS[name]
make_project()
- s = Session(binary, work / name, 'rec-' + name, clip['args'], init=clip.get('init', ''))
+ args = clip['args']() if callable(clip['args']) else clip['args']
+ s = Session(binary, work / name, 'rec-' + name, args, init=clip.get('init', ''))
try:
if 'setup' in clip:
clip['setup'](s)
@@ -586,19 +873,26 @@ def record(binary, work, name):
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('binary', type=Path, help='a pardes-gui built with -Dplatform=gui')
- parser.add_argument('clips', nargs='*', help='which clips (default: all): ' + ', '.join(CLIPS))
+ parser.add_argument('clips', nargs='*',
+ help='which clips (default: all), or themes for the theme gallery: ' + ', '.join(CLIPS))
parser.add_argument('--work', type=Path, default=REPO.parent / '.scratch/rec/work',
help='frames and intermediates (large; default %(default)s)')
parser.add_argument('--theme', default=THEME[0], help='a theme name, or default (default %(default)s)')
+ parser.add_argument('--pdf', type=Path, default=PDF[0],
+ help='the PDF the pdf clips page through, opened read-only (default %(default)s)')
args = parser.parse_args()
THEME[0] = args.theme
+ PDF[0] = args.pdf.resolve()
names = args.clips or list(CLIPS)
for name in names:
- if name not in CLIPS:
+ if name not in CLIPS and name != 'themes':
parser.error(f'no clip {name!r}: ' + ', '.join(CLIPS))
try:
for name in names:
- record(args.binary, args.work.resolve(), name)
+ if name == 'themes':
+ record_themes(args.binary, args.work.resolve())
+ else:
+ record(args.binary, args.work.resolve(), name)
finally:
shutil.rmtree(PROJECT, ignore_errors=True)
shutil.rmtree(HOME, ignore_errors=True)
@@ -606,7 +900,8 @@ def main():
lines = dict(line.split(': ', 1) for line in captions.read_text().splitlines() if ': ' in line) \
if captions.exists() else {}
for name in names:
- lines[name] = CLIPS[name]['caption']
+ if name in CLIPS:
+ lines[name] = CLIPS[name]['caption']
captions.write_text(''.join(f'{name}: {lines[name]}\n' for name in CLIPS if name in lines))