summaryrefslogtreecommitdiff
path: root/src/macos.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-22 11:15:46 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit31cb659ded4cf50af5903fc107f8c868ee3c7311 (patch)
tree8ceaf0ae085cfee121a1b4bdcb923b2c03b01a60 /src/macos.zig
parenta1d5ee19a648abc65b557dffa14a3b2f70577286 (diff)
downloadpardes-31cb659ded4cf50af5903fc107f8c868ee3c7311.tar.gz
pardes-31cb659ded4cf50af5903fc107f8c868ee3c7311.zip
Answer 9P on the connection's task, so a session can open its own tree
The editor's loop was the only thing that could answer a 9P request, which made the editor's own syscalls through a mount of its own tree -- a Look at /mnt/9p/pardes/<me>/anything under a `9ns --mntgen` view, a Save into it -- requests only the blocked loop could serve. The name-based refusal that followed (ownMountSuffix) and the in-process routing of a mount of oneself (Client.sameSession) were patches over that, and both are gone, with the mailbox that shipped every request to the editor's thread. One rule replaces them, `pardes.turn`: the core is single-threaded, the editor's thread has the turn by default and gives it up in two kinds of gap -- while it waits for input and while a step of it is out in a host syscall -- and a cloud9 connection task takes it in those gaps to answer. `out` counts the steps that are out, from any thread: while one is, the core reads consistently but that step still holds pointers into it, so a request that would change a pane (a write, a truncation, an rmdir) is parked in the engine and retried when the turn is next given up with nothing out, and the editor's own wake waits for the count to reach zero. It is never a write of its own that a step waits on out there -- writes come from a shell performing a save between steps -- so a parked request is never the syscall's own, and making a pane or rendering a screen need not park: every yield sits before its step's mutation, so the layout and the surface are whole under it. A changing request that queued effects is answered once the editor has performed them (`echo Save > exec` returns with the file written, as acme's `put` does), and it settles the way a step does, because without that a /log reader waited for the user's next keystroke. Every host syscall on a user path has to give the turn up, not fs.zig's alone: the first end-to-end run hung in `inotify_add_watch` performing the new pane's watch effect. PDFs and images are read whole at open, so no draw goes out into the host. The core's allocator takes its fixed buffer through the lock-free interface, since a connection task allocates while the editor's thread is out in a syscall that allocates too. A Restore puts the replacement in first and releases every task waiting on the old core. cloud9 (pinned at eb1a104) parks an open, a truncating wstat, a clunk and a remove on `again`, not only reads and writes, and answers a parked job whose fid was clunked without asking the backend. Verified: test/selfmount.py runs the editor under `9ns --mntgen` and Looks at, reads and Saves its own tree through the mount; a unit test pins that a change parks while the editor is out mid-step and lands when it rests, while a read is answered in the window. 9P over the Unix socket against a tty session, same machine, Debug builds: a read of /index 278us -> 61us, a truncating body write 1184us -> 609us, exec Save 718us -> 583us; the gesture benchmark is unchanged (geometric mean 0.997 over 53 cells). Also from the reviews: a notice chip over an image or PDF pane was painted out by the picture drawn after the cells, so pictures give up the rows; in the GUI a tree-sitter context band painted over the chip, so body layers are emitted first; a message is one row of printable text, its 256-byte cut never leaves half a glyph, and one wider than its pane keeps its tail (the file name, the reason) rather than its head. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'src/macos.zig')
-rw-r--r--src/macos.zig125
1 files changed, 123 insertions, 2 deletions
diff --git a/src/macos.zig b/src/macos.zig
index d3b33c55..a7ee3164 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -773,6 +773,10 @@ fn initCore(runtime: ?*const Runtime, cols_arg: u16, rows_arg: u16) !void {
for (&st.ptys, 0..) |*slot, id| if (slot.*) |*pt| startReader(st, pt, @intCast(id));
pardes.lsp.setStatusSink(st, lspStatusSink);
+ // From here the core is touched only inside the entry points above,
+ // each of which takes the turn for its own duration; between them the
+ // 9P connections have it.
+ pardes.turn.rest();
}
fn wakeNinep(ctx: ?*anyopaque) void {
@@ -781,6 +785,7 @@ fn wakeNinep(ctx: ?*anyopaque) void {
}
export fn pardes_deinit() void {
+ pardes.turn.wake();
const st = &(state orelse return);
if (st.ninep) |listener| {
listener.deinit(st.gpa);
@@ -823,6 +828,8 @@ export fn pardes_deinit() void {
}
export fn pardes_should_quit() bool {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return true);
return st.core.quit;
}
@@ -890,11 +897,15 @@ test "the animation clock spends real time, not callbacks" {
}
export fn pardes_animating() bool {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return false);
return st.core.animationActive() or st.rotate_coasting or st.scroll_coasting;
}
export fn pardes_theme_bg() u32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const th = if (state) |*st| st.core.theme() else &pardes.themes[0];
const bg = th.bg orelse return color_default;
return @as(u32, bg[0]) << 16 | @as(u32, bg[1]) << 8 | bg[2];
@@ -913,6 +924,8 @@ export fn pardes_theme_bg() u32 {
/// own (pardes_theme_bg == PARDES_COLOR_DEFAULT) drops the ground entirely,
/// whatever percentage is set here.
export fn pardes_window_opacity() u8 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return 100);
st.core.settings.window_opacity_pending = false;
return st.core.settings.window_opacity;
@@ -920,6 +933,8 @@ export fn pardes_window_opacity() u8 {
/// Public AppKit backdrop strength, independent of background paint opacity.
export fn pardes_window_blur() u8 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
return if (state) |st| st.core.settings.window_blur else 0;
}
@@ -928,10 +943,14 @@ fn taglineFontPercent(core: ?*const pardes.Pardes) u8 {
}
export fn pardes_gui_tagline_font_percent() u8 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
return taglineFontPercent(if (state) |*st| st.core else null);
}
export fn pardes_tagline_band_offset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32) u32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
return pardes.taglineBandOffset(row, canvas_h, cell_h, tagline_h, if (state) |*st| st.core.settings.workspace_tag else true);
}
@@ -940,16 +959,22 @@ export fn pardes_topbar_pane_border_px(cell_h: u32, tagline_h: u32) u32 {
}
export fn pardes_tagline_origin_col(col: u16, row: u16) f32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return @floatFromInt(col));
return pardes.taglineOriginColForFrame(st.core, col, row);
}
export fn pardes_grid_col_at(x: f32, row: u16, body_w: f32, tagline_w: f32) u16 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return pardes.gridColAt(null, x, row, body_w, tagline_w));
return pardes.gridColAt(st.core, x, row, body_w, tagline_w);
}
export fn pardes_topbar_pane_border_rgb() u32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const rgb = pardes.config.gui_topbar_pane_border_rgb orelse fromTheme: {
const st = state orelse return color_default;
break :fromTheme st.core.chromeTheme().border;
@@ -958,11 +983,15 @@ export fn pardes_topbar_pane_border_rgb() u32 {
}
export fn pardes_tag_active_bg() u32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = state orelse return color_default;
const rgb = st.core.chromeTheme().tag_active_bg;
return @as(u32, rgb[0]) << 16 | @as(u32, rgb[1]) << 8 | rgb[2];
}
export fn pardes_tagline_bg() u32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = state orelse return color_default;
const rgb = st.core.chromeTheme().tag_bg;
return @as(u32, rgb[0]) << 16 | @as(u32, rgb[1]) << 8 | rgb[2];
@@ -1008,6 +1037,8 @@ test "the fallback preference order crosses the ABI intact and ends at the bound
}
export fn pardes_scene() Scene {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return .{});
const seconds = @as(f64, @floatFromInt(st.scene_ns)) / @as(f64, std.time.ns_per_s);
return .{
@@ -1018,6 +1049,8 @@ export fn pardes_scene() Scene {
}
export fn pardes_postprocessor_unavailable() void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
st.core.disableSceneEffects();
st.core.settings.panel_transition = .off;
@@ -1026,6 +1059,8 @@ export fn pardes_postprocessor_unavailable() void {
}
export fn pardes_panel_animation_failed() void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
st.core.abandonPanelAnimations();
}
@@ -1210,6 +1245,8 @@ fn reloadWatchedFile(st: *State, pane: u8, announce: bool) bool {
}
export fn pardes_watch_changed(pane: u8, generation: u32) void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
if (pane >= watch_slot_count) return;
if (st.file_watches.notify(pane, generation)) wake(st);
@@ -1265,11 +1302,13 @@ fn drainInbox(st: *State) bool {
}
export fn pardes_tick() bool {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return false);
st.inbox.wake_pending.store(false, .release);
var did = drainInbox(st);
if (st.ninep) |listener| {
- const drained = listener.tick(st.core);
+ const drained = listener.tick();
did = did or drained.count != 0;
if (drained.pending) wake(st);
}
@@ -1277,7 +1316,9 @@ export fn pardes_tick() bool {
did = true;
st.core.perform(effect);
}
+ pardes.turn.settle();
if (restoreCore(st)) did = true;
+ pardes.turn.restoreSettled();
return did;
}
@@ -1309,7 +1350,7 @@ fn restoreCore(st: *State) bool {
const generation = st.file_watches.stop(st.gpa, id);
hostWatchFile(id, generation, null, 0);
};
- if (st.ninep) |listener| listener.reset(st.core);
+ if (st.ninep) |listener| listener.reset(replacement);
replacement.host = hostFor(st);
st.core.deinit();
st.core = replacement;
@@ -1485,6 +1526,8 @@ test "mac Restore cancels a PTY reader waiting for output capacity" {
}
export fn pardes_animation_tick() bool {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return false);
const now: u64 = @intCast(@max(0, monotonicNs()));
@@ -1546,6 +1589,8 @@ export fn pardes_animation_tick() bool {
}
export fn pardes_key(cp_arg: u32, text_ptr: ?[*]const u8, len: usize, mods: u32) void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
if (cp_arg > std.math.maxInt(u21)) return;
const text: []const u8 = if (text_ptr) |p| p[0..len] else "";
@@ -1559,16 +1604,22 @@ export fn pardes_key(cp_arg: u32, text_ptr: ?[*]const u8, len: usize, mods: u32)
}
export fn pardes_paste(text_ptr: ?[*]const u8, len: usize) void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
const text: []const u8 = if (text_ptr) |p| p[0..len] else "";
st.core.update(.{ .paste = text });
}
export fn pardes_mouse(button_arg: c_int, kind_arg: c_int, col: u16, row: u16, mods: u32) void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
mouseWithHit(button_arg, kind_arg, col, row, mods, null, null);
}
export fn pardes_mouse_pixel(button_arg: c_int, kind_arg: c_int, col: u16, row: u16, mods: u32, x: f32, y: f32, bw: f32, bh: f32, tw: f32, th: f32) void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
const source = st.core.presentation.pointerFractional(st.core.screen_w, st.core.screen_h, x / @max(1, bw), y / @max(1, bh));
var hit: ?pardes.Mouse.BodyHit = null;
@@ -1620,11 +1671,15 @@ fn mouseWithHit(button_arg: c_int, kind_arg: c_int, col: u16, row: u16, mods: u3
}
export fn pardes_pointer_leave() void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
st.core.update(.pointer_leave);
}
export fn pardes_scroll(delta_rows: f32, delta_cols: f32, col: u16, row: u16) void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
st.scroll_col = col;
st.scroll_row = row;
@@ -1636,6 +1691,8 @@ export fn pardes_scroll(delta_rows: f32, delta_cols: f32, col: u16, row: u16) vo
/// spinning dial. Also drops whatever the last gesture left banked, so the
/// first millimetre of a new swipe cannot inherit a nearly-complete row.
export fn pardes_scroll_begin() void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
st.scroll_lag = 0;
st.scroll_lag_x = 0;
@@ -1655,6 +1712,8 @@ export fn pardes_scroll_begin() void {
/// momentum simply takes the gesture over, and the tail of that momentum is
/// below the fling floor, so its end arms nothing.
export fn pardes_scroll_end() void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
const last = st.scroll_last_ns;
st.scroll_last_ns = 0;
@@ -1671,6 +1730,8 @@ export fn pardes_scroll_end() void {
}
export fn pardes_rotate(degrees: f32) void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
if (degrees == 0) {
st.rotate_lag = 0;
@@ -1684,6 +1745,8 @@ export fn pardes_rotate(degrees: f32) void {
}
export fn pardes_rotate_end() void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
const last = st.rotate_last_ns;
st.rotate_last_ns = 0;
@@ -1726,6 +1789,8 @@ fn spendRotation(st: *State, degrees: f32) void {
}
export fn pardes_command(text_ptr: ?[*]const u8, len: usize) void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
const text: []const u8 = if (text_ptr) |p| p[0..len] else "";
if (text.len == 0) return;
@@ -1733,6 +1798,8 @@ export fn pardes_command(text_ptr: ?[*]const u8, len: usize) void {
}
export fn pardes_resize(cols_arg: u16, rows_arg: u16, cell_w: u16, cell_h: u16) void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
const cols = @max(1, cols_arg);
const rows = @max(1, rows_arg);
@@ -1747,6 +1814,8 @@ export fn pardes_resize(cols_arg: u16, rows_arg: u16, cell_w: u16, cell_h: u16)
}
export fn pardes_frame() u32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return 0);
const tz = tracy.zone(@src(), "pardes_frame");
defer tz.end();
@@ -1921,26 +1990,36 @@ fn collectImages(st: *State, surface: *const pardes.Surface) void {
}
export fn pardes_frame_images() u32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return 0);
return @intCast(st.images_len);
}
export fn pardes_frame_image_list() ?[*]const Image {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return null);
return if (st.images_len == 0) null else st.images.ptr;
}
export fn pardes_frame_panel_tracks() u32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return 0);
return @intCast(st.panel_tracks_len);
}
export fn pardes_frame_panel_track_list() ?[*]const PanelTrack {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return null);
return if (st.panel_tracks_len == 0) null else st.panel_tracks[0..].ptr;
}
export fn pardes_frame_presented(animated_panels: bool) bool {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return false);
const was_animating = st.core.animationActive();
if (animated_panels)
@@ -1951,11 +2030,15 @@ export fn pardes_frame_presented(animated_panels: bool) bool {
}
export fn pardes_frame_cells() ?[*]const Cell {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return null);
return if (st.frame_len == 0) null else st.cells.ptr;
}
export fn pardes_frame_previous_cells() ?[*]const Cell {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return null);
return if (st.panel_diff_len != st.frame_len or st.panel_diff_len == 0)
null
@@ -1964,6 +2047,8 @@ export fn pardes_frame_previous_cells() ?[*]const Cell {
}
export fn pardes_frame_changed_cells() ?[*]const u8 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return null);
return if (st.panel_diff_len != st.frame_len or st.panel_diff_len == 0)
null
@@ -1972,16 +2057,22 @@ export fn pardes_frame_changed_cells() ?[*]const u8 {
}
export fn pardes_frame_cols() u16 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return 0);
return st.frame_cols;
}
export fn pardes_frame_rows() u16 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return 0);
return st.frame_rows;
}
export fn pardes_cursor_x() i32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return -1);
if (tagCursorCovered(st.core)) return -1;
return if (st.core.surface.cursor) |c| c.x else -1;
@@ -2000,6 +2091,8 @@ export fn pardes_tag_layer_limit() u32 {
}
export fn pardes_tag_layer_value(index: u32, field: u32) u32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return 0);
if (index >= pardes.MAX_TAG_LAYERS) return 0;
const layer = &st.core.surface.tag_layers[index];
@@ -2025,6 +2118,8 @@ export fn pardes_tag_layer_value(index: u32, field: u32) u32 {
}
export fn pardes_tag_layer_cells(index: u32) ?[*]const Cell {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return null);
if (index >= pardes.MAX_TAG_LAYERS) return null;
const layer = &st.core.surface.tag_layers[index];
@@ -2036,6 +2131,8 @@ export fn pardes_tag_layer_cells(index: u32) ?[*]const Cell {
}
export fn pardes_body_layer_value(index: u32, field: u32) u32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return 0);
if (index >= pardes.MAX_PANES) return 0;
const layer = &st.core.surface.body_layers[index];
@@ -2056,6 +2153,8 @@ export fn pardes_body_layer_value(index: u32, field: u32) u32 {
}
export fn pardes_body_layer_cells(index: u32) ?[*]const Cell {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return null);
if (index >= pardes.MAX_PANES) return null;
const layer = &st.core.surface.body_layers[index];
@@ -2071,27 +2170,37 @@ export fn pardes_body_layer_limit() u32 {
}
export fn pardes_row_metrics(body_w: u16, body_h: u16, tagline_w: u16, tagline_h: u16) void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
st.core.update(.{ .resize = .{ .cols = st.core.screen_w, .rows = st.core.screen_h, .cell_pixels = st.core.cell_pixels, .row_metrics = .{ .body_w = @max(1, body_w), .body_h = @max(1, body_h), .tagline_w = @max(1, tagline_w), .tagline_h = @max(1, tagline_h) } } });
}
export fn pardes_pointer_shape() u32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return 0);
return @intFromEnum(st.core.surface.pointer_shape);
}
export fn pardes_cursor_y() i32 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return -1);
if (tagCursorCovered(st.core)) return -1;
return if (st.core.surface.cursor) |c| c.y else -1;
}
export fn pardes_cursor_bar() bool {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return false);
return if (st.core.surface.cursor) |c| c.bar else false;
}
export fn pardes_take_haptic() c_int {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return 0);
return switch (st.core.takeHaptic()) {
.none => 0,
@@ -2103,6 +2212,8 @@ export fn pardes_take_haptic() c_int {
var font_path_z: [4096:0]u8 = undefined;
export fn pardes_font_take(size_hundredths: *u16) ?[*:0]const u8 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return null);
if (comptime !pardes.font_picker) return null;
const want = st.core.takeFontRequest() orelse return null;
@@ -2118,6 +2229,8 @@ export fn pardes_font_observe(
len: usize,
point_hundredths: u16,
) bool {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return false);
const ptr = effective_name orelse return false;
if (len == 0 or len > 255 or point_hundredths == 0) return false;
@@ -2129,6 +2242,8 @@ export fn pardes_font_ack(
len: usize,
point_hundredths: u16,
) bool {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return false);
const ptr = effective_name orelse return false;
if (len == 0 or len > 255 or point_hundredths == 0) return false;
@@ -2136,6 +2251,8 @@ export fn pardes_font_ack(
}
export fn pardes_font_reject() void {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return);
st.core.rejectFont();
}
@@ -2143,6 +2260,8 @@ export fn pardes_font_reject() void {
var active_path_z: [4096:0]u8 = undefined;
export fn pardes_active_path() ?[*:0]const u8 {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return null);
const path = activeFilePath(st) orelse return null;
if (path.len == 0 or path.len >= active_path_z.len) return null;
@@ -2152,6 +2271,8 @@ export fn pardes_active_path() ?[*:0]const u8 {
}
export fn pardes_active_dirty() bool {
+ pardes.turn.wake();
+ defer pardes.turn.rest();
const st = &(state orelse return false);
const pane = st.core.panes[st.core.active] orelse return false;
const f = if (pane.file) |*x| x else return false;