summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-21 14:55:53 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit44ce573e15c773cf5bb0d42941a143b63e44d900 (patch)
treec2c6ecd5b298a85044cb1b68e1db8f7b6657ead7 /src/pardes.zig
parentfd50bd971d6dea7eaaa4ee5436ba16b95fa25b30 (diff)
downloadpardes-44ce573e15c773cf5bb0d42941a143b63e44d900.tar.gz
pardes-44ce573e15c773cf5bb0d42941a143b63e44d900.zip
Make the macOS shell a first-class host
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) <[email protected]>
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig149
1 files changed, 97 insertions, 52 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index ed71102e..dee8a902 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -88,12 +88,19 @@ pub const BOX_H: u16 = 1;
pub const TAG_GAP: u16 = 1;
pub const TAG_TEXT_INSET: u16 = config.GUTTER + TAG_GAP;
-pub fn taglineBandOffset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32) u32 {
+pub fn taglineBandOffset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32, workspace: bool) u32 {
const spare = cell_h -| tagline_h;
const border = topbarPaneBorderPixels(cell_h, tagline_h);
+ const rows: u32 = @max(1, @as(u32, @intFromFloat(@floor(canvas_h / @as(f32, @floatFromInt(cell_h))))));
+ if (!workspace) {
+ // No workspace row: nothing above row 0 to share a rule with, so
+ // every band centers in its cell the way the middle rows always
+ // have. The anchor boxes this lets the hosts draw stay symmetric.
+ if (@as(u32, row) + 1 >= rows) return spare;
+ return spare / 2;
+ }
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;
}
@@ -104,8 +111,8 @@ pub fn topbarPaneBorderPixels(cell_h: u32, tagline_h: u32) u32 {
}
pub fn taglineOriginCol(p: *const Pardes, col: u16, row: u16, track: ?layout.Track) f32 {
- if (row < TOPBAR_H) return 0;
- if (row < TOPBAR_H + p.columnBarHeight()) {
+ if (row < p.topBarHeight()) return 0;
+ if (row < p.topBarHeight() + p.columnBarHeight()) {
for (p.col_x[0..p.ncol], p.col_w[0..p.ncol]) |x, w|
if (col >= x and col - x < w) return @floatFromInt(x);
}
@@ -164,8 +171,8 @@ test "paint order is moving, then opening, then closing tombstones on top" {
pub fn gridColAt(p: ?*const Pardes, x: f32, row: u16, body_w: f32, tagline_w: f32) u16 {
const body = @max(body_w, 1);
const tag = @max(tagline_w, 1);
- if (row < TOPBAR_H) return colFromSpan(x, tag);
- if (p) |core| if (row < TOPBAR_H + core.columnBarHeight()) {
+ if (row < (if (p) |core| core.topBarHeight() else TOPBAR_H)) return colFromSpan(x, tag);
+ if (p) |core| if (row < core.topBarHeight() + core.columnBarHeight()) {
for (core.col_x[0..core.ncol], core.col_w[0..core.ncol]) |origin, width| {
if (width == 0) continue;
const left = @as(f32, @floatFromInt(origin)) * body;
@@ -3395,6 +3402,12 @@ pub const Cell = struct {
len: u8 = 1,
style: CellStyle = .{},
default: bool = true,
+ /// The look-hover affordance is on this cell: the word under the pointer
+ /// is a real Look or Exec operand, and hosts that draw the quiet tint as
+ /// more than a flat fill — macOS renders it as a glass overlay — read
+ /// this instead of guessing from the background colour, which a real
+ /// selection shares.
+ hover: bool = false,
pub fn grapheme(c: *const Cell) []const u8 {
return c.text[0..c.len];
@@ -3670,8 +3683,12 @@ test "pixel attachment fit and pan do not invalidate cached pixels" {
/// The canonical interface: what a frame of pardes IS. The tty shell writes
/// these cells to vaxis one-to-one; the SDL shells rasterize them.
pub const Surface = struct {
- pub const PointerShape = enum(u8) { arrow, link };
+ pub const PointerShape = enum(u8) { arrow, link, target };
pointer_shape: PointerShape = .arrow,
+ /// Set only for the duration of the look-hover paint, so the cells that
+ /// paint the quiet affordance can carry the hover bit through the ABI
+ /// without every selection painter learning a new parameter.
+ mark_hover: bool = false,
body_layers: [MAX_PANES]BodyLayer = @splat(.{}),
tag_layers: [MAX_TAG_LAYERS]TagLayer = @splat(.{}),
cols: u16 = 0,
@@ -3717,7 +3734,11 @@ pub const Surface = struct {
pub fn at(s: *Surface, x: u16, y: u16) *Cell {
std.debug.assert(x < s.cols and y < s.rows);
- return &s.cells[@as(usize, y) * s.cols + x];
+ const cell = &s.cells[@as(usize, y) * s.cols + x];
+ // The look-hover paint runs with mark_hover set, and its cells are
+ // exactly the ones this marks — see render's preview block.
+ if (s.mark_hover) cell.hover = true;
+ return cell;
}
pub fn set(s: *Surface, x: u16, y: u16, text: []const u8, style: CellStyle) void {
@@ -6263,6 +6284,14 @@ pub const Pardes = struct {
return config.topbar_str;
}
+ /// The workspace tag row's height: `TOPBAR_H` while the setting keeps
+ /// it, zero once a native shell takes its commands over. Layout and hit
+ /// testing read this rather than the constant, the way they read
+ /// `columnBarHeight`.
+ pub fn topBarHeight(p: *const Pardes) u16 {
+ return if (p.settings.workspace_tag) TOPBAR_H else 0;
+ }
+
pub fn columnBarHeight(p: *const Pardes) u16 {
return if (p.settings.column_tags and p.screen_h >= 3) COLBAR_H else 0;
}
@@ -7765,7 +7794,7 @@ pub const Pardes = struct {
}
var dest: ?usize = null;
for (p.panes, 0..) |slot, i| {
- if (slot == null or p.rects[i].y != TOPBAR_H + p.columnBarHeight()) continue;
+ if (slot == null or p.rects[i].y != p.topBarHeight() + p.columnBarHeight()) continue;
if (p.header_column) |c| if (p.rects[i].x != p.col_x[c]) continue;
if (i == p.active) {
dest = i;
@@ -11015,7 +11044,7 @@ pub const Pardes = struct {
pub const ChromeTarget = struct { col: u16, row: u16 };
pub fn chromeTarget(p: *const Pardes, col: u16, row: u16) ?ChromeTarget {
- if (row < TOPBAR_H + p.columnBarHeight()) return .{ .col = col, .row = row };
+ if (row < p.topBarHeight() + p.columnBarHeight()) return .{ .col = col, .row = row };
for (p.panes, 0..) |slot, id| {
if (slot == null) continue;
const rect = p.rects[id];
@@ -11330,7 +11359,7 @@ pub const Pardes = struct {
p.chord_arg = null;
}
}
- if (mrow >= TOPBAR_H and mrow < TOPBAR_H + p.columnBarHeight() and m.kind == .press) {
+ if (mrow >= p.topBarHeight() and mrow < p.topBarHeight() + p.columnBarHeight() and m.kind == .press) {
for (0..p.ncol) |column| {
if (mcol < p.col_x[column] or mcol >= p.col_x[column] + @min(config.GUTTER, p.col_w[column])) continue;
if (m.button == config.select_button) {
@@ -11347,7 +11376,7 @@ pub const Pardes = struct {
if (m.kind == .release) p.header_drag = false;
return;
}
- if (mrow < TOPBAR_H + p.columnBarHeight() and m.kind == .press and
+ if (mrow < p.topBarHeight() + p.columnBarHeight() and m.kind == .press and
(m.button == config.select_button or m.button == config.exec_button or m.button == config.look_button))
{
if (m.button == config.select_button) if (p.panes[p.active]) |pane| {
@@ -11355,7 +11384,7 @@ pub const Pardes = struct {
};
var column: ?usize = null;
var x: u16 = 0;
- if (mrow >= TOPBAR_H) {
+ if (mrow >= p.topBarHeight()) {
for (0..p.ncol) |c| if (mcol >= p.col_x[c] and mcol < p.col_x[c] + p.col_w[c]) {
column = c;
x = p.col_x[c] + TAG_TEXT_INSET;
@@ -13186,6 +13215,15 @@ pub const Pardes = struct {
}
}
};
+ // A look-hover preview means the pointed word is a real Look or Exec
+ // operand: the same plumbing a click would act on. Hosts that model
+ // the pointer per-text (the macOS I-beam) read this as "target" and
+ // afford it the arrow instead.
+ if (p.look_hover_preview != null) s.pointer_shape = .target;
+ // Stale hover bits must not survive into a frame whose pointer has
+ // moved on: the ordinary content paint does not clear fields it
+ // never set.
+ for (s.cells) |*c| c.hover = false;
for (&s.body_layers) |*layer| layer.rows = 0;
for (&s.tag_layers) |*layer| layer.cols = 0;
s.nimages = 0;
@@ -13252,21 +13290,24 @@ pub const Pardes = struct {
}
}
- // global tagbar: full width, top row
+ // global tagbar: full width, top row — unless a native shell has
+ // taken its commands over, in which case the row is not ours to draw
p.revealHeader();
- s.fill(0, 0, s.cols, TOPBAR_H, .{
- .bg = .{ .rgb = chrome.tag_bg },
- .font_role = .tagline,
- });
var tb_buf: [1200]u8 = undefined;
const global_bar = p.topbar(&tb_buf);
const global_scroll = if (p.topbar_col != null and p.header_column == null) p.header_scroll else 0;
- _ = s.print(0, 0, s.cols, global_bar[panes.File.rawAtDisplay(global_bar, global_scroll)..], .{
- .fg = .{ .rgb = chrome.tag_fg },
- .bg = .{ .rgb = chrome.tag_bg },
- .font_role = .tagline,
- });
- if (p.pointer_inside and p.hover_row < TOPBAR_H) {
+ if (p.settings.workspace_tag) {
+ s.fill(0, 0, s.cols, TOPBAR_H, .{
+ .bg = .{ .rgb = chrome.tag_bg },
+ .font_role = .tagline,
+ });
+ _ = s.print(0, 0, s.cols, global_bar[panes.File.rawAtDisplay(global_bar, global_scroll)..], .{
+ .fg = .{ .rgb = chrome.tag_fg },
+ .bg = .{ .rgb = chrome.tag_bg },
+ .font_role = .tagline,
+ });
+ }
+ if (p.pointer_inside and p.hover_row < p.topBarHeight()) {
const bar = p.topbar(&tb_buf);
if (wordBoundsAtCol(bar, panes.File.rawAtDisplay(bar, p.hover_col + global_scroll))) |bounds| {
var col = panes.File.rawDisplayCol(bar, bounds.lo) -| global_scroll;
@@ -13296,24 +13337,24 @@ pub const Pardes = struct {
const lum = (@as(u16, grip_bg[0]) * 3 + @as(u16, grip_bg[1]) * 6 + @as(u16, grip_bg[2])) / 10;
const ink: [3]u8 = if (lum > 140) .{ 0, 0, 0 } else .{ 255, 255, 255 };
const grip_style: CellStyle = .{ .bg = .{ .rgb = grip_bg }, .fg = .{ .rgb = ink }, .font_role = .tagline };
- s.fill(p.col_x[c], TOPBAR_H, grip, 1, grip_style);
- s.fill(p.col_x[c] + grip, TOPBAR_H, inset - grip, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
+ s.fill(p.col_x[c], p.topBarHeight(), grip, 1, grip_style);
+ s.fill(p.col_x[c] + grip, p.topBarHeight(), inset - grip, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
- s.fill(x, TOPBAR_H, w, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
+ s.fill(x, p.topBarHeight(), w, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
const bar_scroll = if (p.topbar_col != null and p.header_column == c) p.header_scroll else 0;
const tag = p.columnTag(c);
- _ = s.print(x, TOPBAR_H, w, tag[panes.File.rawAtDisplay(tag, bar_scroll)..], .{
+ _ = s.print(x, p.topBarHeight(), w, tag[panes.File.rawAtDisplay(tag, bar_scroll)..], .{
.fg = .{ .rgb = fg },
.bg = .{ .rgb = bg },
.font_role = .tagline,
});
- if (p.pointer_inside and p.hover_row == TOPBAR_H and p.hover_col >= x and p.hover_col < x + w) {
+ if (p.pointer_inside and p.hover_row == p.topBarHeight() and p.hover_col >= x and p.hover_col < x + w) {
const bar = p.columnTag(c);
if (wordBoundsAtCol(bar, panes.File.rawAtDisplay(bar, p.hover_col - x + bar_scroll))) |bounds| {
var col = panes.File.rawDisplayCol(bar, bounds.lo) -| bar_scroll;
const hi = panes.File.rawDisplayCol(bar, bounds.hi) -| bar_scroll;
while (col < hi and col < w) : (col += 1) {
- const cell = s.at(x + @as(u16, @intCast(col)), TOPBAR_H);
+ const cell = s.at(x + @as(u16, @intCast(col)), p.topBarHeight());
cell.style.bg = .{ .rgb = th.sel_bg };
cell.style.fg = .{ .rgb = th.sel_fg };
}
@@ -13324,7 +13365,7 @@ pub const Pardes = struct {
const bar = p.headerText();
const x = if (p.header_column) |c| p.col_x[c] + TAG_TEXT_INSET else 0;
const w = if (p.header_column) |c| p.col_w[c] -| TAG_TEXT_INSET else s.cols;
- const y: u16 = if (p.header_column != null) TOPBAR_H else 0;
+ const y: u16 = if (p.header_column != null) p.topBarHeight() else 0;
const anchor = p.header_anchor orelse cur;
var col = panes.File.rawDisplayCol(bar, @min(cur, anchor)) -| p.header_scroll;
const hi = panes.File.rawDisplayCol(bar, @max(cur, anchor)) -| p.header_scroll;
@@ -13342,7 +13383,7 @@ pub const Pardes = struct {
switch (p.drag) {
.border_v => |d| {
if (d.cur_x < s.cols) {
- var row: u16 = TOPBAR_H;
+ var row: u16 = p.topBarHeight();
while (row < s.rows) : (row += 1) s.overlayDash(d.cur_x, row, "╎");
}
if (d.corner) |k| if (d.cur_y < s.rows) {
@@ -13367,11 +13408,11 @@ pub const Pardes = struct {
for (&s.body_layers) |*layer| if (layer.rows > 0 and layer.viewport.x + layer.viewport.w == s.cols) {
layer.viewport.w -|= 1;
};
- for (&s.tag_layers) |*layer| if (layer.cols > 0 and layer.viewport.x + layer.viewport.w == s.cols and layer.viewport.y >= TOPBAR_H) {
+ for (&s.tag_layers) |*layer| if (layer.cols > 0 and layer.viewport.x + layer.viewport.w == s.cols and layer.viewport.y >= p.topBarHeight()) {
layer.viewport.w -|= 1;
};
}
- var row: u16 = TOPBAR_H;
+ var row: u16 = p.topBarHeight();
while (row < s.rows) : (row += 1) s.overlayDash(x, row, "╎");
}
},
@@ -13389,7 +13430,7 @@ pub const Pardes = struct {
var c: usize = 0;
while (c + 1 < p.ncol) : (c += 1) {
if (p.hover_col == p.col_x[c] + p.col_w[c] -| 1) {
- var row: u16 = TOPBAR_H;
+ var row: u16 = p.topBarHeight();
while (row < s.rows) : (row += 1) s.overlayDash(p.hover_col, row, "╎");
}
}
@@ -13443,27 +13484,27 @@ pub const Pardes = struct {
const ow: u16 = 38;
const oh: u16 = 10;
const ox: u16 = if (s.cols > ow) s.cols - ow else 0;
- if (ox + ow <= s.cols and TOPBAR_H + oh <= s.rows) {
+ if (ox + ow <= s.cols and p.topBarHeight() + oh <= s.rows) {
// vaxis single_rounded border ring, default style
- s.set(ox, TOPBAR_H, "╭", .{});
- s.set(ox + ow - 1, TOPBAR_H, "╮", .{});
- s.set(ox, TOPBAR_H + oh - 1, "╰", .{});
- s.set(ox + ow - 1, TOPBAR_H + oh - 1, "╯", .{});
+ s.set(ox, p.topBarHeight(), "╭", .{});
+ s.set(ox + ow - 1, p.topBarHeight(), "╮", .{});
+ s.set(ox, p.topBarHeight() + oh - 1, "╰", .{});
+ s.set(ox + ow - 1, p.topBarHeight() + oh - 1, "╯", .{});
var bx = ox + 1;
while (bx < ox + ow - 1) : (bx += 1) {
- s.set(bx, TOPBAR_H, "─", .{});
- s.set(bx, TOPBAR_H + oh - 1, "─", .{});
+ s.set(bx, p.topBarHeight(), "─", .{});
+ s.set(bx, p.topBarHeight() + oh - 1, "─", .{});
}
- var by = TOPBAR_H + 1;
- while (by < TOPBAR_H + oh - 1) : (by += 1) {
+ var by = p.topBarHeight() + 1;
+ while (by < p.topBarHeight() + oh - 1) : (by += 1) {
s.set(ox, by, "│", .{});
s.set(ox + ow - 1, by, "│", .{});
}
- s.fill(ox + 1, TOPBAR_H + 1, ow - 2, oh - 2, .{ .bg = .{ .rgb = chrome.scroll_track } });
+ s.fill(ox + 1, p.topBarHeight() + 1, ow - 2, oh - 2, .{ .bg = .{ .rgb = chrome.scroll_track } });
var lines = std.mem.splitScalar(u8, text, '\n');
- var ly: u16 = TOPBAR_H + 1;
+ var ly: u16 = p.topBarHeight() + 1;
while (lines.next()) |line| : (ly += 1) {
- if (ly >= TOPBAR_H + oh - 1) break;
+ if (ly >= p.topBarHeight() + oh - 1) break;
_ = s.print(ox + 1, ly, ow - 2, line, .{ .fg = .{ .rgb = chrome.tag_fg } });
}
}
@@ -13702,11 +13743,14 @@ pub const Pardes = struct {
layer.cursor = if (p.topbar_col == null and pane.promptAt() == null and temporary.cursor != null) .{ .col = temporary.cursor.?.x - TAG_TEXT_INSET, .bar = temporary.cursor.?.bar } else null;
}
var text_buf: [1200]u8 = undefined;
- try p.renderHeaderLayer(arena, MAX_PANES + MAX_COLS, .workspace, 0, .{ .x = 0, .y = 0, .w = p.screen_w, .h = TOPBAR_H }, p.topbar(&text_buf), chrome.tag_bg, chrome.tag_fg);
+ // With the workspace tag row handed to native chrome there is no
+ // workspace layer to render; the menu bar carries its commands.
+ if (p.settings.workspace_tag)
+ try p.renderHeaderLayer(arena, MAX_PANES + MAX_COLS, .workspace, 0, .{ .x = 0, .y = 0, .w = p.screen_w, .h = TOPBAR_H }, p.topbar(&text_buf), chrome.tag_bg, chrome.tag_fg);
if (p.columnBarHeight() > 0) for (0..p.ncol) |column| {
if (p.col_w[column] == 0) continue;
const active = if (layout.findPane(p, p.active)) |found| found.col == column and p.settings.focus_tint else false;
- try p.renderHeaderLayer(arena, MAX_PANES + column, .column, @intCast(column), .{ .x = p.col_x[column] + @min(TAG_TEXT_INSET, p.col_w[column]), .y = TOPBAR_H, .w = p.col_w[column] -| TAG_TEXT_INSET, .h = COLBAR_H }, p.columnTag(column), if (active) chrome.tag_active_bg else chrome.tag_bg, if (active) chrome.tag_active_fg else chrome.tag_fg);
+ try p.renderHeaderLayer(arena, MAX_PANES + column, .column, @intCast(column), .{ .x = p.col_x[column] + @min(TAG_TEXT_INSET, p.col_w[column]), .y = p.topBarHeight(), .w = p.col_w[column] -| TAG_TEXT_INSET, .h = COLBAR_H }, p.columnTag(column), if (active) chrome.tag_active_bg else chrome.tag_bg, if (active) chrome.tag_active_fg else chrome.tag_fg);
};
}
@@ -14010,13 +14054,14 @@ pub const Pardes = struct {
if (p.look_hover_preview) |preview| {
if (preview.pane == id and preview.serial == pane.serial) {
// Preserve body ink, then let every real user selection below
- // win over this quiet affordance.
+ // win over this quiet affordance. mark_hover carries the
+ // cells this paints out to the hosts as the affordance rect —
+ // macOS composes its glass overlay over exactly these.
const preview_bg = mix(page_rgb, mix(page_rgb, th.sel_bg));
if (preview.file_word) |word|
panes.File.paintWordSelection(p, pane, r, word.row, word.lo, word.hi, preview_bg)
else if (preview.sel) |sel|
- if (@max(sel.r0, sel.r1) >= BOX_H)
- p.paintPointerSelection(pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null);
+ p.paintPointerSelection(pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null);
}
}
const modal_hover = if (p.look_hover_preview) |preview|