diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-08 10:44:56 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-10 09:17:07 -0300 |
| commit | c3c8bbd8d8add99088c774c54bc1acf1e39ec895 (patch) | |
| tree | a602212f59134909532093765a80c87f219c6c4b /docs | |
| parent | 8aafc3fa24c7475a07259eb06cd5d217f510df98 (diff) | |
| download | pardes-c3c8bbd8d8add99088c774c54bc1acf1e39ec895.tar.gz pardes-c3c8bbd8d8add99088c774c54bc1acf1e39ec895.zip | |
a native macOS backend: libpardes plus an AppKit shell
Adds -Dplatform=macos, a fourth backend beside tty, gui and web. Zig keeps the
core, the ptys, every effect and the worker threads; Swift owns NSApplication,
the window, input translation, and drawing the cell grid with CoreText. They
meet at a hand-written C ABI in src/macos/pardes.h, built as a static library
the app links.
The ABI is src/web.zig's boundary with the wasm removed, because both hosts are
the same animal: someone else owns the clock, feeds events in through flat
functions, and reads one packed cell buffer out. The browser proved the shape.
The one divergence is that the browser has no processes and forwards every
effect to JavaScript, whereas forkpty is right here, so src/macos.zig performs
them — spawn, write, resize_pty, save_file, new_file, write_dump, open_link,
set_clipboard. lsp, pipe and watch are answered with nothing and marked; the
core already tolerates that, since the browser answers none of them either.
This deliberately inverts ghostty's split, which was studied first and is
written up in docs/ghostty-macos-notes.md. Ghostty hands Zig a bare NSView*,
installs its own CALayer and owns the frame clock; Swift never renders. Pardes
does the opposite because its frame is already a cell grid and CoreText draws
one natively — the alternative is a second hand-rolled glyph atlas, which is
what most of gui.zig's 4,300 lines already are. It would also have been written
blind: the Swift half cannot be compiled here.
What makes the scaffold verifiable rather than dead code is that the Zig half is
ordinary POSIX and builds and tests on Linux. Borrowing ghostty's best trick,
build.zig translate-C's the header into the test build and src/macos.zig asserts
every constant, struct layout, and exported function's arity and widths against
it. That guard earned its place immediately: pardes_scroll grew a cell
coordinate after the Swift view had been written against the older form.
Skipped, and named as the upgrade path in docs/macos.md: the Xcode project,
xcframework, lipo and codesigning ghostty needs. All four exist for
distribution; a dev build is a swiftc invocation and a directory with a plist.
The Swift app is a scaffold and says so — every uncertain API spelling carries
an UNVERIFIED marker, and no part of it has been compiled.
tty is unaffected: 75/75 snapshot scripts and both unit suites pass.
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/ghostty-macos-notes.md | 198 | ||||
| -rw-r--r-- | docs/macos.md | 252 |
2 files changed, 450 insertions, 0 deletions
diff --git a/docs/ghostty-macos-notes.md b/docs/ghostty-macos-notes.md new file mode 100644 index 00000000..556831af --- /dev/null +++ b/docs/ghostty-macos-notes.md @@ -0,0 +1,198 @@ +# Notes: how ghostty integrates Zig with Swift + +Research notes taken while designing `docs/macos.md`, kept so the next person +does not have to read ghostty again. Line references are against the checkout at +`~/05-genizah/ghostty` (commit `ba38b493`, "Update to Zig 0.16.0"); they will +drift, the shapes will not. + +Ghostty is the only large Zig program with a shipping native macOS app, so it is +the reference whether or not you copy it. Read this next to `docs/macos.md`, +which records where pardes deliberately went the other way. + +## The seam is small, and it is a hand-written C header + +`include/ghostty.h` is 1207 lines for an application of ghostty's size: about 90 +exported functions, four opaque handles (`typedef void* ghostty_app_t` and +friends, `:56-60`), and one callback struct. Everything is a `void*` on the C +side; the Zig `export fn` signatures carry the real types, so the compiler checks +one end and the other is untyped by construction. + +The header is **not generated**. It is maintained by hand, with `IMPORTANT: Any +changes here update include/ghostty.h` comments scattered through the Zig +(`src/input/key.zig:83`, `src/apprt/action.zig:75`). What keeps it honest is a +test: `build.zig:359-365` runs `addTranslateC` over the header and imports the +result into the unit-test build, and `src/lib/enum.zig:91-141` walks every Zig +enum asserting each tag equals the matching `GHOSTTY_*` constant, failing in both +directions — a Zig tag with no C constant, and a C constant with no Zig tag. + +This is the single most transferable thing in the repository, it costs about ten +lines of build plumbing, and it runs on Linux. pardes now does the same thing in +`src/macos.zig`, extended to compare exported function arity and scalar widths as +well, because a hand-written header drifts in signatures too. + +## Zig→host is one callback, not N + +`ghostty_runtime_config_s` (`include/ghostty.h:999-1027`) holds `userdata` and +six function pointers: `wakeup`, `action`, `read_clipboard`, +`confirm_read_clipboard`, `write_clipboard`, `close_surface`. Only `wakeup` and +the clipboard ones are what they look like. Everything else the core wants from +the host — new window, set title, toggle fullscreen, desktop notification, +sixty-odd cases — travels through the single `action` callback as a tagged union. + +The union is generated at comptime from the action enum +(`src/apprt/action.zig:419-484`) with `@Union(.@"extern", ...)`, and asserted to +a fixed size so adding a case cannot silently move the ABI: + +```zig +comptime { + assert(@sizeOf(CValue) == switch (@sizeOf(usize)) { 4 => 16, 8 => 24, else => unreachable }); +} +``` + +The payoff is that adding a feature adds an enum case, not a struct field: hosts +that do not implement it return `false` and the ABI never changed. Worth copying +the day a fixed callback list starts growing. pardes has two callbacks today and +does not need it yet. + +Two userdata scopes, worth noting: the runtime struct's `userdata` is +app-scoped, and each surface carries its own. Swift round-trips both with +`Unmanaged.passUnretained(self).toOpaque()` — libghostty never retains, so +lifetime stays entirely on the Swift side. + +## Rendering: Zig owns the layer, Swift never draws + +This is the surprising part and the main thing pardes did *not* copy. + +Swift creates an `NSView`, gives it a non-zero frame, and passes the bare +pointer through a tagged platform union (`include/ghostty.h:448-465`): + +```c +typedef struct { void* nsview; } ghostty_platform_macos_s; +``` + +Zig then makes that view layer-hosting and installs a layer *it* owns +(`src/renderer/Metal.zig:108-146`): + +```zig +var layer = try IOSurfaceLayer.init(); +info.view.setProperty("layer", layer.layer.value); +info.view.setProperty("wantsLayer", true); +``` + +Note the order — assigning `.layer` before `wantsLayer = true` is what makes the +view layer-*hosted*, meaning AppKit will never draw into it. And it is not a +`CAMetalLayer` or an `MTKView`: `src/renderer/metal/IOSurfaceLayer.zig` builds a +`CALayer` subclass at runtime with `objc.allocateClassPair`, renders into an +`IOSurface`, and sets it as `layer.contents`. + +The consequence is that Swift is not in the render loop at all. There is no +`ghostty_surface_draw` call anywhere in the Swift sources; the renderer thread +drives its own vsync. Swift pushes only metadata: `set_size` (from a SwiftUI +`GeometryReader`, not `NSView.resize` — macOS 12 did not call it), +`set_content_scale` from `viewDidChangeBackingProperties`, `set_display_id` so +Zig can pick the right refresh rate, and `set_occlusion`. + +The debug inspector is the counter-example and shows both models are supported: +there Swift owns an `MTKView` and passes a command buffer *into* Zig +(`ghostty_inspector_metal_render`, `macos/Sources/Ghostty/Surface View/InspectorView.swift:417`). + +pardes inverts this deliberately — see `docs/macos.md`. The short version: a +terminal grid rendered by a hand-written Metal atlas is what `src/gui/gui.zig` +already spends thousands of lines on, and a cell grid is exactly what CoreText +draws natively. + +## Threading + +Per surface, ghostty spawns two threads (`src/Surface.zig:727-741`): a renderer +thread owning Metal and its own frame clock, and an IO thread doing pty reads and +VT parsing. Both run libxev loops with a `BlockingQueue` mailbox. + +Everything in `ghostty.h` is main-thread-only. The one documented exception is +`wakeup`, and its whole Swift body is a hop: + +```swift +DispatchQueue.main.async { state.appTick() } +``` + +The detail worth stealing: the wakeup is baked into the mailbox push +(`src/App.zig:559-576`), so it is impossible to enqueue without waking. Worker +threads never call host callbacks directly — `action_cb` fires from +`drainMailbox` on the main thread, so although `wakeup` is any-thread, in +practice every other callback is not. + +## Build plumbing, and how much of it is distribution + +Two build systems glued together, and ghostty's own CI never uses the integrated +path — `zig build -Demit-macos-app=false` then `xcodebuild`, because Nix breaks +xcodebuild (`.github/workflows/release-tip.yml:436-441`, and `macos/AGENTS.md` +says the same to humans). + +The Zig half: + +- `src/build/GhosttyLib.zig:19-58` — a static lib rooted at `src/main_c.zig` + with `bundle_compiler_rt = true` and `bundle_ubsan_rt = true`, then every + dependency archive merged into one. The comment is blunt: *"These must be + bundled since we're compiling into a static lib. Otherwise, you get undefined + symbol errors."* pardes's `build.zig` copies exactly this. +- `LibtoolStep.zig` / `CombineArchivesStep.zig` — `libtool -static` on Darwin, + an MRI script through `zig ar -M` elsewhere. Note the workaround at + `LibtoolStep.zig:54-78`: newer Xcode `libtool` drops 64-bit archive members + unless each input is copied and `ranlib`'d first. +- `LipoStep.zig` — 42 lines, `lipo -create`, fed by building the same static lib + twice against retargeted deps (`GhosttyLib.zig:169-199`). +- `XCFrameworkStep.zig` — 77 lines, `xcodebuild -create-xcframework`. It writes + to a fixed repo-relative path outside the Zig cache, which is why it needs + `has_side_effects` and an explicit `rm -rf`; the price of a stable path for + Xcode to reference. +- `GhosttyXCFramework.zig:56-60` stages a headers directory containing *only* + `ghostty.h` plus `module.modulemap`, because pointing at `include/` directly + drags in the separate libghostty-vt headers and Clang complains that the + umbrella header does not include them. + +The Xcode side needs almost nothing: one `PBXFileReference` for the xcframework +with `sourceTree = "<group>"`, added to the frameworks build phase, plus +`OTHER_LDFLAGS = -lstdc++` for the vendored C++. No xcconfig, no search paths, no +bridging header for libghostty — Swift says `import GhosttyKit` and the module +map inside the framework does the rest. (The bridging header that does exist is +for two unrelated Objective-C helpers.) + +Resources are Xcode *folder references* pointing at `../zig-out/share/*`, so Zig +writes the tree and Xcode copies it verbatim. Fonts are not resources at all — +they are `@embedFile`'d (`src/font/embedded.zig`). Metal shaders are compiled to +a `.metallib` by `xcrun` at Zig build time and embedded into the library +(`MetallibStep.zig`, `SharedDeps.zig:504-511`), which is much simpler than +routing shaders through Xcode build rules. + +**What genuinely needs a Mac:** `libtool`, `lipo`, `xcodebuild`, `xcrun metal`, +`dsymutil`, `codesign`, and Swift itself. Everything else — including the +header-drift test — runs on Linux. `flake.nix:63-66` excludes Darwin from +buildable platforms entirely. + +pardes skips lipo, the xcframework, the Xcode project and codesigning. All four +exist to ship a signed universal bundle to someone else's machine; a dev build +needs a `swiftc` invocation and a directory with a plist in it. They are the +named upgrade path in `docs/macos.md`, in that order. + +## Small things worth remembering + +- Enum value 0 is reserved as "invalid" throughout the C API + (`PlatformTag` at `src/apprt/embedded.zig:390-395`), so a zero-initialized + struct is detectable, and tags are validated with `intToEnum` rather than + trusted. +- Zig errors never cross the boundary. Every export is a thin wrapper: + `fn foo_() !T` plus `catch |err| { log; return null; }` + (`src/apprt/embedded.zig:1398-1406`). There is no error channel at all. +- The exports live inside `pub const CAPI = struct {...}` and are only emitted + because `src/main_c.zig:34-47` references the struct in a `comptime` block. +- libghostty defends against host garbage rather than trusting it — + `updateContentScale` rejects NaN and clamps below 1 with the comment *"We are + an embedded API so the caller can send us all sorts of garbage"* + (`src/apprt/embedded.zig:777-793`), and `updateSize` drops no-op resizes + because *"Runtimes sometimes generate superfluous resize events even if the + size did not actually change (SwiftUI)"*. +- Swift keeps a dependency-free `GhosttyPackageMeta.swift` so auxiliary bundles + (the dock tile plugin) can share types without linking the Zig library. +- The Swift wrapper layer compiles unchanged for iOS, which is the real proof + that the C seam is the right width. It manages that by turning Zig→host + actions into `NSNotification`s keyed on the surface, decoupling the callback + from any particular view controller. diff --git a/docs/macos.md b/docs/macos.md new file mode 100644 index 00000000..35fba5f3 --- /dev/null +++ b/docs/macos.md @@ -0,0 +1,252 @@ +# Native macOS backend + +Zig owns the core, the ptys, every effect and the worker threads. Swift owns +`NSApplication`, the window, input translation, and drawing. They meet at a +hand-written C ABI, `src/macos/pardes.h`, built as a static library that the app +links; the Swift half is ordinary AppKit that pumps events in and reads one +packed cell buffer out. There is exactly one core per process, so the ABI has no +handles — the state is a file-scoped singleton and every function names it +implicitly, exactly like the browser backend. + +The research this design came out of is written down separately in +`docs/ghostty-macos-notes.md` — how ghostty wires Zig to Swift, what its build +plumbing actually requires, and which parts of it are distribution machinery +rather than integration. Read that for the alternatives; this file is what was +built and why. + +## Why not ghostty's split + +Ghostty was read carefully before this was written, and this backend +deliberately inverts its division of labour. Ghostty hands Zig a bare `NSView*` +through a tagged platform union (`ghostty_platform_macos_s.nsview`), and Zig +then creates a layer, assigns it as the view's layer, sets `wantsLayer`, and +installs a display callback — `src/renderer/Metal.zig` in that tree. Swift never +renders a glyph; it supplies a rectangle and gets out of the way. + +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,300 lines are a 2048² stb_truetype R8 atlas, +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, +never once run — whereas CoreText drawing is a small amount of Swift that only +exists on the machine that can run it. + +The ceiling is accepted and named. Per-cell CoreText drawing is slower than an +atlas, and if it fails to hold a full-screen redraw at the target rate the +escalation is ghostty's model verbatim: Zig owns a `CAMetalLayer` installed into +the view and drives its own frame clock, and the ABI grows a `platform` pointer +field carrying the `NSView*` — one field, because the rest of the seam does not +change. Nothing here is designed to make that harder. + +## Why the ABI mirrors src/web.zig + +The browser and a Cocoa app are the same host, and the browser proved the shape +first. In both, someone else owns the clock and the event loop; events arrive +through flat functions that take scalars and borrowed byte ranges; one call +renders, and the result is one contiguous array of packed cells the host walks +linearly. `pardes_cell_s` is byte-for-byte `WebCell`: seven UTF-8 bytes of +grapheme plus a guaranteed zero, tagged `fg`/`bg` words (`0x01000000` default, +`0x02000000 | index` for the palette, otherwise `0x00RRGGBB`), an attribute +bitfield with the underline style in the high bits, and a `flags` bit meaning +"the core never painted this cell" so a host can draw background only and skip +the glyph. Two hosts spelling the same encoding is not duplication worth +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. + +## The seam + +**Frame.** `pardes_frame` renders and returns the cell count; +`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. + +**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 +characters. Keys that carry no text are the four ASCII controls (enter, escape, +tab, backspace) and a private-use block starting at `0xF0001` for the arrows and +navigation keys — private-use precisely so a functional key can never be +confused with a real codepoint arriving as text. `pardes_mouse` speaks acme's +vocabulary: three buttons, where 1 selects, 2 executes and 3 looks, with wheel +directions as ordinary buttons rather than a separate axis. Ctrl is the only +modifier the core consults (a left press with ctrl is goto-definition), but the +full mask is passed anyway to keep the signature identical to the web one. +`pardes_scroll` carries a fractional row delta from a trackpad; the Zig side +accumulates it and synthesizes whole `wheel_up`/`wheel_down` presses, because +the core scrolls on button events and its `touch_scroll` event only records the +residual for the debug overlay. `src/gui/gui.zig` (`takeScrollTicks`) and +`src/web/app.mjs` both do exactly this already. A real mouse notch skips the +smoothing and goes through `pardes_mouse`. `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, +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. + +**Lifecycle.** `pardes_init` returns 0 or an opaque nonzero code, +`pardes_deinit` tears down, `pardes_tick` drains pty output plus the effect +queue and returns whether anything changed, `pardes_should_quit` reports the +Exit builtin or the last pane closing, and `pardes_animating` says a theme +transition wants ~60 Hz ticks until it settles — the one thing in an otherwise +event-driven frontend that redraws on a clock, handled in `tty.zig` by sleeping +the loop and forcing a tick. + +The ordering contract is the part a header cannot enforce. **Init with the real +grid size.** The core defers each shell's greeting until it has seen a resize: +`sync()` in `src/pardes.zig` only emits the opening `ls` once `resize_count > 0` +and the pty has produced its first prompt. Setting `Options.cols`/`rows` alone +never bumps that counter, so init must turn its arguments into an actual resize +event the way `src/web.zig` does immediately after construction — and the size +must be true, because the first `forkpty` takes its winsize from the core's +current grid and a shell booted at a placeholder draws its first prompt at the +wrong width. **Call `pardes_tick` after every input function.** The input calls +only advance the state machine; the writes to the pty, the spawns, the saves all +happen in the drain, so an input without a following tick is an input that +visibly did nothing. **`wakeup` is the only any-thread entry point in the whole +ABI.** Everything else is main-thread only, and the host's `wakeup` must do +nothing but hop — one `DispatchQueue.main.async` that calls `pardes_tick` and +marks the view dirty. + +## 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. + +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 +and must render independently of input. Pardes does not own the clock here — the +host does, through `wakeup` and dirty rects — so there is no third thread to +synchronize and no mailbox protocol to get wrong. + +## Building + +Two commands, because they are two different machines' problems. + +```sh +zig build -Dplatform=macos +``` + +produces `zig-out/lib/libpardes.a` and installs `zig-out/include/pardes.h` +beside it. This is ordinary Zig cross-compilation and runs anywhere, which is +the whole point of the next section. + +```sh +zig build macos-app -Dplatform=macos # on a Mac; or run the script directly +``` + +runs `src/macos/build-app.sh`, which is the whole second half: + +```sh +swiftc -O -import-objc-header src/macos/pardes.h \ + -o zig-out/pardes.app/Contents/MacOS/pardes \ + src/macos/Sources/*.swift \ + zig-out/lib/libpardes.a -lc++ \ + -framework AppKit -framework CoreText -framework CoreGraphics +``` + +plus copying `Contents/Info.plist`, and the bundle is done. 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. `-lc++` is there because +ghostty-vt pulls in simdutf and highway, which are C++ — the Zig side bundles +`compiler_rt` and `ubsan_rt` into the archive (`bundle_compiler_rt` in +`build.zig`), so the C++ runtime is the only thing left for this link to supply. +Without that bundling the swiftc link ends in undefined symbols, which is the +first thing ghostty's `GhosttyLib.initStatic` does too. + +No Xcode project, no xcframework, no `lipo`, no codesigning — ghostty has all four +(`macos/Ghostty.xcodeproj`, `src/build/GhosttyXCFramework.zig`, the entitlements +files), and every one of them exists for *distribution*: a universal binary for +two architectures, a framework other targets can consume, a signature and +notarization for Gatekeeper. A dev build that runs on the machine that compiled +it needs none of it. They are the named upgrade path, in that order: `lipo` when +a second architecture matters, an xcframework when something other than this app +links the core, codesigning and entitlements the day it is handed to someone +else. + +## 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: + +```sh +zig build unit-test -Dplatform=macos +``` + +Only the Swift app needs a Mac. That is what makes this scaffold verifiable +rather than dead code: the ABI's Zig side, the pty plumbing and the effect drain +are all exercised on the machine they were written on, and the part that cannot +be is small, visible, and made of AppKit calls. + +The header is kept honest with ghostty's trick. `build.zig` runs `translate-C` +over `src/macos/pardes.h` into the unit-test build, and `src/macos.zig` +asserts every constant and every struct layout against the Zig side — the color +tags, the attribute bits, the key codepoints, the mouse and modifier ordinals, +`@sizeOf(pardes_cell_s)` and each field offset. A hand-written header is a +second source of truth, and the only defensible way to keep one is a test that +fails the moment the two disagree. + +A second test compares every exported function's arity and scalar widths against +the header's declaration. It is not a type equality — `translate-C` spells +pointers `[*c]` and mints its own struct types, so nothing would ever match +exactly — but arity and width are what actually break. It earned its place +immediately: `pardes_scroll` grew a cell coordinate after the Swift view had +already been written against the one-argument form, and nothing but a human +reading both files would have caught it. + +## Not implemented + +- **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. +- **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 + `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 + either. +- **IME and marked text.** Only finished characters reach `pardes_key`. Real + composition means implementing `NSTextInputClient` and giving the core a way + to render an underlined preedit run, which no backend has yet. +- **Tabs and splits at the window level.** One window, one grid. Pardes's own + columns and panes are the layout, and a second window would need a second + core, which the singleton ABI is precisely a decision not to have yet. +- **Native image and PDF placement.** `Surface.images` is ignored. The tty + backend draws these with Kitty graphics and the SDL one with GPU textures; + macOS would be a third path, a `CALayer` or `CGImage` per placement positioned + from the cell rectangle. `pardes_resize` already carries the physical cell + size that path needs. +- **App-bundle resources.** The plist is the minimum that makes a windowed app: + no icon, no bundled fonts, no asset catalog, no localization. +- **argv and file-open handling.** No positional path, no `-l` dump load, and no + `application:openFile:`. The first pane spawns with an empty cwd, so the shell + inherits the process's — which for a bundle launched from Finder is `/`, and + is the first thing worth fixing here. |
