summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 14:04:47 -0300
committerGabriel Schneider <[email protected]>2026-10-01 14:04:47 -0300
commit54948bf22fb891bd31fcd5dc33e0bce7552fe1d4 (patch)
tree180f523481936626607d7da8c44bdcd231c896e0
parentbf3ab868aa9092d90bdc318e6bac8d92044e5c2c (diff)
downloadpardes-54948bf22fb891bd31fcd5dc33e0bce7552fe1d4.tar.gz
pardes-54948bf22fb891bd31fcd5dc33e0bce7552fe1d4.zip
A chords clip for the guide: B1 sweeps and stays down, B2 cuts, B3 pastes, B2 then B3 copies, every held button drawn
record_demos.py chords: a small notes file; each chord glides, stops, presses B1 and keeps it down while B2 or B3 clicks. The pointer wears a ring per button down and a mouse whose held buttons are lit, named "left held + middle-click" and the like; B1 is drawn blue. clips.toml lists it with landing = false, for the guide to embed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/site/media/captions.txt1
-rw-r--r--docs/site/media/chords.mp4bin0 -> 150705 bytes
-rw-r--r--docs/site/media/chords.pngbin0 -> 33061 bytes
-rw-r--r--docs/site/media/chords.webmbin0 -> 139047 bytes
-rw-r--r--docs/site/media/clips.toml6
-rw-r--r--test/record_demos.py143
6 files changed, 141 insertions, 9 deletions
diff --git a/docs/site/media/captions.txt b/docs/site/media/captions.txt
index e4370744..6a1a2274 100644
--- a/docs/site/media/captions.txt
+++ b/docs/site/media/captions.txt
@@ -5,3 +5,4 @@ text-is-the-interface: Middle-click make in the README; right-click the error.
terminals-are-panes: A terminal is a pane: Ctrl-b, then move over its output.
scripting-over-9p: A script drives the session through its virtual filesystem (9P).
diff-review: Right-click a diff line to open that line of the file.
+chords: Hold left, click middle to cut; click right to paste.
diff --git a/docs/site/media/chords.mp4 b/docs/site/media/chords.mp4
new file mode 100644
index 00000000..43bc556b
--- /dev/null
+++ b/docs/site/media/chords.mp4
Binary files differ
diff --git a/docs/site/media/chords.png b/docs/site/media/chords.png
new file mode 100644
index 00000000..a140347c
--- /dev/null
+++ b/docs/site/media/chords.png
Binary files differ
diff --git a/docs/site/media/chords.webm b/docs/site/media/chords.webm
new file mode 100644
index 00000000..42a6f4e6
--- /dev/null
+++ b/docs/site/media/chords.webm
Binary files differ
diff --git a/docs/site/media/clips.toml b/docs/site/media/clips.toml
index ca3002d0..11f6e8f3 100644
--- a/docs/site/media/clips.toml
+++ b/docs/site/media/clips.toml
@@ -39,3 +39,9 @@ ready = true
name = "diff-review"
caption = "Right-click a diff line to open that line of the file."
ready = true
+
+[[clip]]
+name = "chords"
+caption = "Hold left, click middle to cut; click right to paste."
+ready = true
+landing = false
diff --git a/test/record_demos.py b/test/record_demos.py
index 3cc07f15..7ab22d95 100644
--- a/test/record_demos.py
+++ b/test/record_demos.py
@@ -110,6 +110,7 @@ def make_project():
(PROJECT / 'src/main.c').write_text(MAIN_C)
(PROJECT / 'Makefile').write_text(MAKEFILE)
(PROJECT / 'README').write_text(README)
+ (PROJECT / 'notes.txt').write_text(NOTES)
class Session:
@@ -157,6 +158,7 @@ class Session:
self.pointer = None # the drawn pointer's (x, y), None for none
self.button = None # the SDL button held, drawn as a ring and its name
self.badge = None # a key or two, drawn in the corner
+ self.held = [] # the buttons a chord holds, each drawn as a ring and on a mouse
self.settle()
def _drain(self):
@@ -309,16 +311,43 @@ class Session:
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, ring=ring,
- ring_button=ring_button or self.button))
+ ring_button=ring_button or self.button, held=tuple(self.held)))
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, ring=None))
+ self.shots.append(dict(last, secs=seconds, start=self.pointer, end=(x, y), poster=False, ring=None,
+ held=tuple(self.held)))
self.pointer = (x, y)
- self.mouse('motion', self.button or 0, x, y)
+ self.mouse('motion', self.button or (self.held[0] if self.held else 0), x, y)
+
+ def chord_press(self, x, y, rest=.4, hold=.5):
+ """Glide to (x, y), stop, and press B1 there, holding it."""
+ if self.pointer != (x, y):
+ self.glide(x, y)
+ self.shot(rest)
+ self.mouse('down', 1, x, y)
+ self.held = [1]
+ self.shot(hold)
+
+ def chord_click(self, button, done, what, hold=.45, after=1.0):
+ """With B1 held and the pointer still, click `button`: both rings
+ and the mouse's two buttons lit on the press, then a pulse."""
+ self.mouse('down', button, *self.pointer)
+ self.held.append(button)
+ self.shot(hold, ring='press', ring_button=button)
+ self.mouse('up', button, *self.pointer)
+ self.held.remove(button)
+ self.wait(done, what)
+ self.shot(after, ring='pulse', ring_button=button)
+
+ def chord_release(self, after=.6):
+ """Let B1 go, the pointer still."""
+ self.mouse('up', 1, *self.pointer)
+ self.held = []
+ self.shot(after)
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
@@ -368,8 +397,9 @@ def pointer_sprite(scale):
return im
-# acme's colours for the buttons: B2 red, B3 green.
-BUTTONS = {1: ('click', (70, 70, 70)), 2: ('middle-click', (170, 0, 0)), 3: ('right-click', (0, 110, 40))}
+# acme's colours for the buttons: B2 red, B3 green; B1, which acme leaves
+# black, is blue here so a held left button shows on the mouse.
+BUTTONS = {1: ('click', (30, 80, 190)), 2: ('middle-click', (170, 0, 0)), 3: ('right-click', (0, 110, 40))}
def draw_badge(img, text, xy, font, fill=(40, 40, 40), anchor='lt'):
@@ -386,7 +416,50 @@ class Painter:
self.font = ImageFont.truetype(str(BADGE_FONT), 24)
self.size = size
- def paint(self, img, pointer, badge, ring=None, button=None, since=0.0):
+ def chord(self, img, x, y, held, ring, button, since):
+ """A chord: a ring for every button down (left inside, middle,
+ right outside), a mouse whose held buttons are lit, and its name."""
+ w, h = self.size
+ d = ImageDraw.Draw(img, 'RGBA')
+ radius = {1: 15, 2: 23, 3: 31}
+ for b in held:
+ r = radius[b]
+ d.ellipse((x - r, y - r, x + r, y + r), outline=BUTTONS[b][1] + (255,), width=4)
+ clicked = [b for b in held if b != 1]
+ if ring == 'pulse' and button and button not in held:
+ t = min(since / .45, 1)
+ r = radius[button] + 18 * t
+ if t < 1:
+ d.ellipse((x - r, y - r, x + r, y + r), outline=BUTTONS[button][1] + (int(255 * (1 - t)),), width=4)
+ if since < .7:
+ clicked = [button]
+ lit = set(held) | set(clicked)
+ names = {2: 'middle', 3: 'right'}
+ label = 'left held' + ''.join(f' + {names[b]}-click' for b in clicked)
+ width = self.font.getlength(label) + 14
+ # The mouse, below and right of the pointer (else left of it): three
+ # buttons over a body, the held ones lit, the name beside it.
+ mx, my, mw, mh = x + 40, y + 30, 52, 74
+ if my + mh + 8 > h:
+ my = y - 34 - mh
+ if mx + mw + 12 + width + 16 > w:
+ mx = x - 40 - mw
+ d.rounded_rectangle((mx, my, mx + mw, my + mh), radius=18, fill=(250, 250, 250, 245), outline=(30, 30, 30, 255),
+ width=3)
+ third = mw / 3
+ for b in (1, 2, 3):
+ box = (mx + third * (b - 1) + 3, my + 3, mx + third * b - 3, my + mh * .42)
+ if b in lit:
+ d.rectangle(box, fill=BUTTONS[b][1] + (255,))
+ d.line((mx + 3, my + mh * .42 + 2, mx + mw - 3, my + mh * .42 + 2), fill=(30, 30, 30, 255), width=2)
+ for b in (1, 2):
+ lx = mx + third * b
+ d.line((lx, my + 3, lx, my + mh * .42), fill=(30, 30, 30, 255), width=2)
+ colour = BUTTONS[clicked[-1]][1] if clicked else BUTTONS[1][1]
+ lx = mx + mw + 12 if mx > x else mx - 12 - width
+ draw_badge(img, label, (lx, my + mh / 2), self.font, fill=colour, anchor='lm')
+
+ def paint(self, img, pointer, badge, ring=None, button=None, since=0.0, held=()):
"""`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)."""
@@ -396,7 +469,9 @@ class Painter:
draw_badge(img, badge, (w - 16, h - 16), self.font, anchor='rb')
if pointer is not None:
x, y = pointer
- if ring and button:
+ if held:
+ self.chord(img, x, y, held, ring, button, since)
+ elif ring and button:
label, colour = BUTTONS[button]
d = ImageDraw.Draw(img, 'RGBA')
if ring == 'press':
@@ -446,10 +521,13 @@ def compose(shots, out):
for i in range(n):
t = ease((i + 1) / n)
frame = painter.paint(img, lerp(shot['start'], shot['end'], t), shot['badge'], shot['ring'],
- shot['ring_button'], i / FPS)
+ shot['ring_button'], i / FPS, shot.get('held', ()))
ff.stdin.write(frame.tobytes())
if shot['poster'] and poster is None:
- poster = painter.paint(img, shot['end'], None)
+ # A chord's still keeps its rings and mouse; a click's has none.
+ chord = shot.get('held') or ()
+ poster = painter.paint(img, shot['end'], None, shot['ring'] if chord else None, shot['ring_button'], 0.0,
+ chord)
emitted += n
last = shots[-1]
n = round(.7 * FPS)
@@ -837,6 +915,50 @@ def record_themes(binary, work):
(THEMES / 'themes.toml').write_text('\n'.join(toml))
+NOTES = '''To buy: apples plums pears
+
+
+
+In the bowl: bananas
+'''
+
+
+def chords_setup(s):
+ s.mouse('motion', 0, *s.cell_px(COLS - 2, ROWS - 1))
+
+
+def body_text(s):
+ return s.read('/pane/1/body')
+
+
+def chords(s):
+ """acme's chords: B1 sweeps and stays down; B2 cuts, B3 pastes, B2 then
+ B3 copies."""
+ s.pointer = s.cell_px(40, 12)
+ s.shot(.8)
+ # Cut: sweep " plums", B2 with B1 still down.
+ col, row = s.find(' plums', 0)
+ s.chord_press(*s.cell_px(col, row), rest=.25, hold=.3)
+ s.glide(*s.cell_px(col + 7, row), seconds=.45)
+ s.shot(.3)
+ s.chord_click(2, lambda: 'plums' not in body_text(s), 'the cut', after=.75)
+ s.shots[-2]['poster'] = True
+ s.chord_release(after=.25)
+ # Paste after "bananas": B1 down there, B3.
+ col, row = s.find('bananas', 0)
+ s.chord_press(*s.cell_px(col + 7, row), rest=.25, hold=.3)
+ s.chord_click(3, lambda: 'bananas plums' in body_text(s), 'the paste', after=.8)
+ s.chord_release(after=.25)
+ # Copy: sweep "apples", B2 then B3 with B1 down: it is back, and kept.
+ col, row = s.find('apples', 0)
+ s.chord_press(*s.cell_px(col, row), rest=.25, hold=.3)
+ s.glide(*s.cell_px(col + 6, row), seconds=.45)
+ s.shot(.3)
+ s.chord_click(2, lambda: 'apples' not in body_text(s), 'the copy\'s cut', after=.45)
+ s.chord_click(3, lambda: 'apples' in body_text(s), 'the copy\'s paste', after=.8)
+ s.chord_release(after=1.0)
+
+
CLIPS = {
'pdf-navigation': dict(
setup=pdf_setup, run=pdf_navigation, args=pdf_args,
@@ -859,6 +981,9 @@ CLIPS = {
'diff-review': dict(
setup=diff_setup, run=diff_review, args=[PROJECT / 'README'],
caption='Right-click a diff line to open that line of the file.'),
+ 'chords': dict(
+ setup=chords_setup, run=chords, args=[PROJECT / 'notes.txt'],
+ caption='Hold left, click middle to cut; click right to paste.'),
}