diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-11 18:30:13 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-12 13:04:32 -0300 |
| commit | b424164922842796619cb6894ec46d729a8a6826 (patch) | |
| tree | bd4653accfaeb67012f4e7257c61eaf4fb12c892 /docs/macos.md | |
| parent | 08f32cdde672740b71608f3dfccd629dcbac78f7 (diff) | |
| download | pardes-b424164922842796619cb6894ec46d729a8a6826.tar.gz pardes-b424164922842796619cb6894ec46d729a8a6826.zip | |
clipboard, n/N and the tty prompt: three things that were half-wired
Three changes that all turned out to be the same shape -- a feature that
worked in one direction, or for one pane kind, and quietly did not in the
others.
CLIPBOARD. Every register write emitted set_clipboard, so deleting one
character threw away whatever the desktop was holding; multi-cursor yank took
the join's early return and emitted nothing at all, so the same key reached
the clipboard on one cursor and not on two. Nothing could READ the clipboard:
the SDL shell had no SDL_GetClipboardText anywhere in it, and the tty shell
never asked for OSC 52, so `p` from another application was dead in both.
Now it is helix's split. y/d/c/p/P/R and the acme chords are the DEFAULT
REGISTER and nothing else; the system clipboard is five words on helix's own
letters -- SPC y, SPC Y, SPC p, SPC P, SPC R -- spelled as builtins so they
land in Help and are executable like every other verb. The one exception is
the tag `y` chord, which still mirrors out because a tag is always insert, so
SPC cannot be pressed there, and copying the path out is the whole point of
the chord.
Reading is a new read_clipboard effect answered by an ordinary Event.paste, so
the round trip is honest about being one: SDL and NSPasteboard answer inside
the same drain, the browser answers a promise, and a terminal answers over
OSC 52 or -- far more often -- refuses. A refused read is a paste that does
not happen, and the request dies at the next keystroke rather than landing
minutes late in whatever pane is focused by then.
The tty shell also enables BRACKETED PASTE now and coalesces
paste_start..paste_end into one event. Before this a paste arrived as a flood
of individual key presses: plausible in insert mode, and in normal mode every
pasted character ran as a command.
n/N. They stepped the armed results buffer and immediately Looked each row, so
you could not walk past a hit without opening it. They are a MOTION now:
select the next look-able text, open nothing, and let Enter decide. What they
step is the largest whitespace-delimited run look.resolve can act on
(look.lookableSpan, wrapper punctuation peeled), over a RING of panes -- every
pane that has performed a look, most recent first, then the output buffers
that have not, newest first, and only if both are empty the pane in front of
you. N is the exact inverse of n, computed rather than remembered: both
directions ask the same question about the same spans and compare against the
column the walk parks on, so x presses one way and x back land exactly where
you started, pane boundaries and the ring's seam included.
A ring rather than a list with two ends because a shell's cursor sits at the
prompt, below everything it has printed, so a walk that could not come round
would have nowhere to go on the very first press -- which is the case n/N were
written for.
One motion everywhere, no pane-kind or buffer-kind special case. The only
thing a buffer may change is the GRAIN of what a step selects, and it does it
with one flag rather than a branch: output_pane.Traits.commands (renamed from
`executes`, which named one reader's behaviour rather than the fact) makes a
row select WHOLE, because a ThemeSel line is a word to run and has no path
inside it to pick out. `]d`/`[d` are not n/N -- they are helix's diagnostic
motions, their job is to ARRIVE, and they still reach searchStep.
THE TTY PROMPT. Leaving raw tty blanked the prompt row, and the command you
had typed at that prompt shares the row, so it went too -- a shell out of tty
read as output only. OSC 133 marks the row CELL by cell, so the two are
separable: config.tty_blank = .prompt cuts the prompt's own columns and leaves
the command, left-hugged at column 0 in line with the output under it rather
than in a bay of blanks. .prompt_and_input is the old behaviour, kept.
Because the row is now something you can put a cursor in, enterTty adds the
hidden prompt width back before asking ghostty to walk the shell's own cursor
to it -- the modal column on a cut row is short by exactly that much.
Verified: unit-test 186/186 (nine new), snap 87/87 (new ttyprompt.snap),
hxdiff 481 and hxparity 561 with 0 mismatches, tty and gui both build. And
against the real binaries rather than the harness: in a pty, SPC y emits OSC
52 carrying exactly the selection while plain y emits nothing, SPC p issues
the read and pastes the reply, and a bracketed paste of "dd..." inserts text
instead of deleting two lines. In a real SDL window, SPC y then SPC p round
trips through the system clipboard while the default register holds different
text. Setting tty_blank back to .prompt_and_input reproduces all 86 old
goldens byte for byte.
Diffstat (limited to 'docs/macos.md')
| -rw-r--r-- | docs/macos.md | 73 |
1 files changed, 45 insertions, 28 deletions
diff --git a/docs/macos.md b/docs/macos.md index 7f695000..ff19ff93 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -61,10 +61,12 @@ removing; it is the encoding being right. The one real difference is IO. The browser has no ptys, so `src/web.zig` forwards every effect out to JavaScript as a numbered code plus a byte payload, and JavaScript performs it. macOS has `forkpty`, `read` and `write` right there, -so effects never cross the boundary at all: `pardes_tick` drains the core's -effect queue and performs each one in Zig, the way `drainEffects` in -`src/tty/tty.zig` does. That is why the runtime struct is two callbacks and not -twelve. +so almost no effect crosses the boundary at all: `pardes_tick` drains the +core's effect queue and performs each one in Zig, the way `drainEffects` in +`src/tty/tty.zig` does. That is why the runtime struct is three callbacks and +not twelve — and why two of the three are the clipboard: the pasteboard is +AppKit's, in both directions, and is the one piece of IO the Zig side cannot +reach for itself. ## The seam @@ -100,13 +102,20 @@ is made of and what opens a path from argv or the Dock (`Look <path>`). it. `pardes_resize` also carries one cell in *physical* pixels, which only the native PDF placement path reads — pass the backing-store size, not points. -**Runtime callbacks.** `pardes_runtime_s` is a `userdata` and two functions, +**Runtime callbacks.** `pardes_runtime_s` is a `userdata` and three functions, copied by value during init so the struct need not outlive the call. `wakeup` -means "your state moved, please pump me"; `set_clipboard` fires inside a tick -when the yank register changes, with the text borrowed for the duration of the -call. There is no `open_url` callback because the core already opens links -itself through `/usr/bin/open` (`src/look.zig`), and no file callbacks because -it owns the filesystem side too. +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 +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 +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 +`pardes_tick`. There is no `open_url` callback because the core already opens +links itself through `/usr/bin/open` (`src/look.zig`), and no file callbacks +because it owns the filesystem side too. **Lifecycle.** `pardes_init` returns 0 or an opaque nonzero code, `pardes_deinit` tears down, `pardes_tick` drains pty output plus the effect @@ -195,11 +204,14 @@ already-Exec is the only case with nothing to do. The view also needs system's business and never arrives — and the user needs "Force Click and haptic feedback" on in System Settings, which nothing in this process can read. -Twisting two fingers is a dial, and a dial over a list of search hits is `n`. -`pardes_rotate` takes raw degrees and libpardes quantizes them, one search step -per 10°, keeping the remainder — the same accumulate-and-spend shape as -`pardes_scroll`, in Zig for the same reason: it is then unit-tested on a machine -with no trackpad. AppKit reports counterclockwise as positive and the forward +Twisting two fingers is a dial, and a dial over a list of look-able places is +`n`. A notch moves the SELECTION one place along and opens nothing; Enter opens +the one the dial stopped at, which is what makes the dial worth spinning at all +— a stepper that opened every place it passed could not be. `pardes_rotate` +takes raw degrees and libpardes quantizes them, one step per 10°, keeping the +remainder — the same accumulate-and-spend shape as `pardes_scroll`, in Zig for +the same reason: it is then unit-tested on a machine with no trackpad. AppKit +reports counterclockwise as positive and the forward step is clockwise, so the sign inverts here and nowhere else. The banked remainder is deliberate hysteresis; a gesture beginning passes 0 to clear it, so the first degree of a new twist cannot inherit a nearly-complete notch from the @@ -244,10 +256,11 @@ asserting the machine's timer jitter. Both halves are goldens. `rotate.snap` turns the dial at 4° per 100 ms (40°/s, under the floor) and asserts the screen is byte-identical across the release, then at 12° per 5 ms and asserts it is not. The list it walks is twenty-four -hits rather than three for a reason worth keeping: `n` at the last match has -nowhere to go, so on a short list the hardest possible flick and no flick at -all produce the same screen — a golden that would have passed before momentum -existed. +places rather than three for a reason worth keeping, and `n`/`N` becoming a +RING only sharpened it: a short ring comes round, so a hard flick can stop +exactly where a single notch would have and the two screens agree — a golden +that passes before momentum exists. Twenty-four is longer than the cap can +travel (about eleven), so the fling has nowhere to hide. ## A drop is a click plus Look @@ -306,8 +319,11 @@ the *word under the pointer* and most words resolve to nothing: and the screen does not move. The pulse still fires — the gesture worked. - **Exec** (three fingers) on a builtin name runs it. On ordinary prose it types that word at a shell, which needs a terminal pane to type into. -- **`n`/`N`** (twist) steps a results buffer. With no `/pattern`, Grep or Find - behind it there is nothing to step. +- **`n`/`N`** (twist) moves the SELECTION to the next look-able place and opens + nothing; Enter opens what it landed on. It walks a ring across panes — the + ones a Look came from first, then the output buffers none has — so a twist + 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 stream it came in on, the button it resolved to, the pressure stage, the @@ -319,12 +335,13 @@ because guessing at that from a screenshot cost an afternoon. Every Exec and every Look taps the trackpad. The core arms a one-slot pulse in the ONE dispatcher — `lookAt` and `execute`, the two functions a middle click, a -right click, Enter, Tab, a tag chord, `n`/`N` stepping and the `Look`/`Exec` -builtins all funnel into — and the host takes it once per pump with -`pardes_take_haptic`. Exec gets `.generic`, the definite tap of something done; -Look gets `.alignment`, the lighter detent AppKit uses when a dragged guide -snaps. A pulse, not a queue: five Execs inside one keystroke are one thing the -hand did. +right click, Enter, Tab, a tag chord and the `Look`/`Exec` builtins all funnel +into — and the host takes it once per pump with `pardes_take_haptic`. Exec gets +`.generic`, the definite tap of something done; Look gets `.alignment`, the +lighter detent AppKit uses when a dragged guide snaps. A pulse, not a queue: +five Execs inside one keystroke are one thing the hand did. A twist taps nothing +now that `n`/`N` only move the selection, and that is the honest report: the +detent belongs to the Enter that opens, and that is where it fires. Three details are load-bearing. `execute` arms only at `exec_depth == 0`, because `Exec ls` re-enters as `ls` and one Tab is one gesture however many @@ -672,7 +689,7 @@ zig build unit-test -Dplatform=macos That covers the ABI's Zig side, the effect drain, and the two quantizers the trackpad depends on — `takeScrollTicks` and `takeRotationNotches` are pure -functions precisely so that "how many search steps is a 180° twist" is answerable +functions precisely so that "how many notches is a 180° twist" is answerable on a machine with no trackpad in it. The header is kept honest with ghostty's trick. `build.zig` runs `translate-C` |
