summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-01 13:46:44 -0300
committerGabriel Schneider <[email protected]>2026-09-01 13:57:41 -0300
commitcce6b18a49870086982f9a0e1fda90ed170b9fba (patch)
treee5ce990d0a1d43503375d891bc51f283e0a1c215 /src/pardes.zig
parentae9325a5cb128d0d952afb8f9feaaca68e5e37a2 (diff)
downloadpardes-cce6b18a49870086982f9a0e1fda90ed170b9fba.tar.gz
pardes-cce6b18a49870086982f9a0e1fda90ed170b9fba.zip
macos: one tagline rule for both hosts, a kqueue beside the inotify, and effects that compile
Three things this shell had its own copy of, and in each case the fix is that it stops having one. **The tagline band.** A pane tag draws at `gui_tagline_font_percent` of the body face and the band it sits on shrinks with it, while the grid row stays body-sized — so something has to decide where the shorter band sits in the taller row. This shell decided by centring, always, which is precisely the case `config.gui_topbar_pane_border_px` exists to prevent: the topbar's unused half-band meets the first pane tag's unused half-band and the window background shows through the seam. The strip is as wide as the bands are short — on a 20-pixel cell, 4 physical pixels at the default 82%, 10 at 50%, 14 at 30% — so it grew as the tagline face shrank and read as "the tagline is wrong on the mac" rather than as one missing rule. The rule is `pardes.taglineBandOffset` in the core now and both pixel hosts call it: row zero bottom-aligned, the first pane-tag row top-aligned, the two joined by `gui_topbar_pane_border_px` in the theme's scrollbar-track colour, every row between centred, and a `Tagbottom` band on the final row flush with the window edge — with the sub-cell strip beneath it painted in that band's own colour, because the core grid holds only whole cells and a window is any height it likes. `pardes_tagline_band_offset`, `pardes_topbar_pane_border_px` and `pardes_topbar_pane_border_rgb` carry it over the C ABI as PHYSICAL pixels: the host multiplies its points by the backing scale going in and divides coming out, which is the snapping `Metrics` already does for the cell, and is what keeps a one-pixel rule one pixel instead of a two-pixel smear. **The watch.** `file_watch.zig` was one mark/reconcile transaction over `inotify`, so the tty shell, the SDL window and the detached daemon all watched nothing off Linux: an edit made outside pardes never reached the pane, and a PDF replaced on disk kept rendering the old inode. It is the same transaction over two kernels now — `init`, `wait`, `stop`, `drain`, `markDir` and `unmarkDir` are still the whole of it, and the hosts wait on a kqueue and poll it exactly as they did the old descriptor. A macOS mark is TWO filters, because a kqueue directory filter reports its entries changing and never a write to a file already inside it: the parent mark follows rename-over saves, `markFile` catches in-place writes, and `remarkFile` re-arms the file filter once a rename has moved the inode. That is the same pair the AppKit host's DispatchSources already used for the same reason. Directory marks are deduplicated here by device and inode, because each `EVFILT_VNODE` filter needs a descriptor of its own and inotify did that deduplication itself; `stop` and `drain` wake through the one `EVFILT_USER` filter, since a kqueue cannot simply be read the way an inotify descriptor can. **The effects.** The three `crt.ci.metal` entry points are `extern "C" [[stitchable]]`. `CIKernel.kernels(withMetalString:)` compiles that source at runtime, looks for stitchable functions, and rejects the WHOLE source with "cannot find a valid stitchable Metal function in the source" when it finds none — so `ScenePostprocessor.init?` returned nil and every scene effect and panel transition silently degraded to the plain CoreText draw. The `effect_sources.zig` test pins the exact spelling of all three, and `draw-effect` in the e2e suite catches the degradation rather than the spelling. Beside them, the offscreen harness owes the core a PRESENTATION. Its window is borderless and never ordered front, so AppKit runs no display cycle and `pardes_frame_presented` — whose only caller is `draw(_:)` — never fired. The core holds pointer gestures inert while a layout mutation has not reached a backend, which for an unpresenting harness is the rest of the script: the first pane a script opened silently killed every later click, drag and Look. So `readFrame` presents what it just rendered, into a bitmap nobody reads. `PARDES_CHROME` also looks under `/Applications`, where a browser's executable lives inside an application bundle and never on `PATH`. The macOS goldens are regenerated; docs/macos.md, config.md, detached.md, web.md and the design PDF follow.
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig53
1 files changed, 47 insertions, 6 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index bc9a0939..1eae2113 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -199,6 +199,36 @@ pub const MAX_JUMPS = 64;
pub const TOPBAR_H: u16 = 1;
pub const BOX_H: u16 = 1;
+/// Where a reduced-height tagline band sits inside its body-sized grid row, in
+/// physical pixels down from the row's top. ONE rule for both pixel hosts: the
+/// SDL shell (`src/gui/gui.zig`) and the AppKit shell (`src/macos.zig`, over the
+/// C ABI) both call this. It lived in gui.zig, the native shell grew its own
+/// copy that only ever centred, and centring is precisely the case
+/// `config.gui_topbar_pane_border_px` exists to avoid: two half-bands touching
+/// with a strip of window background showing between them, widening as the
+/// tagline face shrinks.
+///
+/// Row zero and the first pane-tag row face a shared rule instead of centering
+/// two independent bands. A Tagbottom band on the final grid row faces the
+/// window edge, eliminating the matching unused half-band at the bottom.
+pub fn taglineBandOffset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32) u32 {
+ const spare = cell_h -| tagline_h;
+ const border = topbarPaneBorderPixels(cell_h, tagline_h);
+ if (row == 0) return spare - border / 2;
+ if (row == TOPBAR_H) return border - border / 2;
+ const rows: u32 = @max(1, @as(u32, @intFromFloat(@floor(canvas_h / @as(f32, @floatFromInt(cell_h))))));
+ if (@as(u32, row) + 1 >= rows) return spare;
+ return spare / 2;
+}
+
+/// The rule between the topbar band and the first pane-tag band, clamped to the
+/// spare pixels those two bands have between them so a wide compiled value
+/// cannot paint over either.
+pub fn topbarPaneBorderPixels(cell_h: u32, tagline_h: u32) u32 {
+ const spare = cell_h -| tagline_h;
+ return @min(@as(u32, config.gui_topbar_pane_border_px), spare * 2);
+}
+
/// A place the keyboard has been: a pane AND a spot in it, which is the whole
/// upgrade over the stack of bare pane ids this replaces — Ctrl-o can now
/// rewind WITHIN a pane, and a Jumplist row can name a line.
@@ -330,7 +360,10 @@ fn drainForAttach(p: *Pardes) ?AttachRequest {
}
test "Attach asks for a session and tears nothing down" {
- if (comptime !hosted) return; // no unix socket on this platform, so no word
+ // `can_attach` and not `hosted`: 29ac9be compiled both words out of a
+ // frontend that never polls `takeAttach`, which is macOS — hosted, with a
+ // unix socket, and still no word to run here.
+ if (comptime !can_attach) return;
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .tty_only = true });
defer p.deinit();
@@ -356,7 +389,7 @@ test "Attach asks for a session and tears nothing down" {
}
test "Detach asks the frontend to leave, and says so when there is nothing to leave" {
- if (comptime !hosted) return; // no unix socket on this platform, so no word
+ if (comptime !can_attach) return; // compiled out where nothing polls `takeAttach` — see `can_attach`
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .tty_only = true });
defer p.deinit();
@@ -1384,12 +1417,20 @@ test "a Look on a directory does not type ls into an occupied terminal" {
while (p.nextEffect()) |_| {}
// /tmp rather than a made-up name: the look resolves against the real
- // filesystem, so the directory has to exist for this arm to be reached
- p.setCwd(0, "/tmp");
+ // filesystem, so the directory has to exist for this arm to be reached —
+ // and in the spelling `resolve` HANDS BACK, which is the realpath. `/tmp`
+ // is itself on Linux and a symlink to `/private/tmp` on Darwin, and the
+ // `.dir` arm matches a pane by `cwdSlice()` against that realpath, on the
+ // documented invariant that pane paths are canonical (see `lookAt`). A
+ // literal "/tmp" therefore matched nothing on a Mac and forked a second
+ // terminal for a directory that already had one.
+ var realbuf: [4096]u8 = undefined;
+ const tmp = look.resolve("/tmp", "/", &realbuf).dir;
+ p.setCwd(0, tmp);
var host: FakeTtyQuery = .{};
host.install(p);
host.taken[0] = true;
- p.lookAt(0, "/tmp");
+ p.lookAt(0, tmp);
var spawned = false;
while (p.nextEffect()) |effect| switch (effect) {
@@ -1402,7 +1443,7 @@ test "a Look on a directory does not type ls into an occupied terminal" {
// ...and the same look reuses that very pane once its program is gone: the
// core keeps no state of its own about it, so recovery needs nothing reset
host.taken[0] = false;
- p.lookAt(0, "/tmp");
+ p.lookAt(0, tmp);
var sent: [64]u8 = undefined;
var sent_len: usize = 0;
while (p.nextEffect()) |effect| switch (effect) {