summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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)")
}