summaryrefslogtreecommitdiff
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
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]>
-rw-r--r--build.zig8
-rw-r--r--docs/config.md1
-rw-r--r--docs/macos.md38
-rw-r--r--src/CHANGELOG.md12
-rw-r--r--src/config.zig15
-rw-r--r--src/gui/gui.zig22
-rw-r--r--src/host_io.zig320
-rw-r--r--src/layout.zig8
-rw-r--r--src/macos.zig58
-rw-r--r--src/macos/Sources/AppDelegate.swift18
-rw-r--r--src/macos/Sources/PardesView.swift406
-rw-r--r--src/macos/pardes.h12
-rw-r--r--src/pardes.zig149
-rw-r--r--src/themes/acme.zig8
-rw-r--r--src/themes/atelier.zig32
-rw-r--r--src/themes/clay.zig32
-rw-r--r--src/themes/dark.zig12
-rw-r--r--src/themes/daybreak.zig28
-rw-r--r--src/themes/dusk.zig32
-rw-r--r--src/themes/forge.zig28
-rw-r--r--src/themes/forge_black.zig28
-rw-r--r--src/themes/forge_soft.zig28
-rw-r--r--src/themes/harvest.zig32
-rw-r--r--src/themes/helix.zig6
-rw-r--r--src/themes/ink.zig18
-rw-r--r--src/themes/lagoon.zig24
-rw-r--r--src/themes/orchard.zig32
-rw-r--r--src/themes/orchard_black.zig32
-rw-r--r--src/themes/paper.zig32
-rw-r--r--src/themes/solarium.zig32
-rw-r--r--src/themes/spectrum.zig22
-rw-r--r--src/web.zig2
-rw-r--r--test/macos_e2e.swift10
33 files changed, 1160 insertions, 377 deletions
diff --git a/build.zig b/build.zig
index 6734d9b0..0cd7f312 100644
--- a/build.zig
+++ b/build.zig
@@ -301,6 +301,9 @@ pub fn build(b: *std.Build) void {
const esp32p4_cols = b.option(u16, "esp32p4-cols", "the board's grid width in cells (-Dplatform=esp32p4 only)") orelse 56;
const esp32p4_rows = b.option(u16, "esp32p4-rows", "the board's grid height in cells (-Dplatform=esp32p4 only)") orelse 14;
const theme_animation = b.option(bool, "theme-animation", "animate chrome colors across a theme change (default: off for esp32p4)");
+ // The macOS shell carries the workspace tag row's commands in its native
+ // menu bar; the row itself goes away. `-Dworkspace-tag=true` puts it back.
+ const workspace_tag = b.option(bool, "workspace-tag", "draw the workspace tag row (default: on except on macOS, whose menu bar carries its commands)");
const shell_cfg: ShellConfig = .{
.tree_sitter_grammars = tree_sitter_grammars,
.tracy = tracy != null,
@@ -309,6 +312,7 @@ pub fn build(b: *std.Build) void {
.esp32p4_cols = esp32p4_cols,
.esp32p4_rows = esp32p4_rows,
.theme_animation = theme_animation,
+ .workspace_tag = workspace_tag,
.prebuilt_shaders = prebuilt_shaders,
.embed_sources = embed_sources,
.zig_lib_dir = b.graph.zig_lib_directory.path orelse "",
@@ -1434,6 +1438,9 @@ const ShellConfig = struct {
/// null = "the platform's own default", resolved per shell below, because
/// the default is off for the board and on everywhere else.
theme_animation: ?bool,
+ /// null = "the platform's own default": the macOS shell hands the row to
+ /// its native menu bar, every other shell keeps the acme topbar.
+ workspace_tag: ?bool,
prebuilt_shaders: bool,
embed_sources: bool,
zig_lib_dir: []const u8,
@@ -1452,6 +1459,7 @@ fn shellOptions(b: *std.Build, cfg: ShellConfig, platform: Platform) *std.Build.
o.addOption(bool, "mupdf", cfg.mupdf);
o.addOption(u16, "esp32p4_cols", cfg.esp32p4_cols);
o.addOption(u16, "esp32p4_rows", cfg.esp32p4_rows);
+ o.addOption(bool, "workspace_tag", cfg.workspace_tag orelse (platform != .macos));
o.addOption(bool, "theme_animation", cfg.theme_animation orelse (platform != .esp32p4));
// Meaningful only for the SDL shell. Keeping the platform condition here
// prevents Config/EffectCode from describing tty/macOS/web as "prebuilt".
diff --git a/docs/config.md b/docs/config.md
index a55aef1c..a229830b 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -538,6 +538,7 @@ including the standard `-Dtarget` and `-Doptimize` options.
| `-Djpx` | bool | `true` — JPEG 2000, and with it scanned PDFs |
| `-Dtree-sitter` | `disabled`, `zig`, `minimal`, `full` | `full` natively, `zig` for web, `disabled` for esp32p4 |
| `-Dtheme-animation` | bool | on everywhere except `-Dplatform=esp32p4` |
+| `-Dworkspace-tag` | bool; draw the workspace tag row — the macOS shell hands its commands to the native menu bar instead | on except on `-Dplatform=macos` |
| `-Dprebuilt-shaders` | bool | on for a bare `zig build`, off when `-Dplatform` names a shell |
| `-Dtracy` | path to a Tracy source checkout | off |
| `-Dmacos-identity` | codesigning identity for `pardes.app` | `-` (ad-hoc) |
diff --git a/docs/macos.md b/docs/macos.md
index 9190ca67..e104d3a6 100644
--- a/docs/macos.md
+++ b/docs/macos.md
@@ -369,6 +369,14 @@ second spelling of builtins — `AppDelegate` calls `run("New")`, `run("Save")`,
`run("Help")`, `run("Tutor")` — and the items with no builtin behind them are
left out rather than stubbed.
+The workspace tag row — the topmost tagline, the one carrying `Newcol Joincol
+Find Grep …` — is not drawn on this shell. `-Dworkspace-tag` (default off for
+`-Dplatform=macos`, on everywhere else) hands the row to the menu bar: the
+core stops reserving the row (`Pardes.topBarHeight`), the grid starts at the
+column tags, and the row's commands live in a **Builtins** menu instead. The
+items are the tag's own words, spelled exactly as a tag Exec would type them,
+and nothing in that menu is editable — it is the tag's buttons, not the tag:
+
| menu | item | chord | what it does |
|---|---|---|---|
| pardes | About pardes | — | AppKit's panel |
@@ -378,6 +386,7 @@ left out rather than stubbed.
| | New | ⌘N | the `New` builtin |
| | Save | ⌘S | the `Save` builtin |
| | Close Window | ⌘W | AppKit; the app terminates after the last one |
+| Builtins | Newcol / Joincol / Find / Grep / Changelog / Dump / NextColor / Debug / Kill | — | the workspace tag's own builtins, `run(word)` |
| Edit | Paste | ⌘V | the view's own paste path, not a second one |
| View | Zoom In / Zoom Out / Actual Size | ⌘= / ⌘- / ⌘0 | point size, host-side |
| Window | Minimize / Zoom | ⌘M / — | AppKit |
@@ -455,6 +464,32 @@ Nested launches use the same inherited 9P address and pane serial as other
native hosts. They do not inspect ancestor processes or executable names.
See [the control filesystem](fs.md) for paths and transports.
+Two more things this host cannot inherit from its launcher, because a `.app`
+has none:
+
+- **The terminal's identity.** `host_io.ChildEnv` writes `TERM`, `COLORTERM`
+ and `TERM_PROGRAM` over whatever the environment carried and the child is
+ `execve`'d with that array — built before the fork, because a `setenv`
+ between fork and exec can deadlock on the heap a pty reader thread was
+ holding. Every pane is emulated by the bundled VT, so the value describes
+ pardes and never the terminal pardes was started from; `-Dplatform=gui` and
+ the tty shell take the same path, where the difference is only that their
+ launcher usually happened to set something. The names come from
+ `config.child_term` / `child_colorterm` / `child_term_program`, and `TERM`
+ is deliberately `xterm-256color` rather than a name with no installed
+ terminfo entry — `clear`, colour and cursor addressing all resolve through
+ that lookup.
+- **Whether a program has the terminal.** `host_io.ttyTaken` is what
+ `Pardes.takesCommandLine` asks before deciding that Escape is the `Last`
+ builtin rather than a keystroke for the child, and what `Exec` asks before
+ believing a pane is at its prompt. Linux descends `/proc/<pid>/task/<pid>/
+ children`; darwin has neither that nor a children list, so it enumerates the
+ tty's foreground process group with `proc_listpids(PROC_PGRP_ONLY)` and
+ compares each member's `proc_pidpath` against the shell's own. A nested
+ interactive shell is therefore still a prompt, and `bash -c 'sleep 30'` is
+ still taken — the wrapper wears the shell's binary, but `sleep` shares its
+ group. The occupancy suite in `src/host_io.zig` runs on both platforms.
+
## Fonts and zoom
The face is the shell's business and the size is the window's, so the two are
@@ -1109,9 +1144,6 @@ used for the table and is not folded into the direct-path claim.
says nothing either. Wiring it up means a unix-socket frontend loop beside
the AppKit one, which is `src/detached/client.zig`'s job in the tty and SDL
shells; see `docs/detached.md`.
-- **`tty_taken` on darwin.** The pull is wired (`ttyTaken`, `src/macos.zig:1838`)
- but `look.ttyTaken` has no libproc implementation and answers `false`, so an
- `Exec` here always believes the pane is still at the prompt it forked.
- **IME and marked text.** Only finished characters reach `pardes_key`, so a
dead key composes nothing and Option is Alt rather than a compose modifier.
Real composition means implementing `NSTextInputClient` *and* giving the core
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 18bcceb9..e518f97a 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -2,6 +2,18 @@
## 0.0.3
+- Give pty children their own terminal identity. `TERM`, `COLORTERM` and
+ `TERM_PROGRAM` are now written by pardes rather than inherited from whatever
+ launched it, and the child is exec'd with that environment. A macOS `.app`
+ launch supplies no `TERM` at all, so until now `clear` failed, nothing
+ painted a colour, and every cursor-addressing program fell back to printing
+ lines.
+- Answer tty occupancy on macOS. `ttyTaken` had no darwin implementation and
+ said `false` for every pane, which made `Pardes.takesCommandLine` permanently
+ true: a bare Escape in a raw terminal ran the `Last` builtin instead of
+ reaching the child, and `Exec` always believed the pane sat at its prompt.
+ libproc answers it from the tty's foreground process group, and the
+ occupancy suite now runs on both platforms instead of only Linux.
- Flatten the 9P control tree: `self/` is gone, so `/index`, `/pane/<n>/`,
`/screen` and `/os` sit at the root. `/new` is one clone file (reading it
makes a pane and answers its serial), `/ctl` takes `look TEXT` and
diff --git a/src/config.zig b/src/config.zig
index 440549b3..da23f51f 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -222,6 +222,17 @@ pub const tty_blank: enum { prompt, prompt_and_input } = .prompt;
// Projected colors below this contrast use the theme's more legible anchor.
pub const tty_filter_min_contrast: f64 = 1.5;
+// What a pty child is told it is talking to. Every pane is emulated by the
+// bundled VT, so this describes pardes and never the terminal pardes itself
+// was launched from — the child inherits the launcher's TERM otherwise, which
+// under a macOS .app launch is no TERM at all, and `clear`, colour and every
+// cursor-addressing program fail at the terminfo lookup. There is no installed
+// `xterm-ghostty` entry to name, and a name with no entry is worse than a
+// conservative one that every system ships.
+pub const child_term = "xterm-256color";
+pub const child_colorterm = "truecolor";
+pub const child_term_program = "pardes";
+
pub const topbar_str = "Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill";
pub const pane_builtins_str = "Tty Del Collapse";
@@ -538,6 +549,10 @@ pub const Runtime = struct {
tree_context_tag_style: bool = true,
debug: bool = false,
focus_tint: bool = true,
+ /// The workspace tag row. Native shells with their own chrome (the macOS
+ /// menu bar) turn it off and carry its commands there instead; the core
+ /// keeps the row as the acme tradition everywhere else.
+ workspace_tag: bool = true,
column_tags: bool = true,
syntax_bold: bool = false,
window_opacity: u8 = 100,
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 27f3346c..d871e55e 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -5290,10 +5290,12 @@ test "tagline bands face the topbar rule and Tagbottom faces the window edge" {
const canvas_h: f32 = 200;
const border = topbarPaneBorderPixels(cell_h, tagline_h);
try std.testing.expectEqual(@as(u32, 1), border);
- try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(0, canvas_h, cell_h, tagline_h));
- try std.testing.expectEqual(@as(u32, 1), taglineBandOffset(1, canvas_h, cell_h, tagline_h));
- try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(5, canvas_h, cell_h, tagline_h));
- try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(9, canvas_h, cell_h, tagline_h));
+ try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(0, canvas_h, cell_h, tagline_h, true));
+ try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(0, canvas_h, cell_h, tagline_h, false));
+ try std.testing.expectEqual(@as(u32, 1), taglineBandOffset(1, canvas_h, cell_h, tagline_h, true));
+ try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(1, canvas_h, cell_h, tagline_h, false));
+ try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(5, canvas_h, cell_h, tagline_h, true));
+ try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(9, canvas_h, cell_h, tagline_h, true));
}
fn resolveCell(g: *Gui, cell: *const pardes.Cell, role: pardes.FontRole, is_cursor: bool, page: Ground) ResolvedCell {
@@ -5486,7 +5488,7 @@ fn emitBodyLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const
if (left + @as(f32, @floatFromInt(col)) * width >= right) break;
const cell = &layer.cells[index * layer.cols + col];
const cursor = if (layer.cursor) |cu| show_cursor and !cu.bar and cu.x == col and cu.y == row else false;
- const glyph_top = top - (if (header) @as(f32, @floatFromInt(taglineBandOffset(0, win_h, g.cell_h, g.tagline_height))) else 0);
+ const glyph_top = top - (if (header) @as(f32, @floatFromInt(taglineBandOffset(0, win_h, g.cell_h, g.tagline_height, true))) else 0);
emitInstance(g, instances, next.*, 0, 0, .{ .w = width, .h = height, .x_off = left + @as(f32, @floatFromInt(col)) * width, .y_off = glyph_top }, win_w, win_h, track, role, cell, old_layer, cursor, page);
clipBodyInstance(&instances[next.*], bottom, win_h);
clipInstanceRight(&instances[next.*], right, win_w);
@@ -5638,7 +5640,7 @@ fn emitInstance(
else
layout.h;
const band_offset: f32 = if (resolved.role == .tagline)
- @floatFromInt(taglineBandOffset(row, win_h, g.cell_h, g.tagline_height))
+ @floatFromInt(taglineBandOffset(row, win_h, g.cell_h, g.tagline_height, true))
else
0;
const py0 = layout.y_off + @as(f32, @floatFromInt(row)) * layout.h + band_offset;
@@ -6448,7 +6450,7 @@ fn addCursorBar(
const x0 = layout.x_off + @as(f32, @floatFromInt(x)) * layout.w;
const height = std.math.clamp(visual_height, 1.0, layout.h);
const y0 = layout.y_off + @as(f32, @floatFromInt(y)) * layout.h +
- @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, @intFromFloat(layout.h), @intFromFloat(height))));
+ @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, @intFromFloat(layout.h), @intFromFloat(height), true)));
builder.addRect(
x0,
y0,
@@ -6548,7 +6550,7 @@ fn buildOverlay(
const right = left + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w;
const x = left + @as(f32, @floatFromInt(cursor.col)) * width;
if (x >= right) continue;
- const top = @as(f32, @floatFromInt(layer.viewport.y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(layer.viewport.y, builder.win_h, g.cell_h, g.tagline_height)));
+ const top = @as(f32, @floatFromInt(layer.viewport.y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(layer.viewport.y, builder.win_h, g.cell_h, g.tagline_height, true)));
builder.addRect(x, top, @min(right, x + @max(1, width / 8)), top + @as(f32, @floatFromInt(g.tagline_height)), overlayRgb(cursorColor(core, &layer.cells[cursor.col], .tagline)));
};
if (transient_on) appendTouchOverlay(g, &builder, debug_on);
@@ -6569,7 +6571,7 @@ fn appendWorkspacePet(g: *Gui, core: ?*const pardes.Pardes, surface: *const pard
if (cellCodepoint(cell) != ' ') occupied = i + 1;
}
const editing = p.topbar_col != null and p.header_column == null;
- const band_top: f32 = @floatFromInt(taglineBandOffset(0, builder.win_h, g.cell_h, g.tagline_height));
+ const band_top: f32 = @floatFromInt(taglineBandOffset(0, builder.win_h, g.cell_h, g.tagline_height, true));
const region = if (occupied >= surface.cols) null else pet.lane(
occupied,
@floatFromInt(g.tagline_width),
@@ -6741,7 +6743,7 @@ fn appendTagGrips(g: *const Gui, surface: *const pardes.Surface, layout: CellLay
else => continue,
};
const center_x = (@as(f32, @floatFromInt(x)) + @as(f32, @floatFromInt(config.GUTTER)) / 2) * layout.w;
- const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, g.cell_h, g.tagline_height))) + @as(f32, @floatFromInt(g.tagline_height)) / 2;
+ const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, g.cell_h, g.tagline_height, true))) + @as(f32, @floatFromInt(g.tagline_height)) / 2;
const half_w = @max(1, @min(3, layout.w - 2));
builder.addRect(@floor(center_x - half_w), @floor(center_y - 2), @floor(center_x + half_w), @floor(center_y - 1), overlayRgb(rgb));
builder.addRect(@floor(center_x - half_w), @floor(center_y + 1), @floor(center_x + half_w), @floor(center_y + 2), overlayRgb(rgb));
diff --git a/src/host_io.zig b/src/host_io.zig
index 212c23b4..55592957 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -837,6 +837,7 @@ pub const Shell = struct {
extern "c" fn forkpty(amaster: *c_int, name: ?[*:0]u8, termp: ?*const anyopaque, winp: ?*const posix.winsize) c_int;
extern "c" fn execv(path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int;
+extern "c" fn execve(path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8, envp: [*:null]const ?[*:0]const u8) c_int;
extern "c" fn chdir(path: [*:0]const u8) c_int;
extern "c" fn _exit(status: c_int) noreturn;
@@ -845,6 +846,100 @@ pub const Child = struct {
pid: posix.pid_t,
};
+/// The environment a pty child is handed: everything this process carries,
+/// with the terminal's own identity written over whatever the launcher left
+/// behind. The child is talking to the bundled VT, never to the terminal
+/// pardes itself was started from, and a macOS `.app` launch supplies no
+/// `TERM` at all — which is exactly why `clear` fails there, why nothing
+/// paints a colour, and why every cursor-addressing program falls back to a
+/// dumb line-at-a-time mode.
+pub const ChildEnv = struct {
+ /// Entries pardes owns. The name of each is read back out of its own
+ /// spelling, so there is one place to edit and no second list to drift.
+ const own = [_][*:0]const u8{
+ "TERM=" ++ pardes.config.child_term,
+ "COLORTERM=" ++ pardes.config.child_colorterm,
+ "TERM_PROGRAM=" ++ pardes.config.child_term_program,
+ };
+
+ /// Wide enough for any real environment; `build` answers null rather than
+ /// truncate, because a child missing half its variables is a worse bug
+ /// than one missing TERM, and the caller then execs with `environ` intact.
+ pub const Slots = [1024]?[*:0]const u8;
+
+ fn name(entry: [*:0]const u8) []const u8 {
+ const line = std.mem.span(entry);
+ return line[0 .. std.mem.indexOfScalar(u8, line, '=') orelse line.len];
+ }
+
+ pub fn build(slots: *Slots) ?[*:null]const ?[*:0]const u8 {
+ var n: usize = 0;
+ var i: usize = 0;
+ while (libc.environ[i]) |entry| : (i += 1) {
+ const inherited = name(entry);
+ const shadowed = for (own) |mine| {
+ if (std.mem.eql(u8, inherited, name(mine))) break true;
+ } else false;
+ if (shadowed) continue;
+ if (n + own.len + 1 > slots.len) return null;
+ slots[n] = entry;
+ n += 1;
+ }
+ for (own) |mine| {
+ slots[n] = mine;
+ n += 1;
+ }
+ slots[n] = null;
+ return @ptrCast(slots);
+ }
+};
+
+test "the child environment replaces the launcher's terminal identity exactly once" {
+ const saved: ?[]const u8 = if (libc.getenv("TERM")) |t| std.mem.span(t) else null;
+ var saved_buf: [256]u8 = undefined;
+ const restore: ?[:0]const u8 = if (saved) |t| blk: {
+ if (t.len >= saved_buf.len) break :blk null;
+ @memcpy(saved_buf[0..t.len], t);
+ saved_buf[t.len] = 0;
+ break :blk saved_buf[0..t.len :0];
+ } else null;
+ defer if (restore) |t| {
+ _ = Shell.setenv("TERM", t.ptr, 1);
+ } else {
+ _ = unsetenv("TERM");
+ };
+
+ // A launcher TERM must be shadowed rather than duplicated, and the rest of
+ // the environment must arrive untouched.
+ try std.testing.expectEqual(@as(c_int, 0), Shell.setenv("TERM", "dumb", 1));
+ try std.testing.expectEqual(@as(c_int, 0), Shell.setenv("PARDES_CHILD_ENV_PROBE", "1", 1));
+ defer _ = unsetenv("PARDES_CHILD_ENV_PROBE");
+
+ var slots: ChildEnv.Slots = undefined;
+ const envp = ChildEnv.build(&slots) orelse return error.EnvironmentTooLarge;
+ var terms: usize = 0;
+ var colorterms: usize = 0;
+ var probes: usize = 0;
+ var dumb = false;
+ var i: usize = 0;
+ while (envp[i]) |entry| : (i += 1) {
+ const line = std.mem.span(entry);
+ if (std.mem.startsWith(u8, line, "TERM=")) terms += 1;
+ if (std.mem.startsWith(u8, line, "COLORTERM=")) colorterms += 1;
+ if (std.mem.eql(u8, line, "TERM=dumb")) dumb = true;
+ if (std.mem.eql(u8, line, "PARDES_CHILD_ENV_PROBE=1")) probes += 1;
+ }
+ try std.testing.expectEqual(@as(usize, 1), terms);
+ try std.testing.expectEqual(@as(usize, 1), colorterms);
+ try std.testing.expectEqual(@as(usize, 1), probes);
+ try std.testing.expect(!dumb);
+ // TERM_PROGRAM shares the prefix "TERM"; the match is on the name, so it
+ // is a separate entry and never a second TERM.
+ try std.testing.expectEqualStrings("TERM=" ++ pardes.config.child_term, std.mem.span(envp[i - ChildEnv.own.len].?));
+}
+
+extern "c" fn unsetenv(name: [*:0]const u8) c_int;
+
pub fn forkShell(
core: ?*pardes.Pardes,
pane: usize,
@@ -888,6 +983,11 @@ pub fn forkShell(
try @import("linux/v9fs.zig").writeLaunchCommand(&command.writer, path, std.mem.span(socket), &spawn.argv);
if (!try pardes.panes.Terminal.queuePendingCommand(pn, command.written())) return error.ShellAlreadyStarted;
}
+ // Built here and not after the fork: between fork and exec the child may
+ // not call setenv, whose malloc can deadlock against a pty reader thread
+ // that held the heap when the fork took its snapshot.
+ var env_slots: ChildEnv.Slots = undefined;
+ const envp = ChildEnv.build(&env_slots);
const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 };
const pid = forkpty(&master, null, null, &ws);
if (pid < 0) return error.ForkFailed;
@@ -896,6 +996,7 @@ pub fn forkShell(
posix.sigaddset(&set, posix.SIG.WINCH);
posix.sigprocmask(posix.SIG.UNBLOCK, &set, null);
if (cwd_z) |path| if (chdir(path.ptr) != 0) _exit(126);
+ if (envp) |env| _ = execve(spawn.path, &spawn.argv, env);
_ = execv(spawn.path, &spawn.argv);
_exit(127);
}
@@ -925,6 +1026,42 @@ test "shell spawn rejects invalid directories before creating a child" {
try std.testing.expectError(error.FileNotFound, forkShell(null, 0, &rcs, "/bin/sh", missing, 24, 80, null));
}
+test "a forked shell is told which terminal it is talking to" {
+ if (!haveFile("/bin/sh")) return error.SkipZigTest;
+ // Whatever launched the test is the wrong answer, so make it a loud one.
+ const saved = libc.getenv("TERM");
+ var saved_buf: [256]u8 = undefined;
+ const restore: ?[:0]const u8 = if (saved) |t| blk: {
+ const span = std.mem.span(t);
+ if (span.len >= saved_buf.len) break :blk null;
+ @memcpy(saved_buf[0..span.len], span);
+ saved_buf[span.len] = 0;
+ break :blk saved_buf[0..span.len :0];
+ } else null;
+ defer if (restore) |t| {
+ _ = Shell.setenv("TERM", t.ptr, 1);
+ } else {
+ _ = unsetenv("TERM");
+ };
+ _ = Shell.setenv("TERM", "dumb", 1);
+
+ const rcs: Shell.PromptFiles = .{};
+ const child = try forkShell(null, 0, &rcs, "/bin/sh", "", 24, 80, null);
+ defer {
+ _ = libc.kill(child.pid, libc.SIG.KILL);
+ _ = libc.waitpid(child.pid, null, 0);
+ _ = libc.close(child.file.handle);
+ }
+ var sh: TestShell = .{ .master = child.file.handle, .pid = child.pid };
+ // The echoed command carries the unexpanded `$TERM`, so only the shell's
+ // own expansion can match this.
+ try std.testing.expect(writeFd(child.file.handle, "printf '\nPARDES-TERM:%s:%s\n' \"$TERM\" \"$COLORTERM\"; exit\n"));
+ try std.testing.expect(sh.waitText(
+ "PARDES-TERM:" ++ pardes.config.child_term ++ ":" ++ pardes.config.child_colorterm,
+ 5_000,
+ ));
+}
+
test "shell spawn uses an explicit OS directory longer than 256 bytes" {
if (!haveFile("/bin/sh")) return error.SkipZigTest;
var tmp = std.testing.tmpDir(.{});
@@ -1031,6 +1168,14 @@ extern "c" fn tcgetpgrp(fd: c_int) libc.pid_t;
const occ_max_depth: u8 = 8;
const occ_max_visited: usize = 32;
+/// Where `ttyTaken` answers from evidence rather than from `false`. The two
+/// implementations are different walks over the same question, so they share
+/// one suite rather than each asserting half of it.
+const tty_probe_platform = switch (builtin.os.tag) {
+ .linux, .macos => true,
+ else => false,
+};
+
const TtyProbe = struct {
self_exe: [std.fs.max_path_bytes]u8 = undefined,
exe: [std.fs.max_path_bytes]u8 = undefined,
@@ -1085,10 +1230,94 @@ pub fn ttyTaken(shell_pid: libc.pid_t, master_fd: c_int) bool {
}
return !saw_fg;
},
+ .macos => {
+ const fg = tcgetpgrp(master_fd);
+ if (fg <= 0) return false;
+ if (fg == shell_pid) return false;
+ return darwin.taken(shell_pid, fg);
+ },
else => return false,
}
}
+/// The same question the Linux walk asks, answered the way macOS can answer
+/// it. There is no /proc to descend and no children list, but the tty's
+/// foreground process group can be enumerated in one call — and "is anything
+/// but the shell itself holding this terminal?" is exactly a statement about
+/// the executables in that group. The test is the shell's own executable and
+/// never its pid, which is what keeps a nested interactive shell a prompt,
+/// and what still catches `bash -c 'sleep 30'`: the group holds `sleep` too.
+///
+/// Until this existed, `ttyTaken` answered `false` on darwin for every pane,
+/// so `Pardes.takesCommandLine` was permanently true and every bare Escape in
+/// a raw terminal ran the `Last` builtin instead of reaching the child.
+const darwin = struct {
+ const proc_pgrp_only: u32 = 2;
+ const proc_pidt_shortbsdinfo: c_int = 13;
+ const zombie_status: u32 = 5; // SZOMB
+ /// PROC_PIDPATHINFO_MAXSIZE.
+ const path_max = 4 * 1024;
+ /// A foreground group is a pipeline; anything larger is not a prompt.
+ const group_max = 64;
+
+ const ShortBsdInfo = extern struct {
+ pid: u32,
+ ppid: u32,
+ pgid: u32,
+ status: u32,
+ comm: [16]u8,
+ flags: u32,
+ uid: u32,
+ gid: u32,
+ ruid: u32,
+ rgid: u32,
+ svuid: u32,
+ svgid: u32,
+ rfu: u32,
+ };
+
+ extern "c" fn proc_listpids(kind: u32, typeinfo: u32, buffer: ?*anyopaque, buffersize: c_int) c_int;
+ extern "c" fn proc_pidpath(pid: c_int, buffer: [*]u8, buffersize: u32) c_int;
+
+ fn exe(pid: libc.pid_t, buf: *[path_max]u8) ?[]const u8 {
+ const got = proc_pidpath(pid, buf, buf.len);
+ if (got <= 0) return null;
+ return buf[0..@intCast(got)];
+ }
+
+ /// A pid with no readable path is either gone or not ours. Only a zombie
+ /// is provably harmless — it holds no tty — so everything else is taken.
+ fn zombie(pid: libc.pid_t) bool {
+ var info: ShortBsdInfo = undefined;
+ const got = proc_pidinfo(pid, proc_pidt_shortbsdinfo, 0, &info, @sizeOf(ShortBsdInfo));
+ if (got != @sizeOf(ShortBsdInfo)) return false;
+ return info.status == zombie_status;
+ }
+
+ fn taken(shell_pid: libc.pid_t, fg: libc.pid_t) bool {
+ var shell_buf: [path_max]u8 = undefined;
+ const shell_exe = exe(shell_pid, &shell_buf) orelse return false;
+ var group: [group_max]libc.pid_t = undefined;
+ const bytes = proc_listpids(proc_pgrp_only, @intCast(fg), &group, @sizeOf(@TypeOf(group)));
+ // An empty group is a group whose last member just exited: the shell
+ // is about to have its terminal back, and calling that taken would
+ // blink the prompt heuristic off for a frame every time a job ends.
+ if (bytes <= 0) return false;
+ const n = @as(usize, @intCast(bytes)) / @sizeOf(libc.pid_t);
+ if (n >= group.len) return true;
+ var exe_buf: [path_max]u8 = undefined;
+ for (group[0..n]) |pid| {
+ if (pid <= 0 or pid == shell_pid) continue;
+ const path = exe(pid, &exe_buf) orelse {
+ if (zombie(pid)) continue;
+ return true;
+ };
+ if (!std.mem.eql(u8, path, shell_exe)) return true;
+ }
+ return false;
+ }
+};
+
pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySignal) void {
const sig = switch (which) {
.int => libc.SIG.INT,
@@ -1329,23 +1558,33 @@ const TestShell = struct {
}
fn stop(sh: *TestShell) void {
- var probe: TtyProbe = undefined;
- var pending: usize = 0;
- var doomed: [occ_max_visited]libc.pid_t = undefined;
- var n: usize = 0;
- _ = pushChildren(&probe, &pending, sh.pid, 1);
- while (pending > 0) {
- pending -= 1;
- const node = probe.pending[pending];
- if (n == doomed.len) break;
- doomed[n] = node.pid;
- n += 1;
- if (node.depth < occ_max_depth) _ = pushChildren(&probe, &pending, node.pid, node.depth + 1);
- }
- _ = libc.kill(sh.pid, libc.SIG.KILL);
- for (doomed[0..n]) |kid| {
- _ = libc.kill(kid, libc.SIG.KILL);
- _ = libc.kill(-kid, libc.SIG.KILL);
+ if (comptime builtin.os.tag == .linux) {
+ var probe: TtyProbe = undefined;
+ var pending: usize = 0;
+ var doomed: [occ_max_visited]libc.pid_t = undefined;
+ var n: usize = 0;
+ _ = pushChildren(&probe, &pending, sh.pid, 1);
+ while (pending > 0) {
+ pending -= 1;
+ const node = probe.pending[pending];
+ if (n == doomed.len) break;
+ doomed[n] = node.pid;
+ n += 1;
+ if (node.depth < occ_max_depth) _ = pushChildren(&probe, &pending, node.pid, node.depth + 1);
+ }
+ _ = libc.kill(sh.pid, libc.SIG.KILL);
+ for (doomed[0..n]) |kid| {
+ _ = libc.kill(kid, libc.SIG.KILL);
+ _ = libc.kill(-kid, libc.SIG.KILL);
+ }
+ } else {
+ // No children list to descend. forkpty made the shell a session
+ // leader, so its own group plus whichever group currently holds
+ // the tty covers the job the test left running.
+ const fg = tcgetpgrp(sh.master);
+ if (fg > 0 and fg != sh.pid) _ = libc.kill(-fg, libc.SIG.KILL);
+ _ = libc.kill(-sh.pid, libc.SIG.KILL);
+ _ = libc.kill(sh.pid, libc.SIG.KILL);
}
_ = libc.waitpid(sh.pid, null, 0);
_ = libc.close(sh.master);
@@ -1374,7 +1613,7 @@ fn sleepMs(ms: i64) void {
}
test "an idle prompt is free, a foreground job takes the tty, and Ctrl-C hands it back" {
- if (comptime builtin.os.tag != .linux) return error.SkipZigTest;
+ if (comptime !tty_probe_platform) return error.SkipZigTest;
var sh = TestShell.start() orelse return error.SkipZigTest;
defer sh.stop();
@@ -1390,7 +1629,7 @@ test "an idle prompt is free, a foreground job takes the tty, and Ctrl-C hands i
}
test "a background job is not the tty's owner" {
- if (comptime builtin.os.tag != .linux) return error.SkipZigTest;
+ if (comptime !tty_probe_platform) return error.SkipZigTest;
var sh = TestShell.start() orelse return error.SkipZigTest;
defer sh.stop();
@@ -1403,7 +1642,7 @@ test "a background job is not the tty's owner" {
}
test "a nested interactive shell is still a prompt" {
- if (comptime builtin.os.tag != .linux) return error.SkipZigTest;
+ if (comptime !tty_probe_platform) return error.SkipZigTest;
var sh = TestShell.start() orelse return error.SkipZigTest;
defer sh.stop();
@@ -1424,7 +1663,7 @@ test "a nested interactive shell is still a prompt" {
}
test "the walk reaches the leaf: bash -c 'sleep 30' takes the tty" {
- if (comptime builtin.os.tag != .linux) return error.SkipZigTest;
+ if (comptime !tty_probe_platform) return error.SkipZigTest;
var sh = TestShell.start() orelse return error.SkipZigTest;
defer sh.stop();
@@ -1436,23 +1675,40 @@ test "the walk reaches the leaf: bash -c 'sleep 30' takes the tty" {
sh.send("bash --norc -c 'sleep 30; :'\n");
try std.testing.expect(sh.waitTaken(true, 10_000));
- var probe: TtyProbe = undefined;
- var pending: usize = 0;
- try std.testing.expectEqual(Pushed.pushed, pushChildren(&probe, &pending, sh.pid, 1));
- try std.testing.expectEqual(@as(usize, 1), pending);
- var wrapper_buf: [std.fs.max_path_bytes]u8 = undefined;
- var shell_buf: [std.fs.max_path_bytes]u8 = undefined;
- try std.testing.expectEqualStrings(
- procExe(sh.pid, &shell_buf).?,
- procExe(probe.pending[0].pid, &wrapper_buf).?,
- );
+ // Why the shallow answer is the wrong one, in each walk's own evidence:
+ // the wrapper between the shell and `sleep` wears the shell's executable,
+ // so a probe that stopped at it would report a prompt.
+ if (comptime builtin.os.tag == .linux) {
+ var probe: TtyProbe = undefined;
+ var pending: usize = 0;
+ try std.testing.expectEqual(Pushed.pushed, pushChildren(&probe, &pending, sh.pid, 1));
+ try std.testing.expectEqual(@as(usize, 1), pending);
+ var wrapper_buf: [std.fs.max_path_bytes]u8 = undefined;
+ var shell_buf: [std.fs.max_path_bytes]u8 = undefined;
+ try std.testing.expectEqualStrings(
+ procExe(sh.pid, &shell_buf).?,
+ procExe(probe.pending[0].pid, &wrapper_buf).?,
+ );
+ } else {
+ const fg = tcgetpgrp(sh.master);
+ try std.testing.expect(fg > 0 and fg != sh.pid);
+ var leader_buf: [darwin.path_max]u8 = undefined;
+ var shell_buf: [darwin.path_max]u8 = undefined;
+ // The group's leader IS that wrapper, and it is the shell's binary.
+ try std.testing.expectEqualStrings(
+ darwin.exe(sh.pid, &shell_buf).?,
+ darwin.exe(fg, &leader_buf).?,
+ );
+ // Taken all the same, because the rest of the group is not.
+ try std.testing.expect(darwin.taken(sh.pid, fg));
+ }
sh.send("\x03");
try std.testing.expect(sh.waitTaken(false, 10_000));
}
test "a full-screen program takes the tty until it quits" {
- if (comptime builtin.os.tag != .linux) return error.SkipZigTest;
+ if (comptime !tty_probe_platform) return error.SkipZigTest;
const cases = [_]struct { bin: [*:0]const u8, run: []const u8, quit: []const u8 }{
.{ .bin = "/usr/bin/vim", .run = "vim -u NONE -i NONE\n", .quit = "\x1b:q!\r" },
.{ .bin = "/usr/bin/less", .run = "env LESS= less /etc/hosts\n", .quit = "q" },
diff --git a/src/layout.zig b/src/layout.zig
index 47b2c1a4..c6cb7503 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -8,7 +8,7 @@ pub const Rect = struct { x: u16 = 0, y: u16 = 0, w: u16 = 0, h: u16 = 0 };
pub const Snapshot = struct { serial: u32, box: Box };
const MAX_COLS = pardes.MAX_COLS;
const MAX_PANES = pardes.MAX_PANES;
-const TOPBAR_H = pardes.TOPBAR_H;
+
const BOX_H = pardes.BOX_H;
pub const Presentation = struct {
@@ -795,7 +795,7 @@ pub fn movePlacement(p: *Pardes, id: usize, cur_x: u16, cur_y: u16) ?MovePlaceme
const sib = if (src.idx > 0) p.col_panes[src.col][src.idx - 1] else p.col_panes[src.col][src.idx + 1];
heights[sib] +|= p.rects[id].h;
}
- var y: u16 = TOPBAR_H + p.columnBarHeight();
+ var y: u16 = p.topBarHeight() + p.columnBarHeight();
var last: ?MovePlacement = null;
for (0..p.col_n[tc]) |k| {
const pid = p.col_panes[tc][k];
@@ -1123,7 +1123,7 @@ fn collapseNeighbor(p: *Pardes, col: usize, index: usize) ?usize {
}
fn computeCollapsedColumn(p: *Pardes, c: usize, x: u16, width: u16, folded: usize) void {
- var y: u16 = TOPBAR_H + p.columnBarHeight();
+ var y: u16 = p.topBarHeight() + p.columnBarHeight();
const available = p.screen_h -| y;
var expanded_left: usize = 0;
var weight: f32 = 0;
@@ -1196,7 +1196,7 @@ pub fn compute(p: *Pardes) void {
}
if (vsum <= 0) vsum = 1;
- var y: u16 = TOPBAR_H + p.columnBarHeight();
+ var y: u16 = p.topBarHeight() + p.columnBarHeight();
const avail_h = p.screen_h -| y;
for (0..p.col_n[c]) |k| {
const id = p.col_panes[c][k];
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" {
diff --git a/src/macos/Sources/AppDelegate.swift b/src/macos/Sources/AppDelegate.swift
index cbe9cdc9..3cce7592 100644
--- a/src/macos/Sources/AppDelegate.swift
+++ b/src/macos/Sources/AppDelegate.swift
@@ -310,6 +310,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate {
file.addItem(withTitle: "Close Window",
action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w")
+ // The workspace tag row's commands, in the place macOS keeps window
+ // commands. The build hands that row to this menu bar (see
+ // docs/macos.md): the words are the same builtins the tag carried,
+ // spelled exactly as a tag Exec would type them, and nothing here is
+ // editable — the menu is the tag's buttons, not the tag.
+ let builtins = submenu("Builtins", of: main)
+ for word in ["Newcol", "Joincol", "Find", "Grep", "Changelog",
+ "Dump", "NextColor", "Debug", "Kill"] {
+ builtins.addItem(command(word, #selector(menuBuiltin(_:)), ""))
+ }
let edit = submenu("Edit", of: main)
// Paste and nothing else. Copy, Cut, Undo and Select All have no
// builtin behind them — the core yanks into the clipboard on its own
@@ -379,13 +389,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate {
// here would hide half of what Look does.
panel.canChooseDirectories = true
panel.allowsMultipleSelection = true
- panel.resolvesAliases = true
- guard panel.runModal() == .OK else { return }
for url in panel.urls where url.isFileURL {
look(url.path)
}
}
+ /// Any workspace-tag word. The title is the builtin's own spelling, so
+ /// this is the same `run` a tag Exec performs and there is no second
+ /// vocabulary to keep in step.
+ @objc private func menuBuiltin(_ sender: NSMenuItem) { run(sender.title) }
+
+
@objc private func menuNew(_ sender: Any?) { run("New") }
@objc private func menuSave(_ sender: Any?) { run("Save") }
@objc private func menuHelp(_ sender: Any?) { run("Help") }
diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift
index 7981cf46..5ad09c38 100644
--- a/src/macos/Sources/PardesView.swift
+++ b/src/macos/Sources/PardesView.swift
@@ -433,7 +433,26 @@ private struct Metrics {
let symbolic = (traits?[kCTFontSymbolicTrait] as? UInt32) ?? 0
return symbolic & UInt32(CTFontSymbolicTraits.traitBold.rawValue | CTFontSymbolicTraits.traitItalic.rawValue) == 0
}
- let face = CTFontCreateWithFontDescriptor(plain ?? descriptors[0], size, nil)
+ let chosen = CTFontCreateWithFontDescriptor(plain ?? descriptors[0], size, nil)
+ // A variable face's descriptor carries no variation, and CoreText
+ // picks whichever named instance it likes for it — Maple Mono's
+ // variable file comes up Thin. Pin every axis to its own default:
+ // the face the file itself calls regular.
+ var face = chosen
+ if let axes = CTFontCopyVariationAxes(face) as? [[CFString: Any]] {
+ var defaults: [CFNumber: Any] = [:]
+ for axis in axes {
+ guard let tag = axis[kCTFontVariationAxisIdentifierKey],
+ let value = axis[kCTFontVariationAxisDefaultValueKey] else { continue }
+ defaults[tag as! CFNumber] = value
+ }
+ if !defaults.isEmpty {
+ let descriptor = CTFontDescriptorCreateCopyWithAttributes(
+ CTFontCopyFontDescriptor(face),
+ [kCTFontVariationAttribute: defaults] as CFDictionary)
+ face = CTFontCreateWithFontDescriptor(descriptor, size, nil)
+ }
+ }
// The core already filtered for fixed pitch by reading the file's own
// advances. This is the same question asked of the face CoreText
// actually built, which is the one that will be drawn with.
@@ -577,7 +596,14 @@ final class PardesView: NSView {
// full redraw does not allocate 24 times.
private var runGlyphs: [CGGlyph] = []
private var runPositions: [CGPoint] = []
-
+ /// Ligature shaping, keyed by face and text. A sequence ligates when
+ /// shaping it produces fewer glyphs than it has characters — the only
+ /// answer CoreText gives without interrogating the font's own features.
+ /// Cleared with the glyph caches, because it belongs to one face.
+ private var ligatureShaped: [String: Bool] = [:]
+ /// Columns the current row has already consumed as part of a ligature
+ /// drawn further left. One row's worth at a time.
+ private var ligatureCovered: Set<Int> = []
/// Constructed lazily because the ordinary path is still a direct
/// CoreText/CoreGraphics frame. No scene bit and no panel track means no
/// bitmap, no Core Image context and no Metal work.
@@ -748,6 +774,7 @@ final class PardesView: NSView {
bodyGlyphCache.removeAll(keepingCapacity: true)
taglineGlyphCache.removeAll(keepingCapacity: true)
gripGlyphCache.removeAll(keepingCapacity: true)
+ ligatureShaped.removeAll(keepingCapacity: true)
metrics = next
taglines = TaglineMetrics(body: next, percent: taglines.percent, scale: scale)
metricsScale = scale
@@ -844,12 +871,12 @@ final class PardesView: NSView {
// whatever we do, so clipping would save fills and nothing else. Narrow
// the row loops to the dirty band if that ever shows up in a profile.
let count = pardes_frame()
- let nextLinkPointer = pardes_pointer_shape() == 1
- if nextLinkPointer != linkPointer {
- linkPointer = nextLinkPointer
+ let nextPointer = pointerShape()
+ if nextPointer != pointer {
+ pointer = nextPointer
window?.invalidateCursorRects(for: self)
if let window, bounds.contains(convert(window.mouseLocationOutsideOfEventStream, from: nil)) {
- (linkPointer ? NSCursor.pointingHand : NSCursor.iBeam).set()
+ cursorFor(pointer).set()
}
}
let scene = pardes_scene()
@@ -1178,10 +1205,13 @@ final class PardesView: NSView {
drawTagLayers(ctx, hideCursor:hideCursor, canonical:cells, cols:cols, rows:rows)
} else {
for layer in presentedTagLayers {
- drawTagGrip(ctx, values:layer.values, canonical:cells, cols:cols, rows:rows)
- layer.cells.withUnsafeBufferPointer { cells in
- if let base = cells.baseAddress { drawTagLayer(ctx, values:layer.values, cells:base, hideCursor:true) }
+ layer.cells.withUnsafeBufferPointer { tagCells in
+ if let base = tagCells.baseAddress {
+ drawTagLayer(ctx, values: layer.values, cells: base, hideCursor: true,
+ canonical: cells, cols: cols)
+ }
}
+ drawTagGrip(ctx, values: layer.values, canonical: cells, cols: cols, rows: rows)
}
for layer in presentedBodyLayers {
layer.cells.withUnsafeBufferPointer { cells in
@@ -1190,6 +1220,7 @@ final class PardesView: NSView {
}
drawTagRules(ctx, layers: presentedTagLayers.map { $0.values })
}
+ drawHoverGlass(ctx, cells: cells, cols: cols, rows: rows)
}
private var presentedTagLayers: [FrozenBodyLayer] = []
@@ -1212,16 +1243,22 @@ final class PardesView: NSView {
let values = (0...12).map { Int(pardes_tag_layer_value(index, UInt32($0))) }
if values[0] == 0 { continue }
layers.append(values)
- drawTagGrip(ctx, values:values, canonical:canonical, cols:cols, rows:rows)
- if let cells = pardes_tag_layer_cells(index) { drawTagLayer(ctx, values: values, cells: cells, hideCursor: hideCursor) }
+ // The layer's full-row fill must land before the grip: the row
+ // background now spans the anchor columns, and the grip glyph
+ // and the anchor fills have to come back on top of it.
+ if let cells = pardes_tag_layer_cells(index) {
+ drawTagLayer(ctx, values: values, cells: cells, hideCursor: hideCursor,
+ canonical: canonical, cols: cols)
+ }
+ drawTagGrip(ctx, values: values, canonical: canonical, cols: cols, rows: rows)
}
drawTagRules(ctx, layers: layers)
}
/// SDL's chrome overlay is drawn after its compact layers. Keep these
- /// one-pixel rules above the full-row tag backgrounds as well.
+ /// one-point rules above the full-row tag backgrounds as well.
private func drawTagRules(_ ctx: CGContext, layers: [[Int]]) {
- let pixel = 1 / metricsScale
+ let pixel = CGFloat(1)
for values in layers where values[1] != 0 {
let kind = values[1]
let bottom = kind == 2 && values[11] != 0
@@ -1252,15 +1289,17 @@ final class PardesView: NSView {
}
private func drawTagGrip(_ ctx: CGContext, values: [Int], canonical: UnsafePointer<pardes_cell_s>, cols: Int, rows: Int) {
- let width = Int(pardes_grip_columns())
let x = values[4] - Int(pardes_tag_text_inset()), y = values[5]
if values[1] != 2 || x < 0 || x >= cols || y < 0 || y >= rows { return }
var cell = canonical[y * cols + x]
let text = withUnsafeBytes(of: &cell.text) { String(decoding: $0.prefix(Int(cell.len)), as: UTF8.self) }
if text.isEmpty { return }
let style = resolve(cell, block:false, ground:themeBG ?? pardesDefaultBG, clearGround:false)
- let centerX = (CGFloat(x) + CGFloat(width) / 2) * cellWidth
- let centerY = CGFloat(y) * cellHeight + taglines.top(row:y, canvasHeight:bounds.height) + taglines.height / 2
+ // The box drawTagLayer filled is this mark's ground, so the mark
+ // centres on that same square rather than on the grip columns: one
+ // rect measures both and the two cannot drift apart.
+ guard let square = anchorSquare(values: values) else { return }
+ let centerX = square.midX, centerY = square.midY
if text != " " {
let face = Face(bold: cell.attrs & UInt16(PARDES_ATTR_BOLD) != 0,
italic: cell.attrs & UInt16(PARDES_ATTR_ITALIC) != 0)
@@ -1268,9 +1307,7 @@ final class PardesView: NSView {
let originX = ((centerX - measured.ink.midX) * metricsScale).rounded() / metricsScale
let baseline = ((bounds.height - centerY - measured.ink.midY) * metricsScale).rounded() / metricsScale
ctx.saveGState()
- ctx.clip(to: CGRect(x: CGFloat(x) * cellWidth,
- y: CGFloat(y) * cellHeight + taglines.top(row:y, canvasHeight:bounds.height),
- width: CGFloat(width) * cellWidth, height: taglines.height))
+ ctx.clip(to: square)
ctx.translateBy(x: 0, y: bounds.height)
ctx.scaleBy(x: 1, y: -1)
ctx.textMatrix = .identity
@@ -1286,16 +1323,36 @@ final class PardesView: NSView {
fill(ctx, CGRect(x:floor(centerX-halfWidth), y:floor(centerY+1), width:2*halfWidth, height:1), style.fg, 1)
}
- private func drawTagLayer(_ ctx: CGContext, values: [Int], cells: UnsafePointer<pardes_cell_s>, hideCursor: Bool) {
+ private func drawTagLayer(_ ctx: CGContext, values: [Int], cells: UnsafePointer<pardes_cell_s>,
+ hideCursor: Bool, canonical: UnsafePointer<pardes_cell_s>, cols: Int) {
let left = CGFloat(values[4]) * cellWidth
let y = CGFloat(values[5]) * cellHeight + taglines.top(row: values[5], canvasHeight: bounds.height)
// SDL gives the tag a full body-row background; only its glyphs
- // and per-cell highlights use the compact band.
- let viewport = CGRect(x: left, y: CGFloat(values[5]) * cellHeight,
- width: CGFloat(values[6]) * cellWidth, height: cellHeight)
+ // and per-cell highlights use the compact band. The band spans the
+ // row's whole extent, not just its text: the grip and the mode box
+ // live in the inset columns to the left of `values[4]`, and their
+ // own fills cover only the compact band — a row background that
+ // stops at the text leaves holes of ground under those anchors.
+ // The rule in drawTagRules spans exactly this left-to-right range.
+ let rowLeft = CGFloat(max(0, values[4] - Int(pardes_tag_text_inset()))) * cellWidth
+ let rowRight = CGFloat(values[4] + values[6]) * cellWidth
+ let viewport = CGRect(x: rowLeft, y: CGFloat(values[5]) * cellHeight,
+ width: rowRight - rowLeft, height: cellHeight)
ctx.saveGState()
ctx.clip(to: viewport)
fillBackground(ctx, viewport, UInt32(values[10]), backgroundAlpha)
+ // The anchor (grip, mode box) lives in the inset columns that fill
+ // just covered, so its own box goes back on top: one solid fill in
+ // the box colour the anchor cell wears, with drawTagGrip's mark
+ // drawn over it afterwards.
+ if let square = anchorSquare(values: values) {
+ let anchorX = max(0, values[4] - Int(pardes_tag_text_inset()))
+ let cell = canonical[values[5] * cols + anchorX]
+ let style = resolve(cell, block: false,
+ ground: UInt32(values[10]), clearGround: false)
+ if style.visible { fillBackground(ctx, square.integral, style.bg, backgroundAlpha) }
+ }
+
for pass in 0...1 {
for col in 0..<values[0] {
var cell = cells[col]
@@ -1331,6 +1388,184 @@ final class PardesView: NSView {
ctx.restoreGState()
}
+ /// The anchor square: one box drawn from the tag band's height — square,
+ /// at the row's left edge, inset by the same amount on both axes —
+ /// shared by the fill in drawTagLayer and the glyph in drawTagGrip so
+ /// the two can never disagree.
+ private func anchorSquare(values: [Int]) -> CGRect? {
+ let anchorX = max(0, values[4] - Int(pardes_tag_text_inset()))
+ guard anchorX < values[4] else { return nil }
+ let pad = (cellHeight - taglines.height) / 2
+ return CGRect(x: CGFloat(anchorX) * cellWidth + pad,
+ y: CGFloat(values[5]) * cellHeight + taglines.top(row: values[5], canvasHeight: bounds.height),
+ width: taglines.height, height: taglines.height)
+ }
+
+ /// The look-hover affordance as liquid glass: the core marks the word's
+ /// cells (PARDES_CELL_HOVER), and this composes a real material over the
+ /// run — the drawn frame snapshot refracted through CoreImage (a small
+ /// blur, then a lens scale about the pill's centre, which is what makes
+ /// the edges swim), the theme's selection colour as a faint tint, a
+ /// specular sheen from the top edge, and a light rim — with the word's
+ /// own glyphs drawn back on top, crisp, the way text sits on a real
+ /// material rather than behind one.
+ private func drawHoverGlass(_ ctx: CGContext, cells: UnsafePointer<pardes_cell_s>, cols: Int, rows: Int) {
+ var drew = false
+ for row in 0..<rows {
+ var start = -1
+ var tint: UInt32 = 0
+ for col in 0...cols {
+ let hover = col < cols
+ && cells[row * cols + col].flags & UInt8(PARDES_CELL_HOVER) != 0
+ if hover, start < 0 {
+ start = col
+ tint = cells[row * cols + col].bg
+ }
+ if !hover, start >= 0 {
+ let rect = CGRect(x: CGFloat(start) * cellWidth, y: CGFloat(row) * cellHeight,
+ width: CGFloat(col - start) * cellWidth, height: cellHeight)
+ drawLiquidGlass(ctx, rect, tint: tint)
+ redrawHoverRun(ctx, cells, cols: cols, row: row, start: start, end: col)
+ drew = true
+ start = -1
+ }
+ }
+ }
+ // No affordance this frame: the held snapshot describes a frame
+ // that no longer exists, and the next hover must take a fresh one.
+ if !drew { glassShot = nil }
+ }
+
+ /// A CoreImage context for the glass refraction, created once.
+ private lazy var glassContext = CIContext()
+ /// The snapshot behind the current pill. Refraction reads the already
+ /// drawn frame, and re-reading it every pump would blur a moving pill
+ /// out of its own budget, so the shot is taken when the rect changes.
+ private var glassShot: (rect: CGRect, image: CGImage)?
+
+ private func drawLiquidGlass(_ ctx: CGContext, _ rect: CGRect, tint: UInt32) {
+ let radius = min(rect.height / 4, 6)
+ let pill = CGPath(roundedRect: rect.insetBy(dx: 1, dy: 1.5),
+ cornerWidth: radius, cornerHeight: radius, transform: nil)
+ // The refraction: the frame as drawn, blurred a little and scaled
+ // about the pill's centre, so the rim bends what surrounds it. The
+ // shot extends past the pill for exactly this — the lens needs
+ // pixels from outside the glass to pull in.
+ let lens = rect.insetBy(dx: -10, dy: -10)
+ if refract(ctx, pill: pill, rect: rect, lens: lens, tint: tint) {
+ // Specular: light caught at the top edge, fading as it falls.
+ ctx.saveGState()
+ ctx.addPath(pill)
+ ctx.clip()
+ let shine = [CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 0.22),
+ CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 0.0)] as CFArray
+ if let gradient = CGGradient(colorsSpace: CGColorSpace(name: CGColorSpace.sRGB),
+ colors: shine, locations: [0, 0.55]) {
+ ctx.drawLinearGradient(gradient,
+ start: CGPoint(x: rect.minX, y: rect.minY),
+ end: CGPoint(x: rect.minX, y: rect.maxY), options: [])
+ }
+ ctx.restoreGState()
+ } else {
+ // No refraction without a snapshot: the tint alone, faint.
+ ctx.saveGState()
+ ctx.addPath(pill)
+ ctx.clip()
+ ctx.setFillColor(CGColor(srgbRed: CGFloat((tint >> 16) & 0xFF) / 255,
+ green: CGFloat((tint >> 8) & 0xFF) / 255,
+ blue: CGFloat(tint & 0xFF) / 255,
+ alpha: 0.18))
+ ctx.fill(rect)
+ ctx.restoreGState()
+ }
+ // The lens edge: a light rim and a darker bottom hairline, the way a
+ // real material catches its own edge.
+ ctx.addPath(pill)
+ ctx.setStrokeColor(CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 0.35))
+ ctx.setLineWidth(1)
+ ctx.strokePath()
+ ctx.addPath(pill)
+ ctx.setStrokeColor(CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 0.25))
+ ctx.setLineWidth(0.5)
+ ctx.clip()
+ ctx.stroke(CGRect(x: rect.minX, y: rect.maxY - 2, width: rect.width, height: 1.5))
+ }
+
+ /// True when the refraction composited. The snapshot is taken when the
+ /// pill moves and reused while it sits still.
+ private func refract(_ ctx: CGContext, pill: CGPath, rect: CGRect, lens: CGRect, tint: UInt32) -> Bool {
+ guard let shot = snapshotFor(ctx, lens: lens) else { return false }
+ var image = CIImage(cgImage: shot)
+ // The blur is grown slightly past the crop and the whole thing is
+ // pulled toward the centre by a few percent: the rim bends what
+ // surrounds it, which is the refraction the eye reads as glass.
+ let blur = image.applyingGaussianBlur(sigma: 2 * metricsScale)
+ let grown = blur.extent.insetBy(dx: -8 * metricsScale, dy: -8 * metricsScale)
+ let centre = CGPoint(x: grown.midX, y: grown.midY)
+ let scale = 1.06
+ let lensTransform = CGAffineTransform(translationX: centre.x * (1 - scale),
+ y: centre.y * (1 - scale)).scaledBy(x: scale, y: scale)
+ image = blur.transformed(by: lensTransform)
+ guard let filtered = glassContext.createCGImage(image, from: grown) else { return false }
+ ctx.saveGState()
+ ctx.addPath(pill)
+ ctx.clip()
+ ctx.draw(filtered, in: rect)
+ // The glass's own body colour: the theme's selection, faint — the
+ // refraction and the sheen carry the material, not opacity.
+ ctx.setFillColor(CGColor(srgbRed: CGFloat((tint >> 16) & 0xFF) / 255,
+ green: CGFloat((tint >> 8) & 0xFF) / 255,
+ blue: CGFloat(tint & 0xFF) / 255,
+ alpha: 0.15))
+ ctx.fill(rect)
+ ctx.restoreGState()
+ return true
+ }
+
+ private func snapshotFor(_ ctx: CGContext, lens: CGRect) -> CGImage? {
+ if let held = glassShot, held.rect == lens { return held.image }
+ guard let whole = ctx.makeImage() else { return nil }
+ // makeImage hands back backing pixels; the lens is in view points.
+ let region = CGRect(x: lens.minX * metricsScale, y: lens.minY * metricsScale,
+ width: lens.width * metricsScale, height: lens.height * metricsScale)
+ guard let cropped = whole.cropping(to: region) else { return nil }
+ glassShot = (lens, cropped)
+ return cropped
+ }
+
+ /// The hovered word's glyphs, back on top of the material, crisp: text
+ /// sits on liquid glass, not behind it.
+ private func redrawHoverRun(_ ctx: CGContext, _ cells: UnsafePointer<pardes_cell_s>,
+ cols: Int, row: Int, start: Int, end: Int) {
+ ctx.saveGState()
+ ctx.textMatrix = .identity
+ ctx.translateBy(x: 0, y: bounds.height)
+ ctx.scaleBy(x: 1, y: -1)
+ let baseline = bounds.height - (CGFloat(row) * cellHeight + metrics.ascent)
+ for col in start..<end {
+ let cell = cells[row * cols + col]
+ guard cell.flags & UInt8(PARDES_CELL_DEFAULT) == 0 else { continue }
+ let text = withUnsafeBytes(of: cell.text) { raw in
+ String(decoding: raw.prefix(Int(cell.len)), as: UTF8.self)
+ }
+ guard !text.isEmpty, text != " " else { continue }
+ let style = resolve(cell, block: false,
+ ground: themeBG ?? pardesDefaultBG, clearGround: false)
+ guard style.visible else { continue }
+ setFill(ctx, style.fg, style.alpha)
+ ctx.textPosition = CGPoint(x: CGFloat(col) * cellWidth, y: baseline)
+ let attributed = NSAttributedString(string: text, attributes: [
+ fontAttribute: metrics.fonts[Face(bold: cell.attrs & UInt16(PARDES_ATTR_BOLD) != 0,
+ italic: cell.attrs & UInt16(PARDES_ATTR_ITALIC) != 0).rawValue],
+ NSAttributedString.Key(kCTForegroundColorFromContextAttributeName as String): true])
+ CTLineDraw(CTLineCreateWithAttributedString(attributed as CFAttributedString), ctx)
+ }
+ ctx.textMatrix = .identity
+ ctx.restoreGState()
+ }
+
+
+
private struct FrozenBodyLayer {
let values: [Int]
let cells: [pardes_cell_s]
@@ -1626,6 +1861,7 @@ final class PardesView: NSView {
var runAlpha: CGFloat = 1
runGlyphs.removeAll(keepingCapacity: true)
runPositions.removeAll(keepingCapacity: true)
+ ligatureCovered.removeAll(keepingCapacity: true)
func flush() {
guard !runGlyphs.isEmpty else { return }
@@ -1640,6 +1876,7 @@ final class PardesView: NSView {
for col in 0..<cols {
let cell = cells[base + col]
+ if ligatureCovered.contains(col) { continue }
if cell.flags & UInt8(PARDES_CELL_DEFAULT) != 0 { continue }
let tagline = cell.flags & UInt8(PARDES_CELL_TAGLINE) != 0
let cellBaseline = tagline
@@ -1687,6 +1924,24 @@ final class PardesView: NSView {
let units = text.utf16
let known = units.count == 1 ? glyph(face, units.first!, tagline: tagline) : 0
if known != 0 {
+ // Ligatures: consecutive cells shaped together when the face
+ // collapses them into fewer glyphs. A monospace coding
+ // ligature advances by the same whole cells its characters
+ // did, so the grid cannot drift and only the ink changes.
+ if !tagline, let ligature = ligatureSpan(cells, base: base, cols: cols, col: col,
+ face: face, fg: style.fg, alpha: style.alpha,
+ blockCol: blockCol, paneGripCells: paneGripCells) {
+ flush()
+ setFill(ctx, style.fg, style.alpha)
+ let attributed = NSAttributedString(string: ligature.text,
+ attributes: [fontAttribute: metrics.fonts[face.rawValue],
+ NSAttributedString.Key(kCTForegroundColorFromContextAttributeName as String): true])
+ ctx.textPosition = CGPoint(x: x, y: cellBaseline)
+ CTLineDraw(CTLineCreateWithAttributedString(attributed as CFAttributedString), ctx)
+ ctx.textMatrix = .identity
+ for covered in 1..<ligature.covered { ligatureCovered.insert(col + covered) }
+ continue
+ }
if !runGlyphs.isEmpty
&& (face != runFace || tagline != runTagline
|| style.fg != runColor || style.alpha != runAlpha) {
@@ -1747,6 +2002,73 @@ final class PardesView: NSView {
return found
}
+ /// Ligature-prone character heads: a coding ligature always begins with
+ /// one of these, and refusing everything else keeps the per-frame cost of
+ /// ligature detection a set lookup rather than a shape.
+ private static let ligatureHeads: Set<Character> = [
+ "<", ">", "=", "!", "&", "|", "-", "/", ":", ".", "#", "*", "+", "~", "^", "%",
+ ]
+
+ private func ligates(_ face: Face, _ text: String) -> Bool {
+ let key = "\(face.rawValue):\(text)"
+ if let cached = ligatureShaped[key] { return cached }
+ let attributed = NSAttributedString(string: text, attributes: [fontAttribute: metrics.fonts[face.rawValue],
+ NSAttributedString.Key(kCTForegroundColorFromContextAttributeName as String): true])
+ let shaped = CTLineCreateWithAttributedString(attributed as CFAttributedString)
+ var glyphs: [CGGlyph] = []
+ for run in (CTLineGetGlyphRuns(shaped) as? [CTRun]) ?? [] {
+ let count = CTRunGetGlyphCount(run)
+ var runGlyphs = [CGGlyph](repeating: 0, count: count)
+ CTRunGetGlyphs(run, CFRange(location: 0, length: count), &runGlyphs)
+ glyphs.append(contentsOf: runGlyphs)
+ }
+ // A ligature is any divergence from per-character mapping. A font may
+ // collapse the pair into one glyph (JetBrains Mono) or swap each
+ // character for a joining half kept at the same count (Maple Mono) —
+ // either way the sequence must be shaped, not mapped.
+ let singles = text.utf16.map { glyph(face, $0, tagline: false) }
+ let result = glyphs.count != singles.count
+ || zip(glyphs, singles).contains { $0 != $1 }
+ ligatureShaped[key] = result
+ return result
+ }
+
+ /// When the cells at `col` ligate under `face`, the text to shape and how
+ /// many cells the ligature covers. Nil when they do not. Ligature cells
+ /// must carry the same face, colour and alpha and no rules of their own —
+ /// a ligature is one glyph of one style, not a style transition.
+ private func ligatureSpan(_ cells: UnsafePointer<pardes_cell_s>, base: Int, cols: Int, col: Int,
+ face: Face, fg: UInt32, alpha: CGFloat,
+ blockCol: Int, paneGripCells: Set<Int>) -> (text: String, covered: Int)? {
+ func eligible(_ index: Int) -> String? {
+ let cell = cells[base + index]
+ if cell.flags & UInt8(PARDES_CELL_DEFAULT) != 0 { return nil }
+ if cell.flags & UInt8(PARDES_CELL_TAGLINE) != 0 { return nil }
+ if cell.attrs >> UInt16(PARDES_ATTR_UL_SHIFT) != 0
+ || cell.attrs & UInt16(PARDES_ATTR_STRIKETHROUGH) != 0 { return nil }
+ if paneGripCells.contains(base + index) { return nil }
+ let text = withUnsafeBytes(of: cell.text) { raw in
+ String(decoding: raw.prefix(Int(cell.len)), as: UTF8.self)
+ }
+ guard text.utf16.count == 1 else { return nil }
+ let other = resolve(cell, block: blockCol == index,
+ ground: themeBG ?? pardesDefaultBG, clearGround: false)
+ guard other.visible, other.fg == fg, other.alpha == alpha,
+ Face(bold: cell.attrs & UInt16(PARDES_ATTR_BOLD) != 0,
+ italic: cell.attrs & UInt16(PARDES_ATTR_ITALIC) != 0) == face else { return nil }
+ return text
+ }
+ guard let head = eligible(col), PardesView.ligatureHeads.contains(head.first!) else { return nil }
+ guard col + 1 < cols, let second = eligible(col + 1) else { return nil }
+ let pair = head + second
+ if col + 2 < cols, let third = eligible(col + 2) {
+ let triple = pair + third
+ if ligates(face, triple) { return (triple, 3) }
+ }
+ if ligates(face, pair) { return (pair, 2) }
+ return nil
+ }
+
private func drawRules(
_ ctx: CGContext,
_ cell: pardes_cell_s,
@@ -1921,6 +2243,13 @@ final class PardesView: NSView {
override func keyDown(with event: NSEvent) {
let flags = event.modifierFlags
+ // Same reason as the trackpad trace below: whether AppKit delivered a
+ // keystroke at all is not something this process can otherwise answer,
+ // and "Escape does nothing" has two entirely different causes
+ // depending on whether this line prints.
+ trace("key: code=\(event.keyCode) mods=\(flags.rawValue) chars=" +
+ (event.charactersIgnoringModifiers?.unicodeScalars
+ .map { String($0.value, radix: 16) }.joined(separator: ",") ?? "-"))
// The ABI has no super bit, so a Command chord cannot be expressed at all.
// Anything the main menu claims never reaches here; the rest is swallowed
// rather than delivered as the bare keystroke the core would insert.
@@ -2328,12 +2657,35 @@ final class PardesView: NSView {
userInfo: nil))
}
- private var linkPointer = false
+ private enum Pointer: Equatable { case text, link, target }
+ private var pointer: Pointer = .text
+
+ /// The core's pointer_shape, in this shell's cursor vocabulary: a link
+ /// is a PDF hyperlink (the hand), a target is a word the core's own
+ /// Look-hover plumbing validated as a real Look or Exec operand, and
+ /// anything else is text — the I-beam.
+ private func pointerShape() -> Pointer {
+ switch pardes_pointer_shape() {
+ case 1: return .link
+ case 2: return .target
+ default: return .text
+ }
+ }
+
+ private func cursorFor(_ shape: Pointer) -> NSCursor {
+ switch shape {
+ case .link: return .pointingHand
+ case .target: return .arrow
+ case .text: return .iBeam
+ }
+ }
override func resetCursorRects() {
- // Every cell in this view is text, including the tags. An arrow over a
- // grid you can sweep and click words in is the wrong affordance.
- addCursorRect(bounds, cursor: linkPointer ? .pointingHand : .iBeam)
+ // Every cell in this view is text, including the tags. An arrow over
+ // plain text is the wrong affordance — but a word the core has
+ // validated as a Look or Exec target earns it: middle- and
+ // right-clicking there acts, and the cursor says so before it does.
+ addCursorRect(bounds, cursor: cursorFor(pointer))
}
override func setFrameSize(_ newSize: NSSize) {
diff --git a/src/macos/pardes.h b/src/macos/pardes.h
index df3ba04f..f74330e5 100644
--- a/src/macos/pardes.h
+++ b/src/macos/pardes.h
@@ -76,6 +76,11 @@ typedef struct {
#define PARDES_CELL_DEFAULT 0x01u
#define PARDES_CELL_TAGLINE 0x02u
+// The look-hover affordance: the word under the pointer is a real Look or
+// Exec operand. Hosts that draw the affordance as more than the quiet tint
+// (macOS composes a liquid-glass overlay) read this instead of guessing
+// from the background colour, which a real selection shares.
+#define PARDES_CELL_HOVER 0x04u
// One coherent snapshot for the full-window shader pass. The bitmask lets the
// host run CRT, ripple and glitch in one kernel instead of stacking filters;
@@ -323,6 +328,10 @@ uint32_t pardes_topbar_pane_border_rgb(void);
// pardes.TOPBAR_H, and src/macos.zig asserts the two agree.
#define PARDES_TOPBAR_H 1u
+// The active pane's tag row background, for hosts that afford the active
+// tagline more than a tint — macOS raises it like an old-school bevel.
+uint32_t pardes_tag_active_bg(void);
+
// Persistent full-window effect switches and their display-clock time. A zero
// flags word means draw the CoreText frame directly; any nonzero combination
// is one postprocess pass. Snapshot by value, with no borrowed storage.
@@ -510,7 +519,8 @@ bool pardes_active_dirty(void);
int32_t pardes_cursor_x(void);
int32_t pardes_cursor_y(void);
bool pardes_cursor_bar(void);
-// Mouse pointer affordance: 0 = default text pointer, 1 = link hand.
+// Mouse pointer affordance: 0 = default text pointer, 1 = link hand,
+// 2 = a validated Look/Exec target under the pointer (the acme arrow).
uint32_t pardes_pointer_shape(void);
// The Look or Exec the core performed since this was last asked, and clears
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|
diff --git a/src/themes/acme.zig b/src/themes/acme.zig
index 87183ebc..d91e626b 100644
--- a/src/themes/acme.zig
+++ b/src/themes/acme.zig
@@ -14,13 +14,13 @@ pub const theme = .{
.name = "acme",
.bg = .{ 0xff, 0xff, 0xea },
.fg = .{ 0x00, 0x00, 0x00 },
- .sel_bg = .{ 0xee, 0xee, 0x9e },
+ .sel_bg = .{ 0xfa, 0xfa, 0x92 },
.sel_fg = .{ 0x00, 0x00, 0x00 },
.tag_bg = .{ 0xea, 0xff, 0xff },
.tag_fg = .{ 0x00, 0x00, 0x00 },
- .box = .{ 0x6e, 0x4f, 0xd0 },
- .box_dim = .{ 0x38, 0x3c, 0x54 },
- .kw = .{ 0x88, 0x22, 0x99 },
+ .box = .{ 0x66, 0x3f, 0xe0 },
+ .box_dim = .{ 0x35, 0x3a, 0x58 },
+ .kw = .{ 0x97, 0x0d, 0xae },
.str = .{ 0x00, 0x77, 0x33 },
.num = .{ 0xa0, 0x52, 0x00 },
.comment = .{ 0x55, 0x55, 0x55 },
diff --git a/src/themes/atelier.zig b/src/themes/atelier.zig
index 0193e2e9..f367c341 100644
--- a/src/themes/atelier.zig
+++ b/src/themes/atelier.zig
@@ -11,27 +11,27 @@ pub const theme = .{
.tag_active_name_fg = .{ 0x65, 0x3f, 0x6b },
.tag_active_bg = .{ 0xd3, 0xe7, 0xdf },
.tag_active_fg = .{ 0x35, 0x58, 0x4d },
- .box = .{ 0x3e, 0x7a, 0x68 },
- .box_dim = .{ 0x99, 0xb6, 0xa3 },
- .column_box = .{ 0x88, 0x57, 0x22 },
- .column_box_dim = .{ 0xb6, 0xa3, 0x87 },
- .kw = .{ 0x76, 0x4d, 0x80 },
- .str = .{ 0x3c, 0x6c, 0x43 },
- .num = .{ 0x88, 0x57, 0x22 },
- .comment = .{ 0x35, 0x73, 0x3a },
+ .box = .{ 0x36, 0x82, 0x6b },
+ .box_dim = .{ 0x95, 0xba, 0xa2 },
+ .column_box = .{ 0x95, 0x58, 0x15 },
+ .column_box_dim = .{ 0xbc, 0xa4, 0x81 },
+ .kw = .{ 0x7b, 0x44, 0x89 },
+ .str = .{ 0x34, 0x74, 0x3d },
+ .num = .{ 0x9a, 0x58, 0x10 },
+ .comment = .{ 0x29, 0x7c, 0x30 },
.lineno = .{ 0x90, 0x98, 0x80 },
.lineno_active = .{ 0x88, 0x90, 0x78 },
.scroll_track = .{ 0xee, 0xec, 0xcf },
- .scroll_thumb = .{ 0x9d, 0xa9, 0x7b },
- .border = .{ 0xb8, 0xc8, 0xb4 },
- .sel_bg = .{ 0xe5, 0xe9, 0xae },
+ .scroll_thumb = .{ 0x9f, 0xae, 0x76 },
+ .border = .{ 0xff, 0xff, 0xff },
+ .sel_bg = .{ 0xed, 0xf2, 0xa5 },
.sel_fg = .{ 0x35, 0x49, 0x2d },
- .search_bg = .{ 0xf0, 0xd7, 0x94 },
+ .search_bg = .{ 0xfe, 0xdd, 0x86 },
.search_fg = .{ 0x61, 0x47, 0x15 },
- .diagnostic_error = .{ 0xa3, 0x3d, 0x37 },
- .diagnostic_warning = .{ 0x81, 0x59, 0x08 },
- .diagnostic_info = .{ 0x33, 0x65, 0x78 },
- .diagnostic_hint = .{ 0x52, 0x6e, 0x3d },
+ .diagnostic_error = .{ 0xb3, 0x2f, 0x27 },
+ .diagnostic_warning = .{ 0x89, 0x5c, 0x00 },
+ .diagnostic_info = .{ 0x29, 0x6a, 0x82 },
+ .diagnostic_hint = .{ 0x51, 0x75, 0x36 },
.palette = .{
.{ 0x29, 0x3f, 0x35 },
.{ 0xa3, 0x3d, 0x37 },
diff --git a/src/themes/clay.zig b/src/themes/clay.zig
index af824764..85487d48 100644
--- a/src/themes/clay.zig
+++ b/src/themes/clay.zig
@@ -12,27 +12,27 @@ pub const theme = .{
.tag_active_name_fg = .{ 0xb5, 0xec, 0xec },
.tag_active_bg = .{ 0x42, 0x4f, 0x40 },
.tag_active_fg = .{ 0xe3, 0xe2, 0xc2 },
- .box = .{ 0x8e, 0xc0, 0x7c },
- .box_dim = .{ 0x67, 0x77, 0x52 },
- .column_box = .{ 0xd3, 0x86, 0x9b },
- .column_box_dim = .{ 0x83, 0x62, 0x68 },
- .kw = .{ 0xfb, 0x8a, 0x69 },
- .str = .{ 0xb8, 0xbb, 0x26 },
- .num = .{ 0xd3, 0x86, 0x9b },
- .comment = .{ 0xe2, 0xb5, 0x6f },
+ .box = .{ 0x8a, 0xc8, 0x74 },
+ .box_dim = .{ 0x68, 0x7c, 0x4d },
+ .column_box = .{ 0xdd, 0x7c, 0x97 },
+ .column_box_dim = .{ 0x87, 0x5e, 0x65 },
+ .kw = .{ 0xff, 0x88, 0x65 },
+ .str = .{ 0xd1, 0xd5, 0x0c },
+ .num = .{ 0xe0, 0x79, 0x95 },
+ .comment = .{ 0xf6, 0xb9, 0x5b },
.lineno = .{ 0x69, 0x64, 0x55 },
.lineno_active = .{ 0x71, 0x6c, 0x5d },
.scroll_track = .{ 0x22, 0x21, 0x19 },
- .scroll_thumb = .{ 0x8a, 0x90, 0x6b },
- .border = .{ 0x56, 0x64, 0x48 },
- .sel_bg = .{ 0x4f, 0x58, 0x3c },
+ .scroll_thumb = .{ 0x8c, 0x94, 0x67 },
+ .border = .{ 0x74, 0x87, 0x61 },
+ .sel_bg = .{ 0x50, 0x5c, 0x38 },
.sel_fg = .{ 0xfb, 0xf1, 0xc7 },
- .search_bg = .{ 0x66, 0x51, 0x28 },
+ .search_bg = .{ 0x6f, 0x54, 0x1f },
.search_fg = .{ 0xff, 0xe2, 0xa0 },
- .diagnostic_error = .{ 0xfb, 0x8a, 0x69 },
- .diagnostic_warning = .{ 0xfa, 0xbd, 0x2f },
- .diagnostic_info = .{ 0x83, 0xb5, 0xa8 },
- .diagnostic_hint = .{ 0x8e, 0xc0, 0x7c },
+ .diagnostic_error = .{ 0xff, 0x88, 0x65 },
+ .diagnostic_warning = .{ 0xff, 0xbf, 0x2a },
+ .diagnostic_info = .{ 0x7b, 0xbd, 0xac },
+ .diagnostic_hint = .{ 0x89, 0xca, 0x72 },
.palette = .{
.{ 0x10, 0x10, 0x0e },
.{ 0xfb, 0x8a, 0x69 },
diff --git a/src/themes/dark.zig b/src/themes/dark.zig
index 5b39b1c7..82396397 100644
--- a/src/themes/dark.zig
+++ b/src/themes/dark.zig
@@ -13,15 +13,15 @@ pub const theme = .{
.name = "dark",
.bg = null,
.fg = null,
- .sel_bg = .{ 0x4a, 0x3a, 0x1e },
+ .sel_bg = .{ 0x51, 0x3c, 0x17 },
.sel_fg = .{ 0xd8, 0xc8, 0xa8 },
.tag_bg = .{ 0x2c, 0x2a, 0x3e },
.tag_fg = .{ 0x96, 0x96, 0x96 },
- .box = .{ 0x6e, 0x4f, 0xd0 },
- .box_dim = .{ 0x38, 0x3c, 0x54 },
- .kw = .{ 0xcc, 0x70, 0xd8 },
- .str = .{ 0x8a, 0xb8, 0x7a },
- .num = .{ 0xd0, 0xc0, 0x70 },
+ .box = .{ 0x66, 0x3f, 0xe0 },
+ .box_dim = .{ 0x35, 0x3a, 0x58 },
+ .kw = .{ 0xda, 0x5e, 0xea },
+ .str = .{ 0x85, 0xc3, 0x6f },
+ .num = .{ 0xe1, 0xcb, 0x5f },
.comment = .{ 0xf0, 0xf0, 0xf0 },
.lineno = .{ 0x5a, 0x5a, 0x5a },
.scroll_track = .{ 0x2a, 0x2a, 0x2a },
diff --git a/src/themes/daybreak.zig b/src/themes/daybreak.zig
index c55069a9..80f4883c 100644
--- a/src/themes/daybreak.zig
+++ b/src/themes/daybreak.zig
@@ -11,27 +11,27 @@ pub const theme = .{
.tag_active_name_fg = .{ 0x52, 0x25, 0x5f },
.tag_active_bg = .{ 0xdf, 0xea, 0xf3 },
.tag_active_fg = .{ 0x21, 0x3e, 0x56 },
- .box = .{ 0x07, 0x5f, 0x91 },
- .box_dim = .{ 0x81, 0x9d, 0xb1 },
- .column_box = .{ 0x76, 0x42, 0x09 },
- .column_box_dim = .{ 0xb1, 0x9a, 0x80 },
- .kw = .{ 0x62, 0x2a, 0x88 },
- .str = .{ 0x24, 0x5b, 0x30 },
- .num = .{ 0x76, 0x42, 0x09 },
- .comment = .{ 0x14, 0x56, 0x38 },
+ .box = .{ 0x00, 0x61, 0x98 },
+ .box_dim = .{ 0x7b, 0x9e, 0xb7 },
+ .column_box = .{ 0x7f, 0x42, 0x00 },
+ .column_box_dim = .{ 0xb7, 0x9a, 0x7a },
+ .kw = .{ 0x65, 0x1a, 0x98 },
+ .str = .{ 0x1a, 0x65, 0x2b },
+ .num = .{ 0x7f, 0x42, 0x00 },
+ .comment = .{ 0x08, 0x62, 0x39 },
.lineno = .{ 0x88, 0x93, 0x9b },
.lineno_active = .{ 0x80, 0x8b, 0x93 },
.scroll_track = .{ 0xe6, 0xed, 0xf3 },
- .scroll_thumb = .{ 0x6b, 0x85, 0x9b },
- .border = .{ 0x85, 0x9b, 0xac },
- .sel_bg = .{ 0xc1, 0xde, 0xf3 },
+ .scroll_thumb = .{ 0x66, 0x85, 0xa0 },
+ .border = .{ 0xc5, 0xcf, 0xd7 },
+ .sel_bg = .{ 0xb9, 0xdf, 0xfb },
.sel_fg = .{ 0x10, 0x36, 0x4f },
.search_bg = .{ 0xff, 0xe3, 0x96 },
.search_fg = .{ 0x4b, 0x35, 0x00 },
- .diagnostic_error = .{ 0xa1, 0x1c, 0x30 },
+ .diagnostic_error = .{ 0xb3, 0x08, 0x22 },
.diagnostic_warning = .{ 0x70, 0x49, 0x00 },
- .diagnostic_info = .{ 0x07, 0x5a, 0x83 },
- .diagnostic_hint = .{ 0x38, 0x55, 0x2b },
+ .diagnostic_info = .{ 0x00, 0x5c, 0x8a },
+ .diagnostic_hint = .{ 0x36, 0x5b, 0x25 },
.palette = .{
.{ 0x14, 0x2d, 0x40 },
.{ 0xa1, 0x1c, 0x30 },
diff --git a/src/themes/dusk.zig b/src/themes/dusk.zig
index 32948d02..b9377992 100644
--- a/src/themes/dusk.zig
+++ b/src/themes/dusk.zig
@@ -11,27 +11,27 @@ pub const theme = .{
.tag_active_name_fg = .{ 0xa1, 0xd5, 0xc4 },
.tag_active_bg = .{ 0x3b, 0x2f, 0x3b },
.tag_active_fg = .{ 0xd2, 0xc2, 0xd1 },
- .box = .{ 0xe4, 0xb3, 0x9b },
- .box_dim = .{ 0x71, 0x57, 0x68 },
- .column_box = .{ 0xba, 0xc8, 0x94 },
- .column_box_dim = .{ 0x73, 0x77, 0x62 },
- .kw = .{ 0xcf, 0xb1, 0xe8 },
- .str = .{ 0xba, 0xc8, 0x94 },
- .num = .{ 0xe8, 0xb5, 0x92 },
- .comment = .{ 0xf1, 0xb9, 0x89 },
+ .box = .{ 0xed, 0xb0, 0x92 },
+ .box_dim = .{ 0x74, 0x54, 0x69 },
+ .column_box = .{ 0xbd, 0xce, 0x8e },
+ .column_box_dim = .{ 0x75, 0x7a, 0x5f },
+ .kw = .{ 0xd0, 0xa7, 0xf2 },
+ .str = .{ 0xbe, 0xd1, 0x8b },
+ .num = .{ 0xf7, 0xb2, 0x83 },
+ .comment = .{ 0xff, 0xb8, 0x7b },
.lineno = .{ 0x82, 0x71, 0x7e },
.lineno_active = .{ 0x8a, 0x79, 0x86 },
.scroll_track = .{ 0x3d, 0x35, 0x40 },
- .scroll_thumb = .{ 0x8b, 0x69, 0x7c },
- .border = .{ 0x51, 0x41, 0x4f },
- .sel_bg = .{ 0x61, 0x44, 0x53 },
+ .scroll_thumb = .{ 0x8e, 0x66, 0x7c },
+ .border = .{ 0x6d, 0x58, 0x6b },
+ .sel_bg = .{ 0x65, 0x40, 0x53 },
.sel_fg = .{ 0xff, 0xf0, 0xeb },
- .search_bg = .{ 0x5e, 0x49, 0x2e },
+ .search_bg = .{ 0x65, 0x4a, 0x27 },
.search_fg = .{ 0xff, 0xe3, 0xa2 },
- .diagnostic_error = .{ 0xf6, 0xaa, 0xa4 },
- .diagnostic_warning = .{ 0xed, 0xca, 0x8a },
- .diagnostic_info = .{ 0xaa, 0xcb, 0xdc },
- .diagnostic_hint = .{ 0xc2, 0xcd, 0xa1 },
+ .diagnostic_error = .{ 0xff, 0xa2, 0x9b },
+ .diagnostic_warning = .{ 0xfc, 0xce, 0x7b },
+ .diagnostic_info = .{ 0xa2, 0xcd, 0xe4 },
+ .diagnostic_hint = .{ 0xc5, 0xd4, 0x9a },
.palette = .{
.{ 0x39, 0x32, 0x3b },
.{ 0xf6, 0xaa, 0xa4 },
diff --git a/src/themes/forge.zig b/src/themes/forge.zig
index 7f78f2ca..a27f66fb 100644
--- a/src/themes/forge.zig
+++ b/src/themes/forge.zig
@@ -12,27 +12,27 @@ pub const theme = .{
.tag_active_name_fg = .{ 0xf8, 0xcb, 0xa7 },
.tag_active_bg = .{ 0x2d, 0x34, 0x40 },
.tag_active_fg = .{ 0xd1, 0xd5, 0xde },
- .box = .{ 0xa5, 0xc4, 0xee },
- .box_dim = .{ 0x59, 0x65, 0x77 },
- .column_box = .{ 0xb5, 0xce, 0xa8 },
- .column_box_dim = .{ 0x6b, 0x79, 0x69 },
- .kw = .{ 0x75, 0xb5, 0xe8 },
- .str = .{ 0xce, 0x91, 0x78 },
- .num = .{ 0xb5, 0xce, 0xa8 },
+ .box = .{ 0x9c, 0xc3, 0xf7 },
+ .box_dim = .{ 0x55, 0x64, 0x7b },
+ .column_box = .{ 0xb4, 0xd3, 0xa3 },
+ .column_box_dim = .{ 0x6a, 0x7b, 0x67 },
+ .kw = .{ 0x61, 0xb7, 0xfc },
+ .str = .{ 0xdd, 0x8b, 0x69 },
+ .num = .{ 0xb3, 0xd5, 0xa1 },
.comment = .{ 0xb3, 0xca, 0xff },
.lineno = .{ 0x62, 0x65, 0x6c },
.lineno_active = .{ 0x6a, 0x6d, 0x74 },
.scroll_track = .{ 0x1a, 0x1c, 0x21 },
- .scroll_thumb = .{ 0x73, 0x80, 0x92 },
- .border = .{ 0x47, 0x52, 0x63 },
- .sel_bg = .{ 0x26, 0x4f, 0x68 },
+ .scroll_thumb = .{ 0x70, 0x80, 0x95 },
+ .border = .{ 0x60, 0x6f, 0x86 },
+ .sel_bg = .{ 0x1c, 0x51, 0x72 },
.sel_fg = .{ 0xe2, 0xee, 0xf4 },
- .search_bg = .{ 0x59, 0x4a, 0x25 },
+ .search_bg = .{ 0x61, 0x4d, 0x1d },
.search_fg = .{ 0xf5, 0xdf, 0xa1 },
- .diagnostic_error = .{ 0xf2, 0x8a, 0x81 },
- .diagnostic_warning = .{ 0xd7, 0xba, 0x7d },
+ .diagnostic_error = .{ 0xff, 0x7f, 0x74 },
+ .diagnostic_warning = .{ 0xe4, 0xbf, 0x70 },
.diagnostic_info = .{ 0x75, 0xbe, 0xff },
- .diagnostic_hint = .{ 0x8b, 0xce, 0xba },
+ .diagnostic_hint = .{ 0x81, 0xd8, 0xbe },
// ANSI roles are semantic colors, independent of peach string syntax.
.palette = .{
.{ 0x0c, 0x0c, 0x0e },
diff --git a/src/themes/forge_black.zig b/src/themes/forge_black.zig
index d9835110..85f6132e 100644
--- a/src/themes/forge_black.zig
+++ b/src/themes/forge_black.zig
@@ -12,27 +12,27 @@ pub const theme = .{
.tag_active_name_fg = .{ 0xf8, 0xcb, 0xa7 },
.tag_active_bg = .{ 0x2d, 0x34, 0x40 },
.tag_active_fg = .{ 0xd1, 0xd5, 0xde },
- .box = .{ 0xa5, 0xc4, 0xee },
- .box_dim = .{ 0x59, 0x65, 0x77 },
- .column_box = .{ 0xb5, 0xce, 0xa8 },
- .column_box_dim = .{ 0x6b, 0x79, 0x69 },
- .kw = .{ 0x75, 0xb5, 0xe8 },
- .str = .{ 0xce, 0x91, 0x78 },
- .num = .{ 0xb5, 0xce, 0xa8 },
+ .box = .{ 0x9c, 0xc3, 0xf7 },
+ .box_dim = .{ 0x55, 0x64, 0x7b },
+ .column_box = .{ 0xb4, 0xd3, 0xa3 },
+ .column_box_dim = .{ 0x6a, 0x7b, 0x67 },
+ .kw = .{ 0x61, 0xb7, 0xfc },
+ .str = .{ 0xdd, 0x8b, 0x69 },
+ .num = .{ 0xb3, 0xd5, 0xa1 },
.comment = .{ 0xb3, 0xca, 0xff },
.lineno = .{ 0x62, 0x65, 0x6c },
.lineno_active = .{ 0x6a, 0x6d, 0x74 },
.scroll_track = .{ 0x14, 0x16, 0x1a },
- .scroll_thumb = .{ 0x73, 0x80, 0x92 },
- .border = .{ 0x47, 0x52, 0x63 },
- .sel_bg = .{ 0x26, 0x4f, 0x68 },
+ .scroll_thumb = .{ 0x70, 0x80, 0x95 },
+ .border = .{ 0x60, 0x6f, 0x86 },
+ .sel_bg = .{ 0x1c, 0x51, 0x72 },
.sel_fg = .{ 0xe2, 0xee, 0xf4 },
- .search_bg = .{ 0x59, 0x4a, 0x25 },
+ .search_bg = .{ 0x61, 0x4d, 0x1d },
.search_fg = .{ 0xf5, 0xdf, 0xa1 },
- .diagnostic_error = .{ 0xf2, 0x8a, 0x81 },
- .diagnostic_warning = .{ 0xd7, 0xba, 0x7d },
+ .diagnostic_error = .{ 0xff, 0x7f, 0x74 },
+ .diagnostic_warning = .{ 0xe4, 0xbf, 0x70 },
.diagnostic_info = .{ 0x75, 0xbe, 0xff },
- .diagnostic_hint = .{ 0x8b, 0xce, 0xba },
+ .diagnostic_hint = .{ 0x81, 0xd8, 0xbe },
// ANSI roles are semantic colors, independent of peach string syntax.
.palette = .{
.{ 0x00, 0x00, 0x00 },
diff --git a/src/themes/forge_soft.zig b/src/themes/forge_soft.zig
index 896bd9e8..0ca062a4 100644
--- a/src/themes/forge_soft.zig
+++ b/src/themes/forge_soft.zig
@@ -12,27 +12,27 @@ pub const theme = .{
.tag_active_name_fg = .{ 0xf8, 0xcb, 0xa7 },
.tag_active_bg = .{ 0x37, 0x3e, 0x49 },
.tag_active_fg = .{ 0xd1, 0xd5, 0xde },
- .box = .{ 0xa5, 0xc4, 0xee },
- .box_dim = .{ 0x59, 0x65, 0x77 },
- .column_box = .{ 0xb5, 0xce, 0xa8 },
- .column_box_dim = .{ 0x6f, 0x7d, 0x6e },
- .kw = .{ 0x75, 0xb5, 0xe8 },
- .str = .{ 0xda, 0xa5, 0x8e },
- .num = .{ 0xb5, 0xce, 0xa8 },
+ .box = .{ 0x9c, 0xc3, 0xf7 },
+ .box_dim = .{ 0x55, 0x64, 0x7b },
+ .column_box = .{ 0xb4, 0xd3, 0xa3 },
+ .column_box_dim = .{ 0x6d, 0x7f, 0x6c },
+ .kw = .{ 0x61, 0xb7, 0xfc },
+ .str = .{ 0xe7, 0xa0, 0x81 },
+ .num = .{ 0xb3, 0xd5, 0xa1 },
.comment = .{ 0xb3, 0xca, 0xff },
.lineno = .{ 0x86, 0x89, 0x91 },
.lineno_active = .{ 0x8e, 0x91, 0x99 },
.scroll_track = .{ 0x3d, 0x40, 0x47 },
- .scroll_thumb = .{ 0x73, 0x80, 0x92 },
- .border = .{ 0x47, 0x52, 0x63 },
- .sel_bg = .{ 0x26, 0x4f, 0x68 },
+ .scroll_thumb = .{ 0x70, 0x80, 0x95 },
+ .border = .{ 0x60, 0x6f, 0x86 },
+ .sel_bg = .{ 0x1c, 0x51, 0x72 },
.sel_fg = .{ 0xe2, 0xee, 0xf4 },
- .search_bg = .{ 0x59, 0x4a, 0x25 },
+ .search_bg = .{ 0x61, 0x4d, 0x1d },
.search_fg = .{ 0xf5, 0xdf, 0xa1 },
- .diagnostic_error = .{ 0xf2, 0x8a, 0x81 },
- .diagnostic_warning = .{ 0xd7, 0xba, 0x7d },
+ .diagnostic_error = .{ 0xff, 0x7f, 0x74 },
+ .diagnostic_warning = .{ 0xe4, 0xbf, 0x70 },
.diagnostic_info = .{ 0x75, 0xbe, 0xff },
- .diagnostic_hint = .{ 0x8b, 0xce, 0xba },
+ .diagnostic_hint = .{ 0x81, 0xd8, 0xbe },
// ANSI roles are semantic colors, independent of peach string syntax.
.palette = .{
.{ 0x35, 0x38, 0x3e },
diff --git a/src/themes/harvest.zig b/src/themes/harvest.zig
index bf367a24..cf10a779 100644
--- a/src/themes/harvest.zig
+++ b/src/themes/harvest.zig
@@ -12,27 +12,27 @@ pub const theme = .{
.tag_active_name_fg = .{ 0xc5, 0xe7, 0xf7 },
.tag_active_bg = .{ 0x42, 0x4d, 0x38 },
.tag_active_fg = .{ 0xe1, 0xe4, 0xc4 },
- .box = .{ 0xcf, 0xba, 0x8b },
- .box_dim = .{ 0x77, 0x79, 0x53 },
- .column_box = .{ 0xf0, 0x7d, 0x63 },
- .column_box_dim = .{ 0x92, 0x5c, 0x48 },
- .kw = .{ 0xf0, 0x7d, 0x63 },
- .str = .{ 0x99, 0xbe, 0x70 },
- .num = .{ 0xfa, 0xd5, 0x66 },
- .comment = .{ 0xef, 0xc7, 0x8c },
+ .box = .{ 0xd8, 0xbd, 0x83 },
+ .box_dim = .{ 0x7b, 0x7e, 0x4e },
+ .column_box = .{ 0xff, 0x74, 0x54 },
+ .column_box_dim = .{ 0x9b, 0x58, 0x3f },
+ .kw = .{ 0xff, 0x74, 0x54 },
+ .str = .{ 0x9a, 0xcc, 0x62 },
+ .num = .{ 0xff, 0xd8, 0x61 },
+ .comment = .{ 0xff, 0xca, 0x7c },
.lineno = .{ 0x6a, 0x64, 0x56 },
.lineno_active = .{ 0x72, 0x6c, 0x5e },
.scroll_track = .{ 0x20, 0x1d, 0x17 },
- .scroll_thumb = .{ 0x85, 0x8a, 0x62 },
- .border = .{ 0x59, 0x65, 0x47 },
- .sel_bg = .{ 0x4d, 0x54, 0x38 },
+ .scroll_thumb = .{ 0x88, 0x8e, 0x5e },
+ .border = .{ 0x78, 0x88, 0x60 },
+ .sel_bg = .{ 0x4f, 0x58, 0x34 },
.sel_fg = .{ 0xf3, 0xf2, 0xcc },
- .search_bg = .{ 0x66, 0x45, 0x2d },
+ .search_bg = .{ 0x6f, 0x44, 0x24 },
.search_fg = .{ 0xff, 0xe2, 0xac },
- .diagnostic_error = .{ 0xf5, 0x8a, 0x73 },
- .diagnostic_warning = .{ 0xfa, 0xd5, 0x66 },
- .diagnostic_info = .{ 0x86, 0xc1, 0xb9 },
- .diagnostic_hint = .{ 0xb5, 0xc6, 0x8c },
+ .diagnostic_error = .{ 0xff, 0x84, 0x69 },
+ .diagnostic_warning = .{ 0xff, 0xd8, 0x61 },
+ .diagnostic_info = .{ 0x7d, 0xca, 0xbf },
+ .diagnostic_hint = .{ 0xb9, 0xcf, 0x83 },
.palette = .{
.{ 0x10, 0x0e, 0x0b },
.{ 0xf5, 0x8a, 0x73 },
diff --git a/src/themes/helix.zig b/src/themes/helix.zig
index 3c2934f7..76eaeab2 100644
--- a/src/themes/helix.zig
+++ b/src/themes/helix.zig
@@ -45,9 +45,9 @@ pub const theme = .{
.tag_fg = .{ 0x94, 0x94, 0x94 },
.box = .{ 0x62, 0x62, 0x62 },
.box_dim = .{ 0x26, 0x26, 0x26 },
- .kw = .{ 0xcf, 0x87, 0xe8 },
- .str = .{ 0xe3, 0xc7, 0x8a },
- .num = .{ 0xe3, 0xc7, 0x8a },
+ .kw = .{ 0xd7, 0x76, 0xf9 },
+ .str = .{ 0xf3, 0xcd, 0x7a },
+ .num = .{ 0xf3, 0xcd, 0x7a },
.comment = .{ 0xff, 0xff, 0xff },
.lineno = .{ 0x62, 0x62, 0x62 },
.scroll_track = .{ 0x12, 0x12, 0x12 },
diff --git a/src/themes/ink.zig b/src/themes/ink.zig
index 3b92a635..1e254a3c 100644
--- a/src/themes/ink.zig
+++ b/src/themes/ink.zig
@@ -12,26 +12,26 @@ pub const theme = .{
.tag_active_bg = .{ 0x22, 0x33, 0x42 },
.tag_active_fg = .{ 0xe1, 0xee, 0xf9 },
.box = .{ 0x86, 0xd8, 0xff },
- .box_dim = .{ 0x64, 0x79, 0x8b },
+ .box_dim = .{ 0x5f, 0x79, 0x90 },
.column_box = .{ 0xff, 0xd6, 0x94 },
- .column_box_dim = .{ 0x8a, 0x7b, 0x60 },
+ .column_box_dim = .{ 0x8f, 0x7d, 0x5b },
.kw = .{ 0xd5, 0xb8, 0xff },
- .str = .{ 0xb5, 0xee, 0xae },
+ .str = .{ 0xac, 0xf9, 0xa3 },
.num = .{ 0xff, 0xd6, 0x94 },
- .comment = .{ 0xa8, 0xe7, 0xf4 },
+ .comment = .{ 0x9d, 0xee, 0xff },
.lineno = .{ 0x62, 0x6b, 0x75 },
.lineno_active = .{ 0x6a, 0x73, 0x7d },
.scroll_track = .{ 0x15, 0x21, 0x2c },
- .scroll_thumb = .{ 0x8b, 0xa7, 0xbd },
- .border = .{ 0x59, 0x6f, 0x83 },
- .sel_bg = .{ 0x26, 0x52, 0x71 },
+ .scroll_thumb = .{ 0x86, 0xa8, 0xc2 },
+ .border = .{ 0x80, 0x95, 0xa9 },
+ .sel_bg = .{ 0x1b, 0x54, 0x7c },
.sel_fg = .{ 0xff, 0xff, 0xff },
- .search_bg = .{ 0x75, 0x50, 0x1b },
+ .search_bg = .{ 0x7c, 0x4e, 0x0d },
.search_fg = .{ 0xff, 0xff, 0xff },
.diagnostic_error = .{ 0xff, 0xb1, 0xb1 },
.diagnostic_warning = .{ 0xff, 0xe2, 0x9c },
.diagnostic_info = .{ 0x9b, 0xde, 0xff },
- .diagnostic_hint = .{ 0xc6, 0xe4, 0xb5 },
+ .diagnostic_hint = .{ 0xc4, 0xeb, 0xae },
.palette = .{
.{ 0x00, 0x00, 0x00 },
.{ 0xff, 0xb1, 0xb1 },
diff --git a/src/themes/lagoon.zig b/src/themes/lagoon.zig
index 516639b0..4d975794 100644
--- a/src/themes/lagoon.zig
+++ b/src/themes/lagoon.zig
@@ -12,27 +12,27 @@ pub const theme = .{
.tag_active_name_fg = .{ 0xf0, 0xce, 0xf8 },
.tag_active_bg = .{ 0x34, 0x57, 0x4d },
.tag_active_fg = .{ 0xc7, 0xe3, 0xd7 },
- .box = .{ 0x89, 0xc7, 0xb4 },
- .box_dim = .{ 0x59, 0x7e, 0x71 },
+ .box = .{ 0x81, 0xcf, 0xb7 },
+ .box_dim = .{ 0x54, 0x83, 0x72 },
.column_box = .{ 0xff, 0xad, 0x91 },
- .column_box_dim = .{ 0x94, 0x78, 0x68 },
- .kw = .{ 0xd5, 0xa8, 0xed },
- .str = .{ 0xc3, 0xe8, 0x8d },
+ .column_box_dim = .{ 0x9a, 0x76, 0x62 },
+ .kw = .{ 0xd9, 0x9c, 0xf9 },
+ .str = .{ 0xc6, 0xf8, 0x7d },
.num = .{ 0xff, 0xad, 0x91 },
- .comment = .{ 0x92, 0xe1, 0xd1 },
+ .comment = .{ 0x84, 0xef, 0xd9 },
.lineno = .{ 0x7b, 0x91, 0x94 },
.lineno_active = .{ 0x83, 0x99, 0x9c },
.scroll_track = .{ 0x3a, 0x4c, 0x50 },
- .scroll_thumb = .{ 0x72, 0x97, 0x8a },
- .border = .{ 0x50, 0x74, 0x68 },
- .sel_bg = .{ 0x39, 0x5b, 0x65 },
+ .scroll_thumb = .{ 0x6e, 0x9b, 0x8b },
+ .border = .{ 0x6e, 0x9b, 0x8c },
+ .sel_bg = .{ 0x32, 0x5f, 0x6c },
.sel_fg = .{ 0xe6, 0xf2, 0xf3 },
- .search_bg = .{ 0x60, 0x53, 0x27 },
+ .search_bg = .{ 0x69, 0x58, 0x1e },
.search_fg = .{ 0xff, 0xf0, 0xb9 },
.diagnostic_error = .{ 0xff, 0x9d, 0xa1 },
.diagnostic_warning = .{ 0xff, 0xcb, 0x6b },
- .diagnostic_info = .{ 0x82, 0xc9, 0xfa },
- .diagnostic_hint = .{ 0x89, 0xdd, 0xcc },
+ .diagnostic_info = .{ 0x7d, 0xca, 0xff },
+ .diagnostic_hint = .{ 0x7c, 0xea, 0xd4 },
.palette = .{
.{ 0x34, 0x46, 0x4c },
.{ 0xff, 0x9d, 0xa1 },
diff --git a/src/themes/orchard.zig b/src/themes/orchard.zig
index 888a5b27..f927c50e 100644
--- a/src/themes/orchard.zig
+++ b/src/themes/orchard.zig
@@ -11,27 +11,27 @@ pub const theme = .{
.tag_active_name_fg = .{ 0xdf, 0xbc, 0xe5 },
.tag_active_bg = .{ 0x26, 0x36, 0x2d },
.tag_active_fg = .{ 0xbe, 0xcf, 0xc4 },
- .box = .{ 0xad, 0xcc, 0x91 },
- .box_dim = .{ 0x4c, 0x63, 0x56 },
- .column_box = .{ 0xe9, 0xbf, 0x91 },
- .column_box_dim = .{ 0x83, 0x73, 0x5a },
- .kw = .{ 0xc9, 0xb0, 0xe4 },
- .str = .{ 0xb6, 0xce, 0x94 },
- .num = .{ 0xe9, 0xbf, 0x91 },
- .comment = .{ 0xb9, 0xe5, 0x8f },
+ .box = .{ 0xad, 0xd3, 0x8a },
+ .box_dim = .{ 0x49, 0x66, 0x56 },
+ .column_box = .{ 0xf4, 0xc0, 0x86 },
+ .column_box_dim = .{ 0x88, 0x74, 0x55 },
+ .kw = .{ 0xc9, 0xa7, 0xed },
+ .str = .{ 0xb8, 0xd8, 0x8a },
+ .num = .{ 0xf8, 0xc0, 0x82 },
+ .comment = .{ 0xb9, 0xf4, 0x80 },
.lineno = .{ 0x59, 0x67, 0x5c },
.lineno_active = .{ 0x61, 0x6f, 0x64 },
.scroll_track = .{ 0x15, 0x20, 0x19 },
- .scroll_thumb = .{ 0x63, 0x7f, 0x6a },
- .border = .{ 0x35, 0x48, 0x3d },
- .sel_bg = .{ 0x40, 0x5d, 0x4b },
+ .scroll_thumb = .{ 0x60, 0x82, 0x69 },
+ .border = .{ 0x48, 0x61, 0x52 },
+ .sel_bg = .{ 0x3c, 0x61, 0x4a },
.sel_fg = .{ 0xf5, 0xf7, 0xee },
- .search_bg = .{ 0x57, 0x47, 0x2a },
+ .search_bg = .{ 0x5e, 0x49, 0x23 },
.search_fg = .{ 0xf7, 0xe4, 0xa5 },
- .diagnostic_error = .{ 0xf1, 0xa3, 0x9b },
- .diagnostic_warning = .{ 0xe9, 0xc6, 0x80 },
- .diagnostic_info = .{ 0x9b, 0xcb, 0xd5 },
- .diagnostic_hint = .{ 0xb6, 0xc5, 0x9e },
+ .diagnostic_error = .{ 0xfe, 0x98, 0x8e },
+ .diagnostic_warning = .{ 0xf9, 0xcb, 0x70 },
+ .diagnostic_info = .{ 0x92, 0xd1, 0xde },
+ .diagnostic_hint = .{ 0xb7, 0xcb, 0x98 },
.palette = .{
.{ 0x0d, 0x14, 0x10 },
.{ 0xf1, 0xa3, 0x9b },
diff --git a/src/themes/orchard_black.zig b/src/themes/orchard_black.zig
index be960b93..c700947f 100644
--- a/src/themes/orchard_black.zig
+++ b/src/themes/orchard_black.zig
@@ -11,27 +11,27 @@ pub const theme = .{
.tag_active_name_fg = .{ 0xdf, 0xbc, 0xe5 },
.tag_active_bg = .{ 0x26, 0x36, 0x2d },
.tag_active_fg = .{ 0xbe, 0xcf, 0xc4 },
- .box = .{ 0xad, 0xcc, 0x91 },
- .box_dim = .{ 0x4c, 0x63, 0x56 },
- .column_box = .{ 0xe9, 0xbf, 0x91 },
- .column_box_dim = .{ 0x83, 0x73, 0x5a },
- .kw = .{ 0xc9, 0xb0, 0xe4 },
- .str = .{ 0xb6, 0xce, 0x94 },
- .num = .{ 0xe9, 0xbf, 0x91 },
- .comment = .{ 0xb9, 0xe5, 0x8f },
+ .box = .{ 0xad, 0xd3, 0x8a },
+ .box_dim = .{ 0x49, 0x66, 0x56 },
+ .column_box = .{ 0xf4, 0xc0, 0x86 },
+ .column_box_dim = .{ 0x88, 0x74, 0x55 },
+ .kw = .{ 0xc9, 0xa7, 0xed },
+ .str = .{ 0xb8, 0xd8, 0x8a },
+ .num = .{ 0xf8, 0xc0, 0x82 },
+ .comment = .{ 0xb9, 0xf4, 0x80 },
.lineno = .{ 0x59, 0x67, 0x5c },
.lineno_active = .{ 0x61, 0x6f, 0x64 },
.scroll_track = .{ 0x11, 0x18, 0x13 },
- .scroll_thumb = .{ 0x63, 0x7f, 0x6a },
- .border = .{ 0x35, 0x48, 0x3d },
- .sel_bg = .{ 0x40, 0x5d, 0x4b },
+ .scroll_thumb = .{ 0x60, 0x82, 0x69 },
+ .border = .{ 0x48, 0x61, 0x52 },
+ .sel_bg = .{ 0x3c, 0x61, 0x4a },
.sel_fg = .{ 0xf5, 0xf7, 0xee },
- .search_bg = .{ 0x57, 0x47, 0x2a },
+ .search_bg = .{ 0x5e, 0x49, 0x23 },
.search_fg = .{ 0xf7, 0xe4, 0xa5 },
- .diagnostic_error = .{ 0xf1, 0xa3, 0x9b },
- .diagnostic_warning = .{ 0xe9, 0xc6, 0x80 },
- .diagnostic_info = .{ 0x9b, 0xcb, 0xd5 },
- .diagnostic_hint = .{ 0xb6, 0xc5, 0x9e },
+ .diagnostic_error = .{ 0xfe, 0x98, 0x8e },
+ .diagnostic_warning = .{ 0xf9, 0xcb, 0x70 },
+ .diagnostic_info = .{ 0x92, 0xd1, 0xde },
+ .diagnostic_hint = .{ 0xb7, 0xcb, 0x98 },
.palette = .{
.{ 0x00, 0x00, 0x00 },
.{ 0xf1, 0xa3, 0x9b },
diff --git a/src/themes/paper.zig b/src/themes/paper.zig
index b5d24835..f1d6980b 100644
--- a/src/themes/paper.zig
+++ b/src/themes/paper.zig
@@ -11,27 +11,27 @@ pub const theme = .{
.tag_active_name_fg = .{ 0x69, 0x44, 0x7a },
.tag_active_bg = .{ 0xdc, 0xe5, 0xdc },
.tag_active_fg = .{ 0x4a, 0x5a, 0x52 },
- .box = .{ 0x3c, 0x6b, 0x56 },
- .box_dim = .{ 0x91, 0xa3, 0x95 },
- .column_box = .{ 0x87, 0x58, 0x26 },
- .column_box_dim = .{ 0xb7, 0xa1, 0x84 },
- .kw = .{ 0x73, 0x51, 0x90 },
- .str = .{ 0x41, 0x68, 0x48 },
- .num = .{ 0x87, 0x58, 0x26 },
- .comment = .{ 0x2e, 0x71, 0x59 },
+ .box = .{ 0x36, 0x71, 0x57 },
+ .box_dim = .{ 0x8f, 0xa5, 0x94 },
+ .column_box = .{ 0x93, 0x58, 0x1a },
+ .column_box_dim = .{ 0xbd, 0xa2, 0x7e },
+ .kw = .{ 0x74, 0x46, 0x9b },
+ .str = .{ 0x3a, 0x6f, 0x44 },
+ .num = .{ 0x98, 0x59, 0x15 },
+ .comment = .{ 0x20, 0x75, 0x56 },
.lineno = .{ 0x8b, 0x91, 0x85 },
.lineno_active = .{ 0x83, 0x89, 0x7d },
.scroll_track = .{ 0xe5, 0xe8, 0xdf },
- .scroll_thumb = .{ 0x88, 0x9b, 0x8b },
- .border = .{ 0xb5, 0xc1, 0xb3 },
- .sel_bg = .{ 0xc8, 0xdf, 0xd1 },
+ .scroll_thumb = .{ 0x86, 0x9d, 0x8a },
+ .border = .{ 0xfb, 0xfb, 0xfb },
+ .sel_bg = .{ 0xc5, 0xe2, 0xd0 },
.sel_fg = .{ 0x20, 0x3e, 0x31 },
- .search_bg = .{ 0xf0, 0xdd, 0xa8 },
+ .search_bg = .{ 0xfb, 0xe2, 0x9d },
.search_fg = .{ 0x61, 0x46, 0x1c },
- .diagnostic_error = .{ 0xa3, 0x39, 0x38 },
- .diagnostic_warning = .{ 0x80, 0x57, 0x0c },
- .diagnostic_info = .{ 0x30, 0x65, 0x75 },
- .diagnostic_hint = .{ 0x52, 0x6d, 0x3e },
+ .diagnostic_error = .{ 0xb3, 0x29, 0x28 },
+ .diagnostic_warning = .{ 0x8c, 0x5b, 0x00 },
+ .diagnostic_info = .{ 0x26, 0x6b, 0x7f },
+ .diagnostic_hint = .{ 0x51, 0x74, 0x37 },
.palette = .{
.{ 0x30, 0x3b, 0x3b },
.{ 0xa3, 0x39, 0x38 },
diff --git a/src/themes/solarium.zig b/src/themes/solarium.zig
index fe2525a6..21b4c451 100644
--- a/src/themes/solarium.zig
+++ b/src/themes/solarium.zig
@@ -12,27 +12,27 @@ pub const theme = .{
.tag_active_name_fg = .{ 0xf5, 0xcd, 0xec },
.tag_active_bg = .{ 0x21, 0x4e, 0x50 },
.tag_active_fg = .{ 0xd3, 0xe0, 0xc9 },
- .box = .{ 0xd5, 0xbc, 0x72 },
- .box_dim = .{ 0x4c, 0x79, 0x72 },
- .column_box = .{ 0x6a, 0xae, 0xdd },
- .column_box_dim = .{ 0x3e, 0x76, 0x90 },
- .kw = .{ 0x6a, 0xae, 0xdd },
- .str = .{ 0xaf, 0xbd, 0x52 },
- .num = .{ 0xd9, 0x9a, 0x5e },
- .comment = .{ 0xe2, 0xc4, 0x6e },
+ .box = .{ 0xe1, 0xc2, 0x66 },
+ .box_dim = .{ 0x46, 0x7f, 0x76 },
+ .column_box = .{ 0x5c, 0xb1, 0xeb },
+ .column_box_dim = .{ 0x34, 0x7a, 0x9a },
+ .kw = .{ 0x56, 0xb2, 0xf1 },
+ .str = .{ 0xbd, 0xd0, 0x3f },
+ .num = .{ 0xef, 0x99, 0x48 },
+ .comment = .{ 0xf6, 0xce, 0x5a },
.lineno = .{ 0x64, 0x7e, 0x7c },
.lineno_active = .{ 0x6c, 0x86, 0x84 },
.scroll_track = .{ 0x1c, 0x45, 0x4c },
- .scroll_thumb = .{ 0x69, 0x93, 0x8a },
- .border = .{ 0x38, 0x65, 0x5f },
- .sel_bg = .{ 0x27, 0x56, 0x5c },
+ .scroll_thumb = .{ 0x65, 0x97, 0x8c },
+ .border = .{ 0x4c, 0x88, 0x80 },
+ .sel_bg = .{ 0x1f, 0x5c, 0x64 },
.sel_fg = .{ 0xee, 0xe8, 0xd5 },
- .search_bg = .{ 0x64, 0x53, 0x26 },
+ .search_bg = .{ 0x6d, 0x57, 0x1d },
.search_fg = .{ 0xff, 0xf0, 0xb9 },
- .diagnostic_error = .{ 0xed, 0x84, 0x77 },
- .diagnostic_warning = .{ 0xdf, 0xb6, 0x5b },
- .diagnostic_info = .{ 0x6a, 0xae, 0xdd },
- .diagnostic_hint = .{ 0x69, 0xbd, 0xb0 },
+ .diagnostic_error = .{ 0xff, 0x7a, 0x6a },
+ .diagnostic_warning = .{ 0xf3, 0xbe, 0x47 },
+ .diagnostic_info = .{ 0x59, 0xb1, 0xee },
+ .diagnostic_hint = .{ 0x5c, 0xca, 0xb9 },
.palette = .{
.{ 0x17, 0x3e, 0x45 },
.{ 0xed, 0x84, 0x77 },
diff --git a/src/themes/spectrum.zig b/src/themes/spectrum.zig
index 5a1cecc6..a0faf897 100644
--- a/src/themes/spectrum.zig
+++ b/src/themes/spectrum.zig
@@ -13,26 +13,26 @@ pub const theme = .{
.tag_active_bg = .{ 0x4d, 0x47, 0x36 },
.tag_active_fg = .{ 0xe9, 0xe4, 0xc7 },
.box = .{ 0xff, 0xd8, 0x66 },
- .box_dim = .{ 0x8b, 0x80, 0x56 },
- .column_box = .{ 0xab, 0x9d, 0xf2 },
- .column_box_dim = .{ 0x73, 0x6a, 0x91 },
+ .box_dim = .{ 0x92, 0x84, 0x4f },
+ .column_box = .{ 0xa4, 0x92, 0xfd },
+ .column_box_dim = .{ 0x70, 0x65, 0x96 },
.kw = .{ 0xff, 0x61, 0x88 },
- .str = .{ 0xa9, 0xdc, 0x76 },
- .num = .{ 0xab, 0x9d, 0xf2 },
+ .str = .{ 0xa9, 0xee, 0x64 },
+ .num = .{ 0xa2, 0x90, 0xff },
.comment = .{ 0xff, 0xe0, 0x9b },
.lineno = .{ 0x68, 0x61, 0x6b },
.lineno_active = .{ 0x70, 0x69, 0x73 },
.scroll_track = .{ 0x21, 0x1d, 0x23 },
- .scroll_thumb = .{ 0x91, 0x8b, 0x72 },
- .border = .{ 0x6a, 0x64, 0x50 },
- .sel_bg = .{ 0x51, 0x43, 0x5c },
+ .scroll_thumb = .{ 0x94, 0x8d, 0x6f },
+ .border = .{ 0x8f, 0x87, 0x6c },
+ .sel_bg = .{ 0x51, 0x3f, 0x60 },
.sel_fg = .{ 0xfc, 0xfc, 0xfa },
- .search_bg = .{ 0x66, 0x56, 0x27 },
+ .search_bg = .{ 0x6f, 0x5b, 0x1e },
.search_fg = .{ 0xff, 0xf0, 0xb0 },
.diagnostic_error = .{ 0xff, 0x79, 0x9b },
.diagnostic_warning = .{ 0xff, 0xd8, 0x66 },
- .diagnostic_info = .{ 0x78, 0xdc, 0xe8 },
- .diagnostic_hint = .{ 0xa9, 0xdc, 0x76 },
+ .diagnostic_info = .{ 0x67, 0xe9, 0xf9 },
+ .diagnostic_hint = .{ 0xa9, 0xeb, 0x67 },
.palette = .{
.{ 0x10, 0x0e, 0x11 },
.{ 0xff, 0x79, 0x9b },
diff --git a/src/web.zig b/src/web.zig
index b5a992ae..7325af54 100644
--- a/src/web.zig
+++ b/src/web.zig
@@ -376,7 +376,7 @@ export fn pardes_tag_layer_cells(index: u32) ?[*]const WebCell {
}
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, true);
}
export fn pardes_body_layer_value(index: u32, field: u32) u32 {
diff --git a/test/macos_e2e.swift b/test/macos_e2e.swift
index 5efc366c..5f172f9d 100644
--- a/test/macos_e2e.swift
+++ b/test/macos_e2e.swift
@@ -628,8 +628,8 @@ private final class Driver: PardesViewDelegate {
let bottom = kind == 2 && pardes_tag_layer_value(i, 11) != 0
for pixel in 0..<height {
var rgb = pardes_tag_layer_value(i, 10)
- if kind != 0 && pixel == (bottom ? 0 : height - 1) { rgb = 0x475263 }
- if pardes_tag_layer_value(i, 5) == UInt32(PARDES_TOPBAR_H) && pixel == 0 { rgb = 0x475263 }
+ if kind != 0 && (bottom ? pixel <= 1 : pixel >= height - 2) { rgb = 0x606f86 }
+ if pardes_tag_layer_value(i, 5) == UInt32(PARDES_TOPBAR_H) && pixel == 0 { rgb = 0x606f86 }
try check(x: x, y: top + (CGFloat(pixel) + 0.5) / scale,
rgb: rgb, name: "tag \(i), pixel \(pixel)")
}
@@ -656,7 +656,7 @@ private final class Driver: PardesViewDelegate {
guard let c = sample(rep, x: Int(x * scale), y: Int(y * scale)) else {
throw ScriptError(message: "context: no separator pixel")
}
- let rgb: UInt32 = 0x475263 // Forge border, also used by the Linux chrome overlay.
+ let rgb: UInt32 = 0x606f86 // Forge border, also used by the Linux chrome overlay.
guard abs(c.redComponent * 255 - CGFloat((rgb >> 16) & 255)) < 4,
abs(c.greenComponent * 255 - CGFloat((rgb >> 8) & 255)) < 4,
abs(c.blueComponent * 255 - CGFloat(rgb & 255)) < 4,
@@ -1270,6 +1270,10 @@ private func buildWorld(stem: String) throws -> String {
// A wall clock cannot live in a golden. Everything that would print one
// prints fixed characters instead when this is set.
_ = setenv("PARDES_NOTIME", "1", 1)
+ // The scripts speak to a grid that HAS the workspace tag row: the app
+ // hands that row to its native menu bar, but this harness is the
+ // topbar's test bench, so opt back in before pardes_init reads it.
+ _ = setenv("PARDES_WORKSPACE_TAG", "1", 1)
guard fm.changeCurrentDirectoryPath(work) else {
throw ScriptError(message: "could not chdir into \(work)")
}