diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-12 13:32:43 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-12 16:07:33 -0300 |
| commit | bc89f57cb576e23a58572ec35f96db068367f1b4 (patch) | |
| tree | 06f1ea2b7e95f229c10b316214ae904b9d43e343 /docs/macos.md | |
| parent | b424164922842796619cb6894ec46d729a8a6826 (diff) | |
| download | pardes-bc89f57cb576e23a58572ec35f96db068367f1b4.tar.gz pardes-bc89f57cb576e23a58572ec35f96db068367f1b4.zip | |
docs: the tutor taught three keystrokes wrong, and the rest had drifted
The documentation had gone stale in the ordinary way -- claims that were true
when they were written and that nothing since had been obliged to re-read.
Some of them were load-bearing.
THE TUTOR. It still said there is no multi-cursor, that NextColor cycles three
themes, and that its practice blocks "are also run as unit tests (generated
from this file by tutor_gen)" -- a tool that appears nowhere in the tree, and
nothing anywhere parses a `# keys:` block. Left alone, that claim is what
makes the next wrong block survive.
Three of those blocks WERE wrong, and all three for one reason: since the
helix motion model landed, w/e/f/t SELECT the range they cross, so `i` after
one inserts at the SELECTION'S START. `w i Z esc` on "foo bar" gives
"Zfoo bar", not the "foo Zbar" the file promised. They were written against a
vim reading of the same keys. Every block in the file has now been run through
`zig build hxdiff` against the real core and matches byte for byte, and the
trap itself is written down in 3.3 rather than left to be rediscovered.
The tutor gains a PART 4 for everything added since it was written -- PDF
panes, the in-process ZLS backend, themes and fonts, the startup file -- and
PART 3 gains counts (and which keys ignore one), f/F/t/T, the whole g table
(bare `G` is a no-op; `ge` is the START of the last line), multiple cursors
and the s/S regex pair, `m`, `]`/`[`, `|`, insert mode, and all fifty leader
paths.
THE REST. design.typ's line table claimed 7,626 lines against a real 38,048,
and its rows did not sum to its own total; its Event/Effect boundary contract
-- the part a shell author writes against -- named four variants that do not
exist and omitted fourteen that do. lsp.md's probe count. config.md's
theme-name rules, which as written could not reach a zed theme at all.
helix-keys.md's Skipped section, holding five families that have since landed.
macos.md's menu bar, undocumented, along with sixteen other claims. web.md on
what the browser build can actually do.
SOURCE COMMENTS that had rotted alongside them: `tag_normal` is a space, not
the `•` its own comment describes; Wrap is ON by default, not off; a FontSel
row is SELECTED by n and RUN by Tab, not run by n; the SPC paths in lsp.zig
lost their `l` group prefix when the language group moved; and the
differential suites are 481 and 561 cases, not 360 and 440.
TWO THINGS FOUND BY DOCUMENTING THEM, both left standing and written down
rather than papered over. Typing `[^\n]` at an s/S prompt panics: the live
preview compiles every prefix, and `[^\` indexes an empty slice in mvzr's
parseCharSet. Both the tutor and a waiver recommended that pattern as the
workaround for `.` matching a newline; they now say what it costs and what
would make it sayable. And `Exec` is a builtin, so an `Exec` line in the
startup config types that command into a shell before the first frame -- the
tutor said nothing in that file is ever sent to one.
Nine adversarial reviews over two rounds, each with the hxdiff harness to
execute what it doubted. The second round exists because the first round's
fixes needed checking too, and it caught three regressions of my own -- one of
them a probe count I had "corrected" away from the truth.
Verified: unit-test, snap 87/87, hxdiff 481/0, hxparity 561/0, mupdf-check.
docs/design.pdf regenerated. The tutor's first seventeen lines are byte-
identical, which is what tutor.golden pins.
Diffstat (limited to 'docs/macos.md')
| -rw-r--r-- | docs/macos.md | 128 |
1 files changed, 100 insertions, 28 deletions
diff --git a/docs/macos.md b/docs/macos.md index ff19ff93..12e598b5 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -28,7 +28,7 @@ Pardes goes the other way because its frame is *already* a cell grid. draw one: attributed runs, the system font stack, and Apple's own subpixel and color-emoji handling, for free. The alternative is a hand-rolled glyph atlas, and the SDL backend is the measurement of what that costs — roughly three -thousand of `src/gui/gui.zig`'s 4,300 lines are a 2048² FreeType-hinted R8 atlas, +thousand of `src/gui/gui.zig`'s 4,700 lines are a 2048² FreeType-hinted R8 atlas, GPU pipelines, transfer buffers and shaders. Writing a second one, blind, buys nothing the grid needs. @@ -74,8 +74,9 @@ reach for itself. `pardes_frame_cells` hands back a pointer valid until the next `pardes_frame`, with `pardes_frame_cols`/`_rows` giving its shape. The cursor rides alongside as `pardes_cursor_x`/`_y`, each `-1` when it is hidden, plus `pardes_cursor_bar` -asking for a thin insert caret instead of a block. `Surface.images` has no -representation here yet; see the checklist. +asking for a thin insert caret instead of a block. `Surface.images` crosses +separately as `pardes_frame_images` / `pardes_frame_image_list` — see "Pixel +attachments" below. **Input.** `pardes_key` takes a codepoint and the host's already-composed text, because composition is AppKit's job and the core only ever wants finished @@ -106,10 +107,12 @@ native PDF placement path reads — pass the backing-store size, not points. copied by value during init so the struct need not outlive the call. `wakeup` means "your state moved, please pump me". `set_clipboard` hands over text to put on the pasteboard, borrowed for the duration of the call; it fires inside a tick -for the five `SPC` clipboard words and for the tag's own `y` chord, and for +for `SPC y` / `SPC Y` and for the tag's own `y` chord, and for nothing else — an ordinary `y` or `d` writes the core's register and never reaches here, which is what stopped deleting a character from clobbering the -desktop's clipboard. `read_clipboard` is that direction reversed: "give me the +desktop's clipboard. The other three clipboard words — `SPC p`, `SPC P`, +`SPC R` — go the OTHER way and raise `read_clipboard` instead. +`read_clipboard` is that direction reversed: "give me the pasteboard", with no payload either way, because the host answers by calling `pardes_paste` — and NSPasteboard being synchronous, that usually happens inside the callback itself, before it returns. Both are main thread, inside @@ -227,8 +230,8 @@ its own — `momentumPhase` belongs to scroll — so the release speed is measur here, off the monotonic clock, as a smoothed degrees-per-second over the event stream. -The curve is the point. Momentum ramps up **from zero** at a 70°/s floor rather -than switching on at it: +The curve is the point. Momentum is scaled off the EXCESS over a 70°/s floor +rather than being a flat amount the moment the floor is crossed: ```zig excess = min(|speed| - rotation_fling_floor, rotation_fling_max) @@ -236,8 +239,11 @@ excess = min(|speed| - rotation_fling_floor, rotation_fling_max) A plain threshold would hand out two free notches the instant it was crossed, and the same gesture a hair quicker jumping twice as far is how a control stops -feeling like a control. So a slow, deliberate turn coasts not a little but not -at all, and the harder it is thrown the further it goes — bounded, by the cap, +feeling like a control. There is a second floor under that one: an excess below +`rotation_fling_stop` (18) coasts at zero, so 70–88°/s is a dead band and the +smallest coast that happens at all is 18°/s — under a fifth of one notch, which +is why a slow deliberate turn still spends no notches at all. The harder it is +thrown the further it goes — bounded, by the cap, at about eleven matches for the hardest flick a trackpad can report. Two things stop a fling that was never thrown. A release more than 90 ms after @@ -293,7 +299,9 @@ click went where the pointer was. and Cmd-click for the path, the filename, and the dot in the close button. There is no document architecture behind this and deliberately so: no -`NSDocument`, no save panel, no "do you want to save" on close. Save is a +`NSDocument`, no save panel, no "do you want to save" on close. (One piece of +document chrome does exist, and it is the harmless one: File ▸ Open… runs a +real `NSOpenPanel` and hands what you pick to Look.) Save is a builtin, the pane's tag already says so, and this is the same two facts spelled where a Mac user looks for them. A terminal or an output buffer is not a document, so focus landing on one clears the icon and puts the title back to @@ -309,6 +317,41 @@ shell reads it; it exists because a windowed host has somewhere to put the answer. It is marked at ask-time rather than on completion because `save_file` carries none back, which makes it exactly as honest as the tagline already was. +## The menu bar, and the chords the core never sees + +A Mac app without a menu bar is a Mac app that is wrong, and most of what a +menu bar wants to say pardes already has a word for. So the menu is mostly a +second spelling of builtins — `AppDelegate` calls `run("New")`, `run("Save")`, +`run("Help")`, `run("Tutor")` — and the items with no builtin behind them are +left out rather than stubbed. + +| menu | item | chord | what it does | +|---|---|---|---| +| pardes | About pardes | — | AppKit's panel | +| | Hide pardes / Hide Others / Show All | ⌘H / ⌥⌘H / — | AppKit | +| | Quit pardes | ⌘Q | AppKit | +| File | Open… | ⌘O | a real `NSOpenPanel`, then Look on what you picked | +| | New | ⌘N | the `New` builtin | +| | Save | ⌘S | the `Save` builtin | +| | Close Window | ⌘W | AppKit; the app terminates after the last one | +| Edit | Paste | ⌘V | the view's own paste path, not a second one | +| View | Zoom In / Zoom Out / Actual Size | ⌘= / ⌘- / ⌘0 | point size, host-side | +| Window | Minimize / Zoom | ⌘M / — | AppKit | +| Help | pardes Help | ⌘? | the `Help` builtin | +| | Tutorial | — | the `Tutor` builtin | + +Edit holds Paste and nothing else. Copy, Cut, Undo and Select All are +deliberately absent: pardes's own words for those are `SPC y`, the 1-2 chord, +`u` and `%`, and a menu item that ran a different thing under the same name +would be worse than no item. + +The consequence is worth stating plainly, because it is invisible from the +core's side: those chords are now **the menu's**, and the core can never see +them. Nor can it see any other Command chord — the ABI's modifier mask carries +ctrl, alt and shift and has no super bit, so `PardesView` swallows Cmd rather +than sending a key the core would misread as unmodified. Cmd is the host's +layer here; everything pardes binds lives on the other four. + ## When a gesture looks like it did nothing All three verbs are cheap to mistake for broken, because acme's verbs are about @@ -325,7 +368,8 @@ the *word under the pointer* and most words resolve to nothing: with no results buffer anywhere still steps the pane in front of you. A screen with no filename on it is what has nothing to step. -`PARDES_LOG=1` prints every decoded gesture to stderr — the fingers counted, the +Setting `PARDES_LOG` at all — the gate tests presence, not value, so even +`PARDES_LOG=0` counts — prints every decoded gesture to stderr: the fingers counted, the stream it came in on, the button it resolved to, the pressure stage, the rotation degrees. It exists because which events a trackpad produces is decided by hardware plus four System Settings switches this process cannot read, and @@ -430,7 +474,7 @@ collection, derives bold and italic from it, and re-measures. A file it cannot wear leaves the screen exactly as it was — a terminal that cannot draw has no way back out of itself. -Zoom does not touch the core at all. Cmd+, Cmd- and Cmd+0 change the point size, +Zoom does not touch the core at all. Cmd+=, Cmd- and Cmd+0 change the point size, `Metrics` is rebuilt, and the new cell is reported through the same resize path a window drag uses; the core reflows to a different number of columns and knows nothing about points. Cmd+= rather than Cmd++ because AppKit matches the @@ -547,11 +591,17 @@ blank window for every opaque theme the moment it was hidden. ## Threading -One core, touched only from the main thread, plus one pty reader task per pane. -A reader blocks in `read(2)`, appends into that pane's mutex-guarded buffer, and -calls `wakeup`; the next tick swaps the buffers under the lock and feeds the -bytes in as `output` events. Sixteen panes is the ceiling (`MAX_PANES`), so it -is sixteen threads worst case, each of which does nothing but move bytes. +One core, touched only from the main thread, plus one pty reader task per pane +and one socket listener for the nested-pardes protocol. A reader blocks in +`read(2)` and pushes into ONE process-wide `Inbox` — a bounded ring of tagged +messages, not a buffer per pane — then calls `wakeup`; the next tick drains the +ring wholesale and feeds the bytes in as `output` events. The ring is lossy +under sustained backpressure: an `output` message can be dropped, and `eof` and +`command` will evict a queued `output` to get in, because losing a byte of +scrollback is survivable and losing the end of a pane is not. Sixteen panes is +the ceiling (`MAX_PANES`), and the readers run on a default-sized +`std.Io.Threaded` pool, so the thread count is the pool's rather than one per +pane. Ghostty again is the contrast: it runs a renderer thread *and* an IO thread per surface, each with its own mailbox and wakeup, because it owns the frame clock @@ -593,10 +643,17 @@ through `ranlib` first, because ld64 otherwise refuses zig's layout outright missing, which links almost far enough to look like a source problem. ```sh -zig build -Dplatform=macos # leaves a signed zig-out/pardes.app +zig build -Dplatform=macos # zig-out/lib/libpardes.a + include/pardes.h +zig build macos-app -Dplatform=macos # ...and a signed zig-out/pardes.app zig build macos-dmg -Dplatform=macos # ...and zig-out/pardes.dmg to hand over ``` +All three need a Darwin host: the app branch is gated on +`builtin.os.tag.isDarwin()`, and off Darwin `macos-app` and `macos-dmg` resolve +to an explicit build failure rather than a bundle that could not have been +signed. A plain `zig build -Dplatform=macos` still gives you the library and +the header anywhere, which is what the Linux dev loop below uses. + The bundle is assembled by `build.zig` itself, not by a script it shells out to. An `.app` is a directory with a plist, a binary and an icon in it, and each of those is one step whose inputs the build graph knows — so the app rebuilds @@ -625,7 +682,7 @@ system to keep in step for what three `addInstallFileWithDir` calls already do. following `-Doptimize` so both halves of the app are built the same way: ```sh -swiftc -O -target arm64-apple-macos13.0 \ +swiftc -O -target <host-arch>-apple-macos13.0 \ -import-objc-header src/macos/pardes.h \ -o <cache>/pardes \ src/macos/Sources/{main,AppDelegate,PardesView}.swift \ @@ -633,6 +690,11 @@ swiftc -O -target arm64-apple-macos13.0 \ -framework AppKit -framework CoreText -framework CoreGraphics ``` +The arch is derived from the build target, which defaults to the host — `arm64` +on Apple silicon, `x86_64` on an Intel Mac. What is genuinely missing is a +UNIVERSAL binary: there is no `lipo` step anywhere, so a bundle built on one +arch runs on that arch. + The header goes in through `-import-objc-header` rather than a module map, because the header is read straight out of the source tree and there is nothing to stage; a module map is what an *xcframework* needs, and there isn't one. @@ -716,16 +778,24 @@ prohibited) `NSWindow`, over a real core with real ptys: ```sh zig build macos-e2e -Dplatform=macos # run it zig build macos-e2e -Dplatform=macos -- --update # regenerate the goldens -zig build macos-e2e -Dplatform=macos -- test/macos-snapshots/rotate.snap ``` +The step always hands the harness the whole `test/macos-snapshots` directory, +so naming one script after `--` runs it IN ADDITION to the suite rather than +instead of it. To run a single script, invoke +`zig-out/bin/pardes-macos-e2e test/macos-snapshots/rotate.snap` directly. + `test/macos_e2e.swift` links the same Swift sources the app does, minus `main.swift`, into a second binary — test scaffolding does not ship inside the -product. Scripts are `test/macos-snapshots/*.snap` and speak the tty suite's -vocabulary (`start`, `wait`, `stable`, `text`, `key`, `snap`) plus what only +product. Scripts are `test/macos-snapshots/*.snap` (seven of them: boot, cwd, +drop, font, keys, rotate, trackpad) and speak the tty suite's +vocabulary (`start`, `wait`, `stable`, `text`, `key`, `snap`, `command`, +`mouse`, `click`, `wheel`, `resize`, `draw`) plus what only exists here: `fingers <n>`, `force`, `rotate <degrees> [gap_ms]`, `rotate_end`, -`drop <path> <col> <row>`, -`scroll`, `haptic <none|exec|look>` and `draw`. The dial's two extras are what +`drop <path> <col> <row>`, `scroll <rows> <col> <row>`, +`haptic <none|exec|look>`, `nsclick` (the AppKit-event path, as opposed to +`click`'s direct entry-point call), `font <name>`, `zoom` and `clipboard`. +The dial's two extras are what make momentum testable at all: the optional gap is a real sleep before the event, so a script can say how FAST the dial is being turned, and `rotate_end` is the release the fling is measured from. Output is byte-identical in shape to @@ -785,13 +855,15 @@ for exactly that reason. - **The `lsp` effect.** Needs a worker plus a snapshot of the pane's path and content taken *before* it starts, as `LspJob` in `tty.zig` does; the core - keeps editing while a query is in flight. Until then language queries are - dropped and nothing waits for an answer. + keeps editing while a query is in flight. Until then every query is answered + with an EMPTY `lsp_resp` rather than dropped — dropping one leaves the + keystroke that asked (insert-mode Tab after a dot) waiting forever, and dead + for the rest of the session. - **The `pipe` effect.** Selection filters need `pipeRequest(id)` copied into a job, a worker to run the command, and a `pipe_resp` event back. Same shape as `lsp`, one more response type. - **The `watch` effect.** `watchPane` is inotify and returns silently off Linux - (`ponytail:` at `src/tty/tty.zig:1035`). macOS wants the FSEvents half of + (`ponytail:` at `src/tty/tty.zig:1183`). macOS wants the FSEvents half of `std.Build.Watch`, which the standard library already has as `Build/Watch/FsEvents.zig`. Without it the core simply never receives `file_changed`, a state it tolerates because the browser has no filesystem @@ -827,4 +899,4 @@ for exactly that reason. is one `-Dmacos-identity=` away and notarization one `notarytool` call. Still absent: a universal binary, bundled fonts, localization. The app has an icon, a plist that says what it opens, a deployment target it actually enforces and - a signature; the arm64-only slice is the deliberate remaining gap. + a signature; the missing universal binary is the deliberate remaining gap. |
