summaryrefslogtreecommitdiff
path: root/docs/macos.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/macos.md')
-rw-r--r--docs/macos.md191
1 files changed, 135 insertions, 56 deletions
diff --git a/docs/macos.md b/docs/macos.md
index 940d5a58..2b50e6bc 100644
--- a/docs/macos.md
+++ b/docs/macos.md
@@ -27,10 +27,10 @@ Pardes goes the other way because its frame is *already* a cell grid.
`pardes.Surface` is `cols × rows` of `Cell`, and CoreText is the native way to
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,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.
+and the SDL backend is the measurement of what that costs — a large part of
+`src/gui/gui.zig`'s 6,348 lines is a 2048² FreeType-hinted R8 atlas (`atlas_w`
+and `atlas_h`, `src/gui/gui.zig:140`), GPU pipelines, transfer buffers and
+shaders. Writing a second one, blind, buys nothing the grid needs.
Blind is the operative word: this backend was scaffolded on a Linux machine. A
Metal renderer written there would have been untestable code — compiled at best,
@@ -65,13 +65,28 @@ 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 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.
+and JavaScript performs it. macOS has `forkpty`, `read` and `write` right
+there, so almost no effect crosses the boundary at all: `pardes_tick` runs
+`while (core.nextEffect()) |e| core.perform(e)` (`src/macos.zig:1154`) and each
+effect lands on this host's own `Host.VTable`, of whose twenty-one methods
+fourteen are filled in (`src/macos.zig:1761`).
+
+The machine-local half of those methods is no longer this file's. `forkShell`,
+`writeFileBytes` and `writeFd` live in `src/host_io.zig` and are shared with
+the tty shell, the SDL shell and the detached daemon; `src/macos.zig` calls
+them (`:1807`, `:1823`, `:1848`, `:1863`) and its own copies, together with the
+four `extern "c"` declarations they needed (`forkpty`, `execv`, `chdir`,
+`_exit`), are gone. Two `extern "c"` declarations remain here: `setenv`, and
+`pardes_host_watch_file`, which FileWatcher.swift satisfies
+(`src/macos.zig:40`, `:45`). The bug all four hosts' private `forkShell`
+copies had in common is the argument for the shared file existing: none set
+`FD_CLOEXEC` on
+the pty master, so a program in one pane could read and write another pane's
+terminal and closing a master did not reliably hang its shell up.
+
+That is why the runtime struct is three callbacks rather than a second vtable —
+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
@@ -426,8 +441,11 @@ and it answers both.
`Look` resolves against it, so it has to follow the shell rather than stay
where the pane was spawned.
-*When* it is read differs from the other two shells, and deliberately. The tty
-and SDL hosts poll every pane every frame; here the drain has just finished
+*When* it is read differs from the other three shells, and deliberately. The
+tty, SDL and detached-daemon hosts poll every pane every frame — inline in the
+tty loop's frame path (`src/tty/tty.zig:1228-1231`), `pollCwds` in the SDL
+shell (`src/gui/gui.zig:4048`) and `pollFrame` in the daemon
+(`src/detached/server.zig:889`); here the drain has just finished
saying exactly which shells produced bytes, and nothing else can have moved
one — a `cd` is a command, and a shell that ran a command writes at least its
next prompt. So `refreshCwds` reads only for panes flagged by that tick's
@@ -446,19 +464,25 @@ against `getppid()`.
The socket half needed real portability work rather than a second spelling.
Darwin has no `SOCK_CLOEXEC` and no `accept4`, so the flag is set with an
-`fcntl` after the fact — a race only against a fork on another thread, and both
-callers are past that. `sun_path` is 104 bytes here against 108 there, so no
+`fcntl` after the fact — a race only against a fork on another thread, and
+every caller is past that (`src/nested.zig:81-93` names all three).
+`sun_path` is 104 bytes here against 108 there, so no
buffer in the file spells a number any more; they are all sized from the field
itself, and an address that does not fit is refused rather than truncated into
a path pointing somewhere else.
-Identity gained a third sibling. `bin/pardes` and
-`pardes.app/Contents/MacOS/pardes` are one build installed twice and share no
-directory at all, so the comparison is made at the *install prefix* — the
-directory holding the `.app`, or the parent of a `bin` — and the bundle's name
-can never carry the `-os-arch` tail the installed binary does, because
-`CFBundleExecutable` is a fixed string. `zig build -Dplatform=macos` then
-`pardes src/foo.zig` inside the app's own shell opens a pane in the app.
+Identity gained a third sibling. `bin/pardes`, `bin/pardes-gui` and
+`pardes.app/Contents/MacOS/pardes` are three installed frontends of one
+program, so `samePardesExecutable` (`src/nested.zig:142`) compares BASENAMES
+and ignores the paths entirely — the GUI may be system-wide while the tty
+frontend sits in the user's own bin directory. `familyTail` strips a leading
+`pardes-gui` or `pardes` and requires what is left to be either empty or a
+valid `-os-arch` pair, which is what keeps `pardes-snap` and `pardes-perf` out
+of the family; `sameFamily` then accepts two names whose tails agree, or either
+of which has no tail at all. The bundle's copy always has none, because
+`CFBundleExecutable` is a fixed string: the bundled `pardes-macos-aarch64` is
+called `pardes` and nothing in the name records what it was. So `pardes
+src/foo.zig` inside the app's own shell opens a pane in the app.
## Fonts and zoom
@@ -717,24 +741,39 @@ synchronize and no mailbox protocol to get wrong.
## Building
-Two commands, because they are two different machines' problems.
+One command builds everything this shell has. The two extra steps below exist
+only for handing a bundle to somebody else.
```sh
zig build -Dplatform=macos
```
produces `zig-out/lib/libpardes.a` and installs `zig-out/include/pardes.h`
-beside it.
+beside it — and on a Darwin host a signed `zig-out/pardes.app` as well, which
+the next section is about.
-`-Dplatform=macos` is the one platform that overrides the repo's default
-target. Everything else defaults to the Steam Deck (x86_64 linux-gnu, glibc
-pinned low), and that default is not survivable here: swiftc links this archive,
-so a Steam Deck build hands ld64 ELF objects inside a GNU archive and the app
-link dies with `archive member '/SYM64/' not a mach-o file`. On a Mac the
-default becomes the host arch at `macos_min_version`, which is the same triple
-the app's swiftc link is given, so the two halves of the app cannot disagree about
-how old a macOS they support. On any other host it stays plain native, which is
-what keeps the Linux dev loop below runnable.
+`zig-out` and not `~/.local`, and that now needs saying. A bare `zig build`
+redirects the install prefix to `$HOME/.local` and prints that it has, but only
+behind the guard `also_gui and prefixIsUntouched(b)`: `also_gui` is
+`requested_platform == null`, and `prefixIsUntouched` refuses when a `DESTDIR`,
+a `--prefix` or a `--prefix-*dir` has already chosen somewhere. `-Dplatform=macos`
+names a shell, so neither condition holds and every path in this document is
+relative to `zig-out` unless you pass `--prefix` yourself. The `<prefix>/dev`
+directory the benchmark binaries install into likewise never appears in this
+backend; nothing here is a dev binary.
+
+`-Dplatform=macos` is one of the two platforms that override the repo's default
+target; `esp32p4` is the other, and pins its own riscv32-freestanding query.
+The tty, SDL and web shells default to the Steam Deck (x86_64 linux-gnu, glibc
+pinned to 2.38), and that default is not survivable here: swiftc links this
+archive, so a Steam Deck build hands ld64 ELF objects inside a GNU archive and
+the app link dies with `archive member '/SYM64/' not a mach-o file`. On a Mac
+the default becomes the host arch at `macos_min_version`, which is the same
+triple the app's swiftc link is given, so the two halves of the app cannot
+disagree about how old a macOS they support — the arch is spelled rather than
+left null so the CPU model resolves to generic, which is ghostty's
+`Config.genericMacOSTarget` workaround. On any other host it stays plain
+native, which is what keeps the Linux dev loop below runnable.
That archive is also *fat*. `b.addLibrary` emits only this module's own objects;
MuPDF, tree-sitter, zstbi, ZLS and ghostty-vt's simdutf/highway stay in archives
@@ -749,23 +788,37 @@ 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 # 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
+zig build -Dplatform=macos # libpardes.a + pardes.h, and on Darwin a signed pardes.app too
+zig build macos-app -Dplatform=macos # the signed bundle on its own
+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 first line is not "the library only", and that is the part worth stating:
+`b.getInstallStep().dependOn(&sign.step)` puts the bundle on the DEFAULT
+install step, so on a Darwin host an ordinary `zig build -Dplatform=macos`
+assembles `zig-out/pardes.app` and ad-hoc-signs it. build.zig says why in as
+many words — the app is "part of an ORDINARY build rather than a verb to
+remember". Only the dmg is opt-in, because it is for handing over rather than
+for running.
+
+What is gated is that whole branch, and on the TARGET as well as the host:
+`builtin.os.tag.isDarwin() and target.result.os.tag.isDarwin()` is what decides
+whether `fatArchive` runs, and without a Mach-O archive there is nothing for
+swiftc to link. Off either, `macos-app` and `macos-dmg` resolve to an explicit
+`addFail` — *"pardes.app needs a Darwin host and target; drop -Dtarget= or pass
+-Dtarget=native"* — rather than a
+bundle that could not have been signed, and the default install stops at the
+library. The library and the header build 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
-when a Swift source or the archive moves and is left alone when nothing does.
-There is no Xcode project: a hand-written `pbxproj` would be a second build
-system to keep in step for what three `addInstallFileWithDir` calls already do.
+to. An `.app` is a directory with a plist, a binary, a shader and an icon in
+it, and each of those is one step whose inputs the build graph knows — so the
+app rebuilds when a Swift source or the archive moves and is left alone when
+nothing does. There is no Xcode project: a hand-written `pbxproj` would be a
+second build system to keep in step for what four `addInstallFileWithDir` calls
+already do (`install_app_bin`, `install_plist`, `install_scene_kernel`,
+`install_icon`).
- **The plist.** `plutil -replace LSMinimumSystemVersion` reads the committed
`src/macos/Info.plist` and writes a stamped copy into the cache. The source
@@ -784,16 +837,24 @@ system to keep in step for what three `addInstallFileWithDir` calls already do.
`defaultBG`, Glenda `defaultFG`, the strip above her the tag bar, the block
cursor at the end of it `ansi16[11]`), and there is no binary blob in the tree
to disagree with the app it ships in.
-- **The link.** The same swiftc invocation as before, with the optimize mode
- following `-Doptimize` so both halves of the app are built the same way:
+- **The scene kernel.** `shaders/crt.ci.metal` is installed verbatim as
+ `Contents/Resources/crt.ci.metal` and compiled at runtime with
+ `CIKernel.kernels(withMetalString:)`. The same file is also an anonymous
+ module import named `effect-source-crt.ci.metal`, added by `build.zig`'s
+ per-shell module wiring under `if (shell == .macos)`, which is what lets
+ `EffectCode` print the exact source the app executes.
+- **The link.** One swiftc invocation, with the optimize mode following
+ `-Doptimize` — `-Onone` for Debug, `-Osize` for ReleaseSmall, `-O` otherwise
+ — so both halves of the app are built the same way:
```sh
-swiftc -O -target <host-arch>-apple-macos13.0 \
+swiftc <-O|-Osize|-Onone> -target <arch>-apple-macos13.0 \
-import-objc-header src/macos/pardes.h \
-o <cache>/pardes \
- src/macos/Sources/{main,AppDelegate,PardesView}.swift \
+ src/macos/Sources/{main,AppDelegate,PardesView,ScenePostprocessor,FileWatcher}.swift \
<cache>/libpardes.a -lc++ \
- -framework AppKit -framework CoreText -framework CoreGraphics
+ -framework AppKit -framework CoreText -framework CoreGraphics \
+ -framework CoreImage -framework Metal
```
The arch is derived from the build target, which defaults to the host — `arm64`
@@ -846,10 +907,13 @@ for letting something other than this app link the core. This ships arm64.
Three layers, and each one exists because the layer above it cannot reach where
it goes.
-**The Linux dev loop.** The Zig half of this backend is plain POSIX. `forkpty`,
-`read`, `write`, `ioctl(TIOCSWINSZ)` and `/usr/bin/open` differ from the tty
-backend by a string constant. So `-Dplatform=macos` compiles on a Linux host,
-and its tests run there:
+**The Linux dev loop.** The Zig half of this backend is plain POSIX, and most
+of it is no longer even this backend's: `forkShell`, `writeFileBytes` and
+`writeFd` are `src/host_io.zig`'s, byte-identical on both systems, and what is
+left that differs is `ioctl(TIOCSWINSZ)` (absent from `std.c.T` on darwin),
+`/usr/bin/open` against `/usr/bin/xdg-open` (`src/look.zig:33-37`), and libproc
+against `/proc`. So `-Dplatform=macos` compiles on a Linux host, and its tests
+run there:
```sh
zig build unit-test -Dplatform=macos
@@ -897,7 +961,8 @@ 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`,
+exists here: `fingers <n> <col> <row>`, `force <col> <row>`,
+`rotate <degrees> [gap_ms]`, `rotate_end`,
`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>`, `font-size <points>`, `zoom`,
@@ -983,6 +1048,20 @@ used for the table and is not folded into the direct-path claim.
- **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.
+- **Detached sessions.** `Attach` (`SPC s a`) and `Detach` (`SPC s D`) exist on
+ every hosted platform, this one included, because `Builtin.enabled` is
+ `pardes.hosted`. Neither works here. `Detach` emits `Effect.detach`, this
+ host fills in no `push_detach`, and `Pardes.perform` therefore reports
+ `error.NotAttached` on the pane's message row (`src/pardes.zig:6837`).
+ `Attach` emits `Effect.attach`, which `perform` turns into an `attach_req`
+ the shell is supposed to consume from OUTSIDE `pump` with `takeAttach` — and
+ `src/macos.zig` never calls `takeAttach`, so the word does nothing at all and
+ says nothing either. Wiring it up means a unix-socket frontend loop beside
+ the AppKit one, which is `src/detached/client.zig`'s job in the tty and SDL
+ shells; see `docs/detached.md`.
+- **`tty_taken` on darwin.** The pull is wired (`ttyTaken`, `src/macos.zig:1838`)
+ but `look.ttyTaken` has no libproc implementation and answers `false`, so an
+ `Exec` here always believes the pane is still at the prompt it forked.
- **IME and marked text.** Only finished characters reach `pardes_key`, so a
dead key composes nothing and Option is Alt rather than a compose modifier.
Real composition means implementing `NSTextInputClient` *and* giving the core