diff options
| author | Gabriel Schneider <[email protected]> | 2026-10-01 12:42:48 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 13:04:28 -0300 |
| commit | f6a511c61d7ef2ff680f70077b236559a595be69 (patch) | |
| tree | 496144bde72ee3d8cad0d84ab1ff110f65b0308d /test/record_demos.py | |
| parent | 0892e3aa5cd46e43b0bb0ac10f47d2ee4f500409 (diff) | |
| download | pardes-f6a511c61d7ef2ff680f70077b236559a595be69.tar.gz pardes-f6a511c61d7ef2ff680f70077b236559a595be69.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 theme
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/record_demos.py')
| -rw-r--r-- | test/record_demos.py | 407 |
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)) |
