From 44ce573e15c773cf5bb0d42941a143b63e44d900 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 21 Sep 2026 14:55:53 -0300 Subject: Make the macOS shell a first-class host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pty children are exec'd with their own TERM/COLORTERM/TERM_PROGRAM instead of inheriting a .app launch's empty environment, and ttyTaken finally answers on darwin — libproc walks the tty's foreground process group — so Escape reaches the child and Exec stops believing every pane sits at its prompt. The occupancy suite runs on both platforms now. The workspace tag row moves into the native menu bar as a Builtins menu. -Dworkspace-tag (default off for -Dplatform=macos, on everywhere else) drives it, and Pardes.topBarHeight replaces the TOPBAR_H constant so the core stops reserving the row. The view pins every variable-font axis to the file's own default (Maple Mono came up Thin otherwise), shapes ligatures, carries per-shape pointer cursors, and draws the look-hover affordance as refracted glass. Tag rows fill edge to edge, with the anchor box painted back on top of that fill and its mode glyph centred on the same square. Theme accents re-saturated across the set. Co-Authored-By: Claude Opus 5 (1M context) --- src/macos.zig | 58 +++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 13 deletions(-) (limited to 'src/macos.zig') diff --git a/src/macos.zig b/src/macos.zig index 79036c3e..d263cfc6 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -106,6 +106,7 @@ const color_default: u32 = 0x01000000; const color_indexed: u32 = 0x02000000; const cell_flag_default: u8 = 1; const cell_flag_tagline: u8 = 2; +const cell_flag_hover: u8 = 4; const scene_flag_crt: u32 = 1 << 0; const scene_flag_ripple: u32 = 1 << 1; const scene_flag_glitch: u32 = 1 << 2; @@ -730,6 +731,16 @@ fn initCore(runtime: ?*const Runtime, cols_arg: u16, rows_arg: u16) !void { const core = try pardes.Pardes.init(allocs.pardes, opts); errdefer core.deinit(); + // The workspace tag row is the macOS menu bar's business here (see + // docs/macos.md): the build option says whether this shell keeps the + // acme topbar or hands its commands to native chrome. The e2e harness + // opts back in with PARDES_WORKSPACE_TAG=1 — its scripts speak to a + // grid that has the row. Applied before the first resize so the layout + // has never seen it the other way. + core.settings.workspace_tag = if (std.c.getenv("PARDES_WORKSPACE_TAG")) |v| + std.mem.eql(u8, std.mem.span(v), "1") + else + @import("pardes_config").workspace_tag; core.native_images = true; var prompt_rcs = host_io.Shell.prepare(); @@ -917,7 +928,7 @@ export fn pardes_gui_tagline_font_percent() u8 { } export fn pardes_tagline_band_offset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32) u32 { - return pardes.taglineBandOffset(row, canvas_h, cell_h, tagline_h); + return pardes.taglineBandOffset(row, canvas_h, cell_h, tagline_h, if (state) |*st| st.core.settings.workspace_tag else true); } export fn pardes_topbar_pane_border_px(cell_h: u32, tagline_h: u32) u32 { @@ -942,6 +953,11 @@ export fn pardes_topbar_pane_border_rgb() u32 { return @as(u32, rgb[0]) << 16 | @as(u32, rgb[1]) << 8 | rgb[2]; } +export fn pardes_tag_active_bg() u32 { + 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 { const st = state orelse return color_default; const rgb = st.core.chromeTheme().tag_bg; @@ -1756,12 +1772,18 @@ fn clearFrame(st: *State) void { st.panel_diff_len = 0; } -fn blankTagCells(cells: []Cell, cols: u16, rows: u16, layers: []const pardes.TagLayer) void { +fn blankTagCells(cells: []Cell, cols: u16, rows: u16, source: []const pardes.Cell, layers: []const pardes.TagLayer) void { for (layers) |layer| { if (layer.cols == 0 or layer.viewport.y >= rows or layer.viewport.x >= cols) continue; const start = @as(usize, layer.viewport.y) * cols + layer.viewport.x; const count = @min(layer.viewport.w, cols - layer.viewport.x); - for (cells[start..][0..count]) |*cell| cell.* = encodeCell(.{}); + for (cells[start..][0..count], source[start..][0..count]) |*cell, was| { + // The tag text is the layer's to draw, but a look-hover on a tag + // word is a canonical fact: carry the hover bit through so the + // glass overlay still finds the affordance rect. + cell.* = encodeCell(.{}); + cell.flags |= @as(u8, @intFromBool(was.hover)) * cell_flag_hover; + } } } @@ -1800,7 +1822,7 @@ fn presentFrame(ctx: ?*anyopaque, surface: *const pardes.Surface) void { defer tz_cells.end(); for (surface.cells, st.cells[0..count]) |cell, *out| out.* = encodeCell(cell); } - blankTagCells(st.cells[0..count], surface.cols, surface.rows, &surface.tag_layers); + blankTagCells(st.cells[0..count], surface.cols, surface.rows, surface.cells, &surface.tag_layers); collectPanelDiff(st, surface, count); collectImages(st, surface); collectPanelTracks(st, surface); @@ -1831,7 +1853,7 @@ fn collectPanelDiff(st: *State, surface: *const pardes.Surface, count: usize) vo } for (surface.previous_cells, st.previous_cells[0..count]) |cell, *out| out.* = encodeCell(cell); - blankTagCells(st.previous_cells[0..count], surface.cols, surface.rows, surface.previous_tag_layers); + blankTagCells(st.previous_cells[0..count], surface.cols, surface.rows, surface.previous_cells, surface.previous_tag_layers); for (surface.cell_diffs, st.changed_cells[0..count]) |diff, *out| out.* = encodeChanged(diff); st.panel_diff_len = count; @@ -2395,8 +2417,8 @@ fn encodeCell(cell: pardes.Cell) Cell { .fg = encodeColor(cell.style.fg), .bg = encodeColor(cell.style.bg), .attrs = encodeAttrs(cell.style), + .flags = encodeCellFlags(cell.default, cell.style.font_role, cell.hover), .len = if (cell.default) 1 else cell.len, - .flags = encodeCellFlags(cell.default, cell.style.font_role), }; if (cell.default) out.text[0] = ' ' @@ -2418,9 +2440,10 @@ fn encodeAttrs(style: pardes.CellStyle) u16 { return attrs; } -fn encodeCellFlags(default: bool, role: pardes.FontRole) u8 { +fn encodeCellFlags(default: bool, role: pardes.FontRole, hover: bool) u8 { return @as(u8, @intFromBool(default)) * cell_flag_default | - @as(u8, @intFromBool(role == .tagline)) * cell_flag_tagline; + @as(u8, @intFromBool(role == .tagline)) * cell_flag_tagline | + @as(u8, @intFromBool(hover)) * cell_flag_hover; } fn noteScrollVelocity(st: *State, delta_rows: f32, delta_cols: f32) void { @@ -2566,7 +2589,8 @@ test "pardes.h declares every export the way it is defined" { try expectSameAbi(@TypeOf(c.pardes_animating), @TypeOf(pardes_animating)); try expectSameAbi(@TypeOf(c.pardes_gui_tagline_font_percent), @TypeOf(pardes_gui_tagline_font_percent)); try expectSameAbi(@TypeOf(c.pardes_tagline_band_offset), @TypeOf(pardes_tagline_band_offset)); - try expectSameAbi(@TypeOf(c.pardes_topbar_pane_border_px), @TypeOf(pardes_topbar_pane_border_px)); + try expectSameAbi(@TypeOf(c.pardes_topbar_pane_border_rgb), @TypeOf(pardes_topbar_pane_border_rgb)); + try expectSameAbi(@TypeOf(c.pardes_tag_active_bg), @TypeOf(pardes_tag_active_bg)); try expectSameAbi(@TypeOf(c.pardes_tagline_origin_col), @TypeOf(pardes_tagline_origin_col)); try expectSameAbi(@TypeOf(c.pardes_grid_col_at), @TypeOf(pardes_grid_col_at)); try expectSameAbi(@TypeOf(c.pardes_topbar_pane_border_rgb), @TypeOf(pardes_topbar_pane_border_rgb)); @@ -2658,6 +2682,7 @@ test "pardes.h matches the Zig boundary" { try expectEqual(@as(u32, c.PARDES_COLOR_INDEXED), color_indexed); try expectEqual(@as(u8, c.PARDES_CELL_DEFAULT), cell_flag_default); try expectEqual(@as(u8, c.PARDES_CELL_TAGLINE), cell_flag_tagline); + try expectEqual(@as(u8, c.PARDES_CELL_HOVER), cell_flag_hover); try expectEqual(@as(u16, c.PARDES_TOPBAR_H), pardes.TOPBAR_H); try expectEqual(@as(u32, c.PARDES_SCENE_CRT), scene_flag_crt); try expectEqual(@as(u32, c.PARDES_SCENE_RIPPLE), scene_flag_ripple); @@ -2723,9 +2748,10 @@ test "pardes.h matches the Zig boundary" { encodeAttrs(.{ .ul = .curly }), ); - try expectEqual(@as(u8, 0), encodeCellFlags(false, .body)); - try expectEqual(cell_flag_tagline, encodeCellFlags(false, .tagline)); - try expectEqual(cell_flag_default | cell_flag_tagline, encodeCellFlags(true, .tagline)); + try expectEqual(@as(u8, 0), encodeCellFlags(false, .body, false)); + try expectEqual(cell_flag_hover, encodeCellFlags(false, .body, true)); + try expectEqual(cell_flag_tagline, encodeCellFlags(false, .tagline, false)); + try expectEqual(cell_flag_default | cell_flag_tagline, encodeCellFlags(true, .tagline, false)); try expectEqual(pardes.config.gui_tagline_font_percent, pardes_gui_tagline_font_percent()); } @@ -2947,10 +2973,16 @@ test "mac tag layer ABI preserves logical capacity and physical grip" { try std.testing.expectEqual(@as(u32, 11), pardes_tag_layer_value(0, 8)); try std.testing.expectEqual(@as(u8, 'Z'), pardes_tag_layer_cells(0).?[11].text[0]); var canonical: [24]Cell = @splat(encodeCell(.{ .text = .{'G'} ++ .{0} ** 6, .len = 1, .default = false })); - blankTagCells(&canonical, 8, 3, &core.surface.tag_layers); + // A look-hover under the layer is a canonical fact and survives the blank; + // the glyph it marks does not, because the layer redraws that text itself. + var source: [24]pardes.Cell = @splat(.{}); + source[11].hover = true; + blankTagCells(&canonical, 8, 3, &source, &core.surface.tag_layers); try std.testing.expectEqual(@as(u8, 'G'), canonical[9].text[0]); try std.testing.expectEqual(@as(u8, 'G'), canonical[18].text[0]); try std.testing.expectEqual(encodeCell(.{}).len, canonical[10].len); + try std.testing.expectEqual(@as(u8, 0), canonical[10].flags & cell_flag_hover); + try std.testing.expectEqual(cell_flag_hover, canonical[11].flags & cell_flag_hover); } test "mac window blur builtin reaches the ABI without changing opacity" { -- cgit v1.3