summaryrefslogtreecommitdiff
path: root/test/macos-snapshots
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-11 15:58:58 -0300
committerGabriel Schneider <[email protected]>2026-08-11 16:26:20 -0300
commit4ca28745d774c232cd31a29c17878f19bbe24cf5 (patch)
treeface852acae5bc347e6bab2bb5cede501e0ce1d3 /test/macos-snapshots
parentdedfdea43f0d6c7151c541284c81027969d89032 (diff)
parent89d93d5e7348304bc7d8a148f9ad9c1beb200459 (diff)
downloadpardes-4ca28745d774c232cd31a29c17878f19bbe24cf5.tar.gz
pardes-4ca28745d774c232cd31a29c17878f19bbe24cf5.zip
merge the macOS app branch: the AppKit shell, pixel attachments, live theming, and mupdf -Djpx
Three commits off 38e9919 (macos-app@upstream) merged into main's ghostty bump. No textual conflicts, and two things the merge needed: - nested.zig asked libc for fstatat. Darwin has it; on linux std.c declares it `void` (glibc hides it behind a versioned symbol std cannot name), so the tty build stopped at 'type void not a function'. statNoFollow keeps fstatat on darwin and asks statx on linux for the same three fields, which is what this file did before the branch generalized it to both platforms. - .DS_Store rode along with a797a1a. Deleted, and .gitignore now says so. linux: snap 86/86, unit-test, image-harness and mupdf-check green. nested.zig also type-checks for aarch64-macos.
Diffstat (limited to 'test/macos-snapshots')
-rw-r--r--test/macos-snapshots/boot.golden58
-rw-r--r--test/macos-snapshots/boot.snap23
-rw-r--r--test/macos-snapshots/cwd.golden100
-rw-r--r--test/macos-snapshots/cwd.snap32
-rw-r--r--test/macos-snapshots/drop.golden94
-rw-r--r--test/macos-snapshots/drop.snap37
-rw-r--r--test/macos-snapshots/font.golden176
-rw-r--r--test/macos-snapshots/font.snap49
-rw-r--r--test/macos-snapshots/keys.golden325
-rw-r--r--test/macos-snapshots/keys.snap59
-rw-r--r--test/macos-snapshots/rotate.golden280
-rw-r--r--test/macos-snapshots/rotate.snap107
-rw-r--r--test/macos-snapshots/trackpad.golden311
-rw-r--r--test/macos-snapshots/trackpad.snap91
14 files changed, 1742 insertions, 0 deletions
diff --git a/test/macos-snapshots/boot.golden b/test/macos-snapshots/boot.golden
new file mode 100644
index 00000000..1c971b3f
--- /dev/null
+++ b/test/macos-snapshots/boot.golden
@@ -0,0 +1,58 @@
+== snap boot grid=80x24 cursor=4,2
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/boot/cwd New Del
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== draw boot 680x396 nonblank
+== snap resized grid=100x30 cursor=4,2
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/boot/cwd New Del
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== draw resized 850x495 nonblank
diff --git a/test/macos-snapshots/boot.snap b/test/macos-snapshots/boot.snap
new file mode 100644
index 00000000..e3d4ba14
--- /dev/null
+++ b/test/macos-snapshots/boot.snap
@@ -0,0 +1,23 @@
+# The whole boot path in one script: pardes_init at the grid the offscreen
+# window actually measured, the first shell reaching its prompt, and the
+# CoreText pass painting. `draw` is the only command in the suite that touches
+# the drawing code at all — everything else reads the core's cell buffer, which
+# a draw(_:) that returned on its first line would leave perfectly intact.
+#
+# The macOS backend takes no argv, so it always boots the way a bare
+# `start 30 140` does in test/snapshots: one terminal pane in raw tty mode,
+# prompt visible. `$ ` is the prompt the hermetic .bashrc pins, and no chrome
+# row carries a `$`, so waiting on one is waiting on the shell.
+start 24 80
+wait 8000 New Newcol
+wait 8000 $
+stable 700 20000
+snap boot
+draw boot
+# The window changing size is the other half of the boot contract: the core
+# reflows, the view re-measures, and the second `draw` proves the new size is
+# the size that actually got painted.
+resize 30 100
+stable 700 20000
+snap resized
+draw resized
diff --git a/test/macos-snapshots/cwd.golden b/test/macos-snapshots/cwd.golden
new file mode 100644
index 00000000..748d523d
--- /dev/null
+++ b/test/macos-snapshots/cwd.golden
@@ -0,0 +1,100 @@
+== snap spawned grid=100x24 cursor=4,2
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/cwd/cwd New Del
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap moved grid=100x24 cursor=4,3
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp New Del
+| $ cd /tmp
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap moved-again grid=100x24 cursor=4,4
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ / New Del
+| $ cd /tmp
+| $ cd /
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap idle grid=100x24 cursor=4,4
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ / New Del
+| $ cd /tmp
+| $ cd /
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
diff --git a/test/macos-snapshots/cwd.snap b/test/macos-snapshots/cwd.snap
new file mode 100644
index 00000000..87f6f9ef
--- /dev/null
+++ b/test/macos-snapshots/cwd.snap
@@ -0,0 +1,32 @@
+# The pane tag has to follow the shell around, because a relative `Look` is
+# resolved against it: with a stale cwd, clicking `README.md` after a `cd`
+# looks for it in the directory the pane was SPAWNED in and finds nothing.
+#
+# On linux the host re-reads /proc/<pid>/cwd every frame. macOS asks libproc,
+# and asks it only for a pane that just produced output — a `cd` is a command
+# and a shell that ran a command prints its next prompt, so nothing else can
+# have moved one. This script is what proves that gating is not too clever:
+# the tag must be right AFTER the cd and must stay right when nothing happens.
+start 24 100
+wait 8000 $
+stable 700 20000
+# The hermetic world puts the shell in <base>/work, so the boot tag names it
+# and no absolute path from this machine can leak into the golden.
+snap spawned
+# `cd` writes nothing but the next prompt: the one and only signal the lazy
+# refresh gets. Two of them, because the second has to survive the first
+# having already cleared the flag.
+text cd /tmp
+key enter
+wait 8000 $
+stable 700 20000
+snap moved
+text cd /
+key enter
+wait 8000 $
+stable 700 20000
+snap moved-again
+# ...and an idle pass must not lose it again: the refresh clears its own flag,
+# so a tick with no output must leave the tag exactly where it was.
+stable 700 20000
+snap idle
diff --git a/test/macos-snapshots/drop.golden b/test/macos-snapshots/drop.golden
new file mode 100644
index 00000000..b4143983
--- /dev/null
+++ b/test/macos-snapshots/drop.golden
@@ -0,0 +1,94 @@
+== snap booted grid=100x30 cursor=4,3
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/drop/cwd New Del
+| $ printf 'first file\n' > one.txt; printf 'second file\n' > two.txt
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap dropped_on_shell grid=100x30 cursor=7,2
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/drop/cwd/one.txt S$ /private/tmp/pardes-macos-e2e/drop/cwd New Del
+| 1 first file $ printf 'first file\n' > one.txt; printf 'secon
+| 2 d file\n' > two.txt
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap dropped_on_document grid=100x30 cursor=7,17
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/drop/cwd/one.txt S$ /private/tmp/pardes-macos-e2e/drop/cwd New Del
+| 1 first file $ printf 'first file\n' > one.txt; printf 'secon
+| 2 d file\n' > two.txt
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| /private/tmp/pardes-macos-e2e/drop/cwd/two.txt S
+| 1 second file
+| 2
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== draw drop 850x495 nonblank
diff --git a/test/macos-snapshots/drop.snap b/test/macos-snapshots/drop.snap
new file mode 100644
index 00000000..f427fe2a
--- /dev/null
+++ b/test/macos-snapshots/drop.snap
@@ -0,0 +1,37 @@
+# A file dropped ON the grid opens beside the pane it was dropped on.
+#
+# Finder and the Dock already reach the app through application(_:open:), but
+# that path cannot say WHERE — it opens next to whichever pane happened to have
+# focus. A drop knows where the hand was, and in acme that is the whole
+# difference: `Look` places the document relative to the pane it runs in.
+#
+# So a drop is defined as a CLICK followed by `Look`, and nothing more. The
+# click focuses the pane under the pointer exactly as a left click there would,
+# then the ordinary builtin runs in it. There is no drop concept in the core
+# and none was added — everything here the hand could have done itself.
+#
+# `drop` drives PardesView.drop, one call below performDragOperation, because
+# NSDraggingInfo is a protocol with no public conformer: a test that had to
+# build one would be testing its own stub. See the trackpad scripts for the
+# same reasoning about NSTouch and pressure stages.
+start 30 100
+wait 8000 $
+stable 700 20000
+text printf 'first file\n' > one.txt; printf 'second file\n' > two.txt
+key enter
+wait 8000 $
+stable 700 20000
+snap booted
+# Dropped on the shell: one.txt opens as a document beside it. Absolute,
+# because the core resolves a relative Look against the PANE's directory —
+# the same absolute path the AppKit side builds out of an NSURL.
+drop /private/tmp/pardes-macos-e2e/drop/cwd/one.txt 10 6
+stable 700 15000
+snap dropped_on_shell
+# ...and now the placement is the assertion. Dropping the SECOND file inside
+# the pane the first one opened puts it next to that pane, not next to the
+# shell — which is only true because the click went where the pointer was.
+drop /private/tmp/pardes-macos-e2e/drop/cwd/two.txt 10 20
+stable 700 15000
+snap dropped_on_document
+draw drop
diff --git a/test/macos-snapshots/font.golden b/test/macos-snapshots/font.golden
new file mode 100644
index 00000000..759e7738
--- /dev/null
+++ b/test/macos-snapshots/font.golden
@@ -0,0 +1,176 @@
+== snap boot grid=100x24 cursor=4,2
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/font/cwd New Del
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap menlo grid=100x24 cursor=4,2
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/font/cwd New Del
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap unchanged grid=100x24 cursor=4,2
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/font/cwd New Del
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap bigger grid=77x18 cursor=4,2
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/font/cwd New Del
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap reset grid=100x24 cursor=4,2
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/font/cwd New Del
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap floor grid=242x56 cursor=4,2
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/font/cwd New Del
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
diff --git a/test/macos-snapshots/font.snap b/test/macos-snapshots/font.snap
new file mode 100644
index 00000000..71049b55
--- /dev/null
+++ b/test/macos-snapshots/font.snap
@@ -0,0 +1,49 @@
+# The font picker and the zoom, which are the two things this shell decides
+# entirely on its own. Neither is visible in a cell buffer — the core has no
+# font and no idea what a point is — so what a snapshot CAN see is the second
+# effect of both: a cell that changed size fits a different number of columns
+# into the same window, and the core reflows to it.
+#
+# `font` asserts the face by PostScript name, which is the only direct look at
+# the thing under test.
+start 24 100
+wait 8000 $
+stable 700 20000
+snap boot
+
+# Menlo ships with every macOS and ships inside a .ttc, so this is also the
+# end-to-end proof of the collection branch in src/fonts.zig: the core walks
+# the font directories, matches the name, hands over a path, and the host has
+# to pull face 0 out of a file holding four.
+command Font Menlo
+stable 700 20000
+font Menlo-Regular
+snap menlo
+
+# A name that resolves to nothing must leave the screen alone rather than
+# fall back to something. `Font` finds no file, sets no path, and the host is
+# never asked to do anything at all.
+command Font NoSuchFaceExistsHere
+stable 700 20000
+font Menlo-Regular
+snap unchanged
+
+# Zoom is a pure host property: no builtin, no core round trip, just a bigger
+# cell and the reflow that follows it. Four points is enough to move the grid
+# at any starting size.
+zoom 4
+stable 700 20000
+font Menlo-Regular
+snap bigger
+
+# ...and back, which must land on exactly the grid it started from — the whole
+# point of "actual size" being a fixed number rather than an undo stack.
+zoom reset
+stable 700 20000
+snap reset
+
+# Small enough to be worth checking the clamp does not run away: the range
+# floor is 6pt, so this asks for far past it and must simply stop.
+zoom -40
+stable 700 20000
+snap floor
diff --git a/test/macos-snapshots/keys.golden b/test/macos-snapshots/keys.golden
new file mode 100644
index 00000000..cff9484e
--- /dev/null
+++ b/test/macos-snapshots/keys.golden
@@ -0,0 +1,325 @@
+== snap typed grid=80x24 cursor=16,2
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/keys/cwd New Del
+| $ echo al''pha
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap ran grid=80x24 cursor=4,4
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/keys/cwd New Del
+| $ echo al''pha
+| alpha
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap fixed grid=80x24 cursor=16,4
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/keys/cwd New Del
+| $ echo al''pha
+| alpha
+| $ echo bra''vo
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap second grid=80x24 cursor=4,6
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+|$ /private/tmp/pardes-macos-e2e/keys/cwd New Del
+| $ echo al''pha
+| alpha
+| $ echo bra''vo
+| bravo
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap normal grid=80x24 cursor=4,6
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/keys/cwd New Del
+|
+| alpha
+|
+| bravo
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap moved grid=80x24 cursor=7,3
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/keys/cwd New Del
+|
+| alpha
+|
+| bravo
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap home grid=80x24 cursor=2,3
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/keys/cwd New Del
+|
+| alpha
+|
+| bravo
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap end grid=80x24 cursor=6,3
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/keys/cwd New Del
+|
+| alpha
+|
+| bravo
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap pagedown grid=80x24 cursor=2,5
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/keys/cwd New Del
+|
+| alpha
+|
+| bravo
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap pageup grid=80x24 cursor=2,5
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/keys/cwd New Del
+|
+| alpha
+|
+| bravo
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap deleted grid=80x24 cursor=2,5
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/keys/cwd New Del
+|
+| alpha
+|
+| bravo
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap escaped grid=80x24 cursor=2,5
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/keys/cwd New Del
+|
+| alpha
+|
+| bravo
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap tabbed grid=80x24 cursor=2,5
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/keys/cwd New Del
+|
+| alpha
+|
+| bravo
+|
+| bash: bravo: command not found
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
diff --git a/test/macos-snapshots/keys.snap b/test/macos-snapshots/keys.snap
new file mode 100644
index 00000000..b81b88b7
--- /dev/null
+++ b/test/macos-snapshots/keys.snap
@@ -0,0 +1,59 @@
+# Both halves of pardes_key's vocabulary through PardesView.typeKey: the four
+# ASCII controls, the private-use navigation block, and a Ctrl chord. Markers
+# are split with '' so the echoed command line can never be the thing a `wait`
+# matched — the output has to be what arrived.
+start 24 80
+wait 8000 New Newcol
+wait 8000 $
+stable 700 20000
+# raw tty mode: characters and Enter travel to the shell through the pty
+text echo al''pha
+stable 400 5000
+snap typed
+key enter
+wait 8000 alpha
+stable 700 10000
+snap ran
+# backspace before Enter — readline owns the line while the pane is raw
+text echo bra''vv
+key backspace
+text o
+stable 400 5000
+snap fixed
+key enter
+wait 8000 bravo
+stable 700 10000
+snap second
+# Ctrl-b leaves raw tty mode. From here the same keys are the editor's, which
+# is the half of the keyboard the pty never sees.
+key c-b
+stable 600 8000
+snap normal
+key up up left
+stable 400 5000
+snap moved
+key home
+stable 400 5000
+snap home
+key end
+stable 400 5000
+snap end
+key pagedown
+stable 400 5000
+snap pagedown
+key pageup
+stable 400 5000
+snap pageup
+key delete
+stable 400 5000
+snap deleted
+# Escape in normal mode is Last, and with one pane there is nowhere to go —
+# which is exactly the no-op worth pinning: a regression here starts jumping.
+key escape
+stable 400 5000
+snap escaped
+# Tab falls through to Exec on the word under the cursor; parked where there is
+# no word, this pins the empty case rather than spawning anything.
+key tab
+stable 700 10000
+snap tabbed
diff --git a/test/macos-snapshots/rotate.golden b/test/macos-snapshots/rotate.golden
new file mode 100644
index 00000000..f0cc1eac
--- /dev/null
+++ b/test/macos-snapshots/rotate.golden
@@ -0,0 +1,280 @@
+== snap results grid=100x30 cursor=4,25
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/rotate/cwd New Del
+| zz
+| x MARK 14
+| zz
+| x MARK 15
+| zz
+| x MARK 16
+| zz
+| x MARK 17
+| zz
+| x MARK 18
+| zz
+| x MARK 19
+| zz
+| x MARK 20
+| zz
+| x MARK 21
+| zz
+| x MARK 22
+| zz
+| x MARK 23
+| zz
+| x MARK 24
+| zz
+|
+| /private/tmp/pardes-macos-e2e/rotate/cwd/+Search New Del
+| 1 @p0:3:3-6 x MARK 01
+| 2 @p0:5:3-6 x MARK 02
+| 3 @p0:7:3-6 x MARK 03
+== snap forward grid=100x30 cursor=7,4
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/rotate/cwd New Del
+|
+|
+| x MARK 01
+| zz
+| x MARK 02
+| zz
+| x MARK 03
+| zz
+| x MARK 04
+| zz
+| x MARK 05
+| zz
+| x MARK 06
+| zz
+| x MARK 07
+| zz
+| x MARK 08
+| zz
+| x MARK 09
+| zz
+| x MARK 10
+| zz
+| x MARK 11
+| zz
+| /private/tmp/pardes-macos-e2e/rotate/cwd/+Search New Del
+| 1 @p0:3:3-6 x MARK 01
+| 2 @p0:5:3-6 x MARK 02
+| 3 @p0:7:3-6 x MARK 03
+== snap forward2 grid=100x30 cursor=7,6
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/rotate/cwd New Del
+|
+|
+| x MARK 01
+| zz
+| x MARK 02
+| zz
+| x MARK 03
+| zz
+| x MARK 04
+| zz
+| x MARK 05
+| zz
+| x MARK 06
+| zz
+| x MARK 07
+| zz
+| x MARK 08
+| zz
+| x MARK 09
+| zz
+| x MARK 10
+| zz
+| x MARK 11
+| zz
+| /private/tmp/pardes-macos-e2e/rotate/cwd/+Search New Del
+| 1 @p0:3:3-6 x MARK 01
+| 2 @p0:5:3-6 x MARK 02
+| 3 @p0:7:3-6 x MARK 03
+== snap back grid=100x30 cursor=7,6
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/rotate/cwd New Del
+|
+|
+| x MARK 01
+| zz
+| x MARK 02
+| zz
+| x MARK 03
+| zz
+| x MARK 04
+| zz
+| x MARK 05
+| zz
+| x MARK 06
+| zz
+| x MARK 07
+| zz
+| x MARK 08
+| zz
+| x MARK 09
+| zz
+| x MARK 10
+| zz
+| x MARK 11
+| zz
+| /private/tmp/pardes-macos-e2e/rotate/cwd/+Search New Del
+| 1 @p0:3:3-6 x MARK 01
+| 2 @p0:5:3-6 x MARK 02
+| 3 @p0:7:3-6 x MARK 03
+== snap forward_by_key grid=100x30 cursor=7,8
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/rotate/cwd New Del
+|
+|
+| x MARK 01
+| zz
+| x MARK 02
+| zz
+| x MARK 03
+| zz
+| x MARK 04
+| zz
+| x MARK 05
+| zz
+| x MARK 06
+| zz
+| x MARK 07
+| zz
+| x MARK 08
+| zz
+| x MARK 09
+| zz
+| x MARK 10
+| zz
+| x MARK 11
+| zz
+| /private/tmp/pardes-macos-e2e/rotate/cwd/+Search New Del
+| 1 @p0:3:3-6 x MARK 01
+| 2 @p0:5:3-6 x MARK 02
+| 3 @p0:7:3-6 x MARK 03
+== snap placed grid=100x30 cursor=7,8
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/rotate/cwd New Del
+|
+|
+| x MARK 01
+| zz
+| x MARK 02
+| zz
+| x MARK 03
+| zz
+| x MARK 04
+| zz
+| x MARK 05
+| zz
+| x MARK 06
+| zz
+| x MARK 07
+| zz
+| x MARK 08
+| zz
+| x MARK 09
+| zz
+| x MARK 10
+| zz
+| x MARK 11
+| zz
+| /private/tmp/pardes-macos-e2e/rotate/cwd/+Search New Del
+| 1 @p0:3:3-6 x MARK 01
+| 2 @p0:5:3-6 x MARK 02
+| 3 @p0:7:3-6 x MARK 03
+== snap placed_released grid=100x30 cursor=7,8
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/rotate/cwd New Del
+|
+|
+| x MARK 01
+| zz
+| x MARK 02
+| zz
+| x MARK 03
+| zz
+| x MARK 04
+| zz
+| x MARK 05
+| zz
+| x MARK 06
+| zz
+| x MARK 07
+| zz
+| x MARK 08
+| zz
+| x MARK 09
+| zz
+| x MARK 10
+| zz
+| x MARK 11
+| zz
+| /private/tmp/pardes-macos-e2e/rotate/cwd/+Search New Del
+| 1 @p0:3:3-6 x MARK 01
+| 2 @p0:5:3-6 x MARK 02
+| 3 @p0:7:3-6 x MARK 03
+== snap thrown grid=100x30 cursor=7,16
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/rotate/cwd New Del
+|
+|
+| x MARK 01
+| zz
+| x MARK 02
+| zz
+| x MARK 03
+| zz
+| x MARK 04
+| zz
+| x MARK 05
+| zz
+| x MARK 06
+| zz
+| x MARK 07
+| zz
+| x MARK 08
+| zz
+| x MARK 09
+| zz
+| x MARK 10
+| zz
+| x MARK 11
+| zz
+| /private/tmp/pardes-macos-e2e/rotate/cwd/+Search New Del
+| 5 @p0:11:3-6 x MARK 05
+| 6 @p0:13:3-6 x MARK 06
+| 7 @p0:15:3-6 x MARK 07
+== snap thrown_coasted grid=100x30 cursor=7,22
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/rotate/cwd New Del
+| x MARK 07
+| zz
+| x MARK 08
+| zz
+| x MARK 09
+| zz
+| x MARK 10
+| zz
+| x MARK 11
+| zz
+| x MARK 12
+| zz
+| x MARK 13
+| zz
+| x MARK 14
+| zz
+| x MARK 15
+| zz
+| x MARK 16
+| zz
+| x MARK 17
+| zz
+| x MARK 18
+| zz
+| /private/tmp/pardes-macos-e2e/rotate/cwd/+Search New Del
+| 15 @p0:31:3-6 x MARK 15
+| 16 @p0:33:3-6 x MARK 16
+| 17 @p0:35:3-6 x MARK 17
+== draw rotate 850x495 nonblank
diff --git a/test/macos-snapshots/rotate.snap b/test/macos-snapshots/rotate.snap
new file mode 100644
index 00000000..1f2e4667
--- /dev/null
+++ b/test/macos-snapshots/rotate.snap
@@ -0,0 +1,107 @@
+# pardes_rotate spends a two-finger twist as the search-step keys.
+#
+# AppKit reports rotation counterclockwise-positive and the core reads clockwise
+# as `n`, so a NEGATIVE delta is the FORWARD step. That inversion is exactly the
+# kind of thing a unit test on the accumulator cannot catch and a golden can:
+# what the screen does is the assertion.
+#
+# One notch is 10 degrees (rotation_notch_degrees in src/macos.zig) and the
+# remainder is banked, which makes the pairs below exact rather than
+# approximate: -12 spends one notch and banks -2.
+start 30 100
+wait 8000 New Newcol
+wait 8000 $
+stable 700 20000
+# Twenty-four hits, not three. A coast is worth asserting only on a list long
+# enough to coast ALONG: `n` at the last match has nowhere to go, so on a
+# three-hit list the hardest possible flick and no flick at all produce the
+# same screen — which is a golden that would have passed before momentum
+# existed. printf reuses its format once per argument.
+text printf 'x MA''RK %s\nzz\n' 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
+key enter
+wait 10000 MARK 24
+stable 700 10000
+key c-b
+stable 600 8000
+# `/` types the pattern into the tag; Enter searches the pane's scrollback and
+# writes the hits into a +Search buffer, which is what n/N walk.
+text /
+stable 400 5000
+text MARK
+stable 400 5000
+key enter
+wait 10000 @p0:
+stable 700 15000
+snap results
+# A gesture beginning re-zeros the dial, so travel left over from an earlier
+# twist cannot make the first degree of this one jump a match.
+rotate 0
+# Clockwise: one notch of `n`, forward onto the first hit and selecting it.
+rotate -12
+stable 700 15000
+snap forward
+# ...and again, onto the second. Two steps out rather than one, because the
+# first hit is where the list already starts: stepping BACK from it has nowhere
+# to go, and a golden that cannot move is a golden that proves nothing about
+# which way the dial turns.
+rotate -12
+stable 700 15000
+snap forward2
+# A new gesture, because the dial banks its remainder exactly like the scroll
+# accumulator does: two 12-degree notches leave -4 behind, so reversing INSIDE
+# the same twist would have to spend that first and 12 back would move nothing.
+# That hysteresis is wanted — it is what stops a thumb resettling on a notch
+# boundary from flip-flopping between two matches — and lifting the fingers is
+# how a hand clears it.
+rotate 0
+# Counterclockwise: `N`. This must land back exactly on `forward`.
+rotate 12
+stable 700 15000
+snap back
+# And the other half of the contract: a twist IS the keystroke, so typing the
+# key the dial claims to send must go the same way the dial went. `n` here must
+# reproduce `forward2` — if the dial were sending something else, or the sign
+# were inverted, these two would part company.
+text n
+stable 700 15000
+snap forward_by_key
+# ---- momentum ----
+#
+# The dial coasts in proportion to how fast it was RELEASED, ramping up from
+# zero at the floor rather than switching on at it. Both halves of that are
+# here, and the gap argument is what makes them different: it is a real sleep
+# before the event, so libpardes measures a real speed off its clock.
+#
+# PLACED. Four degrees every 100 ms is 40 deg/s, under the 70 deg/s floor, so
+# lifting the fingers changes nothing at all — the twist itself has moved one
+# notch and that is where it stops. This is the case that has to hold: a slow
+# deliberate turn that kept sliding afterwards would be unusable.
+rotate 0
+rotate -4 100
+rotate -4 100
+rotate -4 100
+stable 700 15000
+snap placed
+rotate_end
+stable 700 15000
+# Byte-identical to `placed`, which is the whole assertion.
+snap placed_released
+# THROWN. The same twist delivered in 5 ms slices is hundreds of degrees a
+# second, so the release keeps the list walking after the fingers are gone.
+#
+# `snap` and then `rotate_end` with NO `stable` between them, which is not
+# impatience: a release is only a throw if it arrives while the hand is still
+# moving, and a 700 ms wait here would be a hand that stopped — which is
+# exactly what the PLACED case above already proves. The snapshot is a frame
+# read, far inside the 90 ms that separates the two.
+rotate 0
+rotate -12 5
+rotate -12 5
+rotate -12 5
+snap thrown
+rotate_end
+stable 700 15000
+# ...and this one must NOT match `thrown`: the fling moved on its own, after
+# the gesture was over. That difference IS the feature.
+snap thrown_coasted
+draw rotate
diff --git a/test/macos-snapshots/trackpad.golden b/test/macos-snapshots/trackpad.golden
new file mode 100644
index 00000000..5a82ea85
--- /dev/null
+++ b/test/macos-snapshots/trackpad.golden
@@ -0,0 +1,311 @@
+== snap ready grid=100x30 cursor=4,8
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/trackpad/cwd New Del
+|
+| x MARK a
+| zz
+| x MARK b
+| zz
+| x MARK c
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap adapter grid=100x30 cursor=7,5
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/trackpad/cwd New Del
+|
+| x MARK a
+| zz
+| x MARK b
+| zz
+| x MARK c
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| /private/tmp/pardes-macos-e2e/trackpad/cwd/+Search New Del
+| 1 @p0:2:3-6 x MARK a
+| 2 @p0:4:3-6 x MARK b
+| 3 @p0:6:3-6 x MARK c
+== snap look grid=100x30 cursor=7,5
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/trackpad/cwd New Del
+|
+| x MARK a
+| zz
+| x MARK b
+| zz
+| x MARK c
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| /private/tmp/pardes-macos-e2e/trackpad/cwd/+Search New Del
+| 1 @p0:2:3-6 x MARK a
+| 2 @p0:4:3-6 x MARK b
+| 3 @p0:6:3-6 x MARK c
+== snap dragged grid=100x30 cursor=9,3
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/trackpad/cwd New Del
+|
+| x MARK a
+| zz
+| x MARK b
+| zz
+| x MARK c
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| /private/tmp/pardes-macos-e2e/trackpad/cwd/+Search New Del
+| 1 @p0:2:3-6 x MARK a
+| 2 @p0:4:3-6 x MARK b
+| 3 @p0:6:3-6 x MARK c
+== snap hover grid=100x30 cursor=9,3
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/trackpad/cwd New Del
+|
+| x MARK a
+| zz
+| x MARK b
+| zz
+| x MARK c
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| /private/tmp/pardes-macos-e2e/trackpad/cwd/+Search New Del
+| 1 @p0:2:3-6 x MARK a
+| 2 @p0:4:3-6 x MARK b
+| 3 @p0:6:3-6 x MARK c
+== snap scrolled grid=100x30 cursor=9,3
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/trackpad/cwd New Del
+|
+| x MARK a
+| zz
+| x MARK b
+| zz
+| x MARK c
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| /private/tmp/pardes-macos-e2e/trackpad/cwd/+Search New Del
+| 1 @p0:2:3-6 x MARK a
+| 2 @p0:4:3-6 x MARK b
+| 3 @p0:6:3-6 x MARK c
+== snap notched grid=100x30 cursor=9,3
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/trackpad/cwd New Del
+|
+| x MARK a
+| zz
+| x MARK b
+| zz
+| x MARK c
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| /private/tmp/pardes-macos-e2e/trackpad/cwd/+Search New Del
+| 1 @p0:2:3-6 x MARK a
+| 2 @p0:4:3-6 x MARK b
+| 3 @p0:6:3-6 x MARK c
+== snap exec grid=100x30 cursor=7,10
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/trackpad/cwd New Del
+|
+| x MARK a
+| zz
+| x MARK b
+| zz
+| x MARK c
+|
+| /tmp/pardes-macos-e2e/trackpad/tmp/pardes-XXXXXX Save New Del
+| 1
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| /private/tmp/pardes-macos-e2e/trackpad/cwd/+Search New Del
+| 1 @p0:2:3-6 x MARK a
+| 2 @p0:4:3-6 x MARK b
+| 3 @p0:6:3-6 x MARK c
+== snap force grid=100x30 cursor=54,3
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/trackpad/cwd New D /private/tmp/pardes-macos-e2e/trackpad/cwd New D
+|
+| x MARK a
+| zz
+| x MARK b
+| zz
+| x MARK c
+|
+| /tmp/pardes-macos-e2e/trackpad/tmp/pardes-XXXXXX
+| 1
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| /private/tmp/pardes-macos-e2e/trackpad/cwd/+Sear
+| 1 @p0:2:3-6 x MARK a
+| 2 @p0:4:3-6 x MARK b
+| 3 @p0:6:3-6 x MARK c
+== snap select grid=100x30 cursor=4,3
+|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill
+| /private/tmp/pardes-macos-e2e/trackpad/cwd New D /private/tmp/pardes-macos-e2e/trackpad/cwd New D
+|
+| x MARK a
+| zz
+| x MARK b
+| zz
+| x MARK c
+|
+| /tmp/pardes-macos-e2e/trackpad/tmp/pardes-XXXXXX
+| 1
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| /private/tmp/pardes-macos-e2e/trackpad/cwd/+Sear
+| 1 @p0:2:3-6 x MARK a
+| 2 @p0:4:3-6 x MARK b
+| 3 @p0:6:3-6 x MARK c
+== draw trackpad 850x495 nonblank
diff --git a/test/macos-snapshots/trackpad.snap b/test/macos-snapshots/trackpad.snap
new file mode 100644
index 00000000..05f46e1f
--- /dev/null
+++ b/test/macos-snapshots/trackpad.snap
@@ -0,0 +1,91 @@
+# The trackpad features, asserted without a trackpad, plus the rest of the
+# pointer vocabulary.
+#
+# Trackpad.button(fingers:) is pure policy with no NSEvent in it, so a script
+# can name how many fingers were resting and get back the button a hand would
+# have produced: two is acme's button 3 (Look), three or more is button 2
+# (Exec), and a deep press is button 2 by another route. pardes_take_haptic is
+# the other half — it is how the pulse the core armed is read back on a machine
+# with nothing to feel it with.
+#
+# Coordinates are 0-based cells, which is what pardes_mouse takes. The tty
+# suite's press/release are 1-based SGR and so read one higher.
+start 30 100
+wait 8000 New Newcol
+wait 8000 $
+stable 700 20000
+# something to look at; split so the echoed command line is not the match
+text printf 'x MA''RK a\nzz\nx MA''RK b\nzz\nx MA''RK c\n'
+key enter
+wait 10000 MARK c
+stable 700 10000
+# A raw pane hands clicks to the program on the other end. Normal mode is where
+# the three buttons mean Select, Exec and Look.
+key c-b
+stable 600 8000
+snap ready
+# THE ADAPTER ITSELF, before any of the decoded paths below. `nsclick` builds a
+# real NSEvent and hands it to the view's own rightMouseDown, so this is the one
+# command that exercises the code between AppKit and the entry points. It is
+# here because an adapter that silently dropped every mouse event once passed
+# this entire suite: everything else calls the entry points directly.
+nsclick right 4 3
+wait 10000 @p0:
+stable 700 15000
+haptic look
+snap adapter
+# Back to a clean slate for the finger-count cases below.
+haptic none
+# TWO FINGERS RESTING = button 3 = Look. MARK names no file, so looking at it
+# searches, and the hits land in a +Search buffer split under the shell.
+fingers 2 4 3
+wait 10000 @p0:
+stable 700 15000
+haptic look
+snap look
+# ...and taking the pulse cleared it
+haptic none
+# The three press phases one at a time rather than through `click`, which is
+# what proves press, drag and release are separate entry points and not a lump.
+mouse left press 2 3
+mouse left drag 9 3
+mouse left release 9 3
+stable 700 10000
+snap dragged
+# ponytail: `y` here would yank that selection into the core's register and
+# fire set_clipboard, which is what the harness's `clipboard <text>` asserts.
+# It is not scripted because the expected text has to be READ off this golden
+# first — a guessed span would hand the next person a failing script instead of
+# a capture. Add the two lines once `snap dragged` shows what got selected.
+# A button-less hover: the core tracks it for the link and definition underline.
+mouse none motion 4 5
+stable 400 5000
+snap hover
+# A trackpad's precise scroll is fractional rows the core accumulates into whole
+# wheel presses; a real mouse notch skips the smoothing and arrives as an
+# ordinary button. Both spellings, so the two paths stay distinguishable.
+scroll -3 4 5
+stable 700 10000
+snap scrolled
+wheel wheeldown 4 5
+stable 700 10000
+snap notched
+# THREE FINGERS = button 2 = Exec. `New` in the top tag is a builtin, so this
+# spawns a pane rather than running something the machine would have to own.
+fingers 3 1 0
+stable 700 20000
+haptic exec
+snap exec
+# A DEEP PRESS is the same button 2 by another route; `Newcol` proves it went
+# somewhere different from the click above.
+force 5 0
+stable 700 20000
+haptic exec
+snap force
+# One finger is still an ordinary left press, and selecting is not a verb the
+# hand should feel.
+fingers 1 4 3
+stable 700 10000
+haptic none
+snap select
+draw trackpad