summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/builtins.zig32
-rw-r--r--src/config.zig6
-rw-r--r--src/output_pane.zig54
-rw-r--r--src/pardes.zig159
-rw-r--r--src/themes/acme.zig29
-rw-r--r--src/themes/dark.zig22
-rw-r--r--src/themes/helix.zig47
7 files changed, 249 insertions, 100 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index 7e20a95b..620c6b1c 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -182,6 +182,38 @@ pub const NextColor = struct {
}
};
+/// The theme BY NAME — `Theme acme`. The ring grew past the point where
+/// cycling to the one you want is reasonable, so this is the way to ask for
+/// one, and NextColor stays as the way to browse. Inert without an argument
+/// (there is no theme called nothing), which is also why it has no leader path:
+/// a key path names a builtin and can never carry the name of a theme.
+///
+/// A LINEAR SCAN over a couple of dozen names, on a keystroke: the alternative
+/// is a comptime name->index map, which is a second copy of the ring to build
+/// for a lookup nobody will ever measure.
+pub const Theme = struct {
+ pub fn run(c: Ctx) void {
+ const want = std.mem.trim(u8, c.arg orelse return, " \t\r\n");
+ for (pardes.themes, 0..) |t, i| {
+ if (std.mem.eql(u8, t.name, want)) {
+ c.p.theme_idx = i;
+ return;
+ }
+ }
+ }
+};
+
+/// ...and the list of what Theme takes, as a buffer you walk. Its rows are
+/// `Theme <name>` COMMANDS rather than locations, so n/N execute them instead
+/// of looking them (output_pane.Traits.executes) and stepping the list wears
+/// each theme in turn — the picker is the list, and there is nothing to
+/// confirm because arriving already applied it.
+pub const ThemeSel = struct {
+ pub fn run(c: Ctx) void {
+ output_pane.openThemes(c.p, c.id);
+ }
+};
+
pub const Crt = struct {
pub fn run(c: Ctx) void {
c.p.crt_on = !c.p.crt_on;
diff --git a/src/config.zig b/src/config.zig
index c4249270..35be4b9f 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -108,6 +108,11 @@ pub const leader_path = std.EnumArray(Builtin, ?[]const u8).init(.{
.Colors = "tc",
.NextColor = "tn",
.Crt = "tr",
+ // the theme picker joins the toggles it belongs with; `Theme` itself takes
+ // a NAME, and a key path can never carry one, so it has none (the same
+ // reason Look and Exec have none)
+ .ThemeSel = "tt",
+ .Theme = null,
// the image toggles join the same `t` group; Palette takes `l` because
// `p` is Petscii's and `c` is Colors'.
.Petscii = "tp",
@@ -401,6 +406,7 @@ pub const symbol_marker = " WsSymbols /";
pub const search_buffer = "+Search";
pub const help_buffer = "+Help";
pub const jumps_buffer = "+Jumps";
+pub const themes_buffer = "+Themes";
pub const hover_buffer = "+Hover";
pub const lsp_buffer = "+Lsp";
diff --git a/src/output_pane.zig b/src/output_pane.zig
index 8e08285f..a0583506 100644
--- a/src/output_pane.zig
+++ b/src/output_pane.zig
@@ -80,6 +80,19 @@ pub const Traits = struct {
/// look path resolves, so the buffer IS helix's picker. Prose (a hover
/// blurb, a rename diff) has nowhere to step to.
steps: bool = false,
+ /// ...and what a step DOES with the row it lands on. Off, the row is a
+ /// LOCATION and its leading word is LOOKED. On, the row is a COMMAND LINE
+ /// and the whole of it is EXECUTED — so walking the list runs each row in
+ /// turn, which is what makes a picker over things that take effect
+ /// immediately (ThemeSel) a plain list of the words you would have typed.
+ /// Both go through the ordinary builtin (config.look_cmd / exec_cmd), so a
+ /// row does exactly what the matching mouse button on it would.
+ ///
+ /// Nothing about this is output-pane specific, which is why it is a column
+ /// here and not a branch in searchStep: `file_row` answers it too, so a
+ /// file pane whose lines happen to be commands is one word away from
+ /// behaving the same.
+ executes: bool = false,
/// an answer of exactly ONE row jumps straight there instead of opening
/// this buffer at all — helix: the gotos jump on a single location and
/// show a picker on several, a symbol list is always a picker.
@@ -114,6 +127,10 @@ pub fn traits(o: Origin) Traits {
// the focus history, one location per row: not a search, but the
// same kind of list, so n/N walk it and a row is a look target
.Jumplist => .{ .name = config.jumps_buffer, .steps = true },
+ // the theme ring, one `Theme <name>` per row. The only buffer whose
+ // rows are COMMANDS rather than locations, so n/N execute them:
+ // walking the list wears each theme, and stopping is picking one.
+ .ThemeSel => .{ .name = config.themes_buffer, .steps = true, .executes = true },
// Find (file names) and Grep (file contents) both list locations;
// no other builtin opens a buffer, and the day one does it lands
// here rather than in a call site.
@@ -210,7 +227,6 @@ pub fn open(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u
/// content line for anything holding text, else the pane's directory: enough
/// to recognise the place without opening it.
pub fn openJumps(p: *Pardes, id: usize) void {
- const pane = p.panes[id] orelse return;
const arena = p.scratch.allocator();
var out: std.ArrayList(u8) = .empty;
for (p.jumps[0..p.njumps]) |j| {
@@ -241,19 +257,47 @@ pub fn openJumps(p: *Pardes, id: usize) void {
std.fmt.allocPrint(arena, "{s}:{d}:{d} {s}\n", .{ loc, j.line, j.col, what[0..cut] }) catch return;
out.appendSlice(arena, row) catch return;
}
- const content = p.gpa.dupe(u8, out.items) catch return;
+ openStepped(p, id, .{ .cmd = .Jumplist }, out.items);
+}
+
+/// The ThemeSel builtin: the theme ring written out as one `Theme <name>` row
+/// per theme — the ordinary builtin with its argument, exactly the line you
+/// would type — into a buffer whose `executes` trait makes n/N RUN each row
+/// rather than look it. So walking the list is trying the themes on, and
+/// stopping on one is choosing it; there is no picker mode, no preview state
+/// and nothing to commit or cancel, because every step already did the thing.
+///
+/// The command's own name comes from the builtin rather than a literal: the
+/// word is derived from the struct in exactly one place (builtins.word), and a
+/// rename there must not leave rows here that name something that is gone.
+pub fn openThemes(p: *Pardes, id: usize) void {
+ const arena = p.scratch.allocator();
+ var out: std.ArrayList(u8) = .empty;
+ for (pardes.themes) |t| {
+ out.print(arena, comptime builtins.word(builtins.Theme) ++ " {s}\n", .{t.name}) catch return;
+ }
+ openStepped(p, id, .{ .cmd = .ThemeSel }, out.items);
+}
+
+/// Open a buffer n/N will walk, and arm them on it: the shared tail of every
+/// builtin that answers with a list. `text` is borrowed (the callers build it
+/// in the scratch arena) and copied into a gpa buffer the pane adopts.
+///
+/// Focus stays with the pane that ASKED, exactly as it does after a search:
+/// n/N are read there, and they step the buffer they just armed.
+fn openStepped(p: *Pardes, id: usize, from: Origin, text: []const u8) void {
+ const pane = p.panes[id] orelse return;
+ const content = p.gpa.dupe(u8, text) catch return;
const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
const free = p.freeSlot() orelse {
p.gpa.free(content);
return;
};
- const np = open(p, free, dir, .{ .cmd = .Jumplist }, "", content) catch {
+ const np = open(p, free, dir, from, "", content) catch {
p.gpa.free(content);
return;
};
p.placeDoc(id, free, np);
- // focus stays with the pane that asked, exactly as it does after a search:
- // n/N are read there, and they step the buffer they just armed.
p.active = id;
pane.search_pane = free;
pane.search_row = null;
diff --git a/src/pardes.zig b/src/pardes.zig
index 3bf32c82..4d6556f3 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -216,90 +216,47 @@ pub const Theme = struct {
palette: ?[16][3]u8,
};
-pub const themes = [_]Theme{
- // Copied off the helix the user actually runs: config.toml says
- // theme = "pardes", i.e. ~/.config/helix/themes/pardes.toml. Every value
- // below was then PINNED against a real `hx sample.zig` render captured
- // through a pty, not read off the TOML and hoped for -- the SGR runs were
- // 48;2;8;8;8 page, 38;2;189;189;189 text, 38;2;98;98;98 line numbers,
- // 1;38;2;207;135;232 keywords, 38;2;227;199;138 strings AND numeric
- // constants, 38;2;255;255;255 comments.
- //
- // "dark and minimal" is the half that is not a table. That theme sets no
- // ui.statusline at all, so helix paints its status row in the page's own
- // colors: in the capture, rows 8..23 carry one unbroken 48;2;8;8;8 run and
- // the statusline is told apart from the file by its words alone. pardes
- // cannot go quite that far -- its tag bar is a click target and the gutter
- // is a drag handle -- so chrome lifts by the smallest honest step instead:
- // the theme's greys are all xterm grayscale-ramp entries (232 #080808,
- // 241 #626262, 246 #949494, 250 #bdbdbd), so the chrome takes ramp entries
- // too -- 233 #121212 for the bars, 238 #444444 for the thumb, 235 #262626
- // for an unfocused box. No hue anywhere outside the syntax colors.
- //
- // Shape: bg/fg are explicit rather than null, because "just really dark"
- // must not depend on whose terminal it lands in -- but `palette` stays null
- // so a shell's ANSI indices still resolve to the host's own 16 colors.
- // helix does the same: it dresses the page it draws and leaves the colors a
- // program emits alone. Only acme-light, which has to stay readable on
- // yellow, resolves the palette itself.
- .{
- .name = "helix",
- .bg = .{ 0x08, 0x08, 0x08 },
- .fg = .{ 0xbd, 0xbd, 0xbd },
- .tag_bg = .{ 0x12, 0x12, 0x12 },
- .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 },
- .comment = .{ 0xff, 0xff, 0xff },
- .lineno = .{ 0x62, 0x62, 0x62 },
- .scroll_track = .{ 0x12, 0x12, 0x12 },
- .scroll_thumb = .{ 0x44, 0x44, 0x44 },
- .palette = null,
- },
- .{
- .name = "dark",
- .bg = null,
- .fg = null,
- .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 },
- .comment = .{ 0xf0, 0xf0, 0xf0 },
- .lineno = .{ 0x5a, 0x5a, 0x5a },
- .scroll_track = .{ 0x2a, 0x2a, 0x2a },
- .scroll_thumb = .{ 0x52, 0x52, 0x52 },
- .palette = null,
- },
- .{
- .name = "acme",
- .bg = .{ 0xff, 0xff, 0xea },
- .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 },
- .str = .{ 0x00, 0x77, 0x33 },
- .num = .{ 0xa0, 0x52, 0x00 },
- .comment = .{ 0x55, 0x55, 0x55 },
- .lineno = .{ 0x99, 0x99, 0x80 },
- .scroll_track = .{ 0x99, 0x99, 0x4c },
- .scroll_thumb = .{ 0xff, 0xff, 0xea },
- .palette = .{
- .{ 0x00, 0x00, 0x00 }, .{ 0xaa, 0x22, 0x22 }, .{ 0x22, 0x80, 0x22 }, .{ 0x88, 0x66, 0x00 },
- .{ 0x22, 0x44, 0xbb }, .{ 0x99, 0x33, 0x99 }, .{ 0x11, 0x88, 0x88 }, .{ 0x55, 0x55, 0x55 },
- .{ 0x88, 0x88, 0x88 }, .{ 0xcc, 0x33, 0x33 }, .{ 0x33, 0xaa, 0x33 }, .{ 0xaa, 0x77, 0x00 },
- .{ 0x33, 0x55, 0xdd }, .{ 0xbb, 0x44, 0xbb }, .{ 0x22, 0xaa, 0xaa }, .{ 0x00, 0x00, 0x00 },
- },
- },
+/// The three themes pardes ships with, in RING ORDER, which is the one thing
+/// the files themselves cannot say: `helix` is index 0 and so what boots, and
+/// NextColor walks from here out into the generated ones. Written here rather
+/// than in a fourth file because the order IS the information — and it is
+/// load-bearing, since test/snapshots/theme.snap captures the first three
+/// steps of the ring by their colors.
+const curated = struct {
+ pub const helix = @import("themes/helix.zig");
+ pub const dark = @import("themes/dark.zig");
+ pub const acme = @import("themes/acme.zig");
};
+/// A zig file IS a struct, so the FILES are the list: this walks a container's
+/// declarations — each one an imported theme file — and copies its `theme`
+/// value into a real Theme. FIELD BY FIELD rather than by plain coercion,
+/// because a theme file deliberately imports nothing (it is data, not code) and
+/// zig will not coerce a whole anonymous struct into a named one; assigning one
+/// field at a time puts each value in a result location that knows the type,
+/// which is also what makes a missing field a compile error naming it.
+///
+/// A FUNCTION and not a const for the same reason builtins.all is one: it is
+/// only ever a signature to whoever walks it, never a value in its own way.
+fn fold(comptime C: type) []const Theme {
+ comptime {
+ @setEvalBranchQuota(20000); // themes x fields, and each field a @field lookup
+ var out: []const Theme = &.{};
+ for (@typeInfo(C).@"struct".decls) |d| {
+ var t: Theme = undefined;
+ for (@typeInfo(Theme).@"struct".fields) |f| @field(t, f.name) = @field(@field(C, d.name).theme, f.name);
+ out = out ++ &[_]Theme{t};
+ }
+ return out;
+ }
+}
+
+/// The ring: ours, then every theme tools/gen_themes.zig exported out of the
+/// helix and zed sources in vendor/themes (build.zig runs it and hands the
+/// result over as a module). Adding one is dropping a file in there — there is
+/// no list here to append to, which is the whole point of folding the files.
+pub const themes = fold(curated) ++ fold(@import("generated_themes"));
+
// ---- the boundary types ----
pub const Color = union(enum) { default, index: u8, rgb: [3]u8 };
@@ -2827,10 +2784,12 @@ pub const Pardes = struct {
}
/// n/N: step to the next/previous row of this pane's results buffer and
- /// look it — every row there IS a result, so the leading file-ish word
- /// (`path:LINE:COL` or `@pN:LINE:COL`) does the rest through the ordinary
- /// look path. False = no live search: a terminal's n/N falls back to
- /// lookStep, anything else stays put.
+ /// ACT on it — which of the two acme verbs that is comes from the buffer's
+ /// own traits, so this is one motion over "a list of things you can run",
+ /// not two kinds of stepping. A location list (every search, the jumplist,
+ /// every language answer) Looks the leading `path:LINE:COL` word; a command
+ /// list (ThemeSel) Execs the whole row. False = no live search: a
+ /// terminal's n/N falls back to lookStep, anything else stays put.
fn searchStep(p: *Pardes, id: usize, delta: i32) bool {
const pane = p.panes[id] orelse return false;
const rid = pane.search_pane orelse return false;
@@ -2838,7 +2797,8 @@ pub const Pardes = struct {
const rf = if (rp.file) |*f| f else return false;
// one question covers both hazards: a freed slot can be reused by an
// unrelated pane, and a buffer of PROSE has nowhere to step to
- if (!output_pane.fileTraits(rf.output).steps) return false;
+ const tr = output_pane.fileTraits(rf.output);
+ if (!tr.steps) return false;
// fresh results: n starts at the first row, N has nothing behind it
const nrows: i64 = @intCast(std.mem.count(u8, rf.content, "\n"));
const step: i64 = if (pane.search_row) |c| @as(i64, @intCast(c)) + delta else if (delta > 0) 0 else -1;
@@ -2853,9 +2813,18 @@ pub const Pardes = struct {
rp.cur_pinned = true;
rp.ensureCursorVisible();
const ln = modal.lineSlice(rf.content, @intCast(step));
- var hi: usize = 0;
- while (hi < ln.len and config.isFileChar(ln[hi])) hi += 1;
- p.lookAt(rid, ln[0..hi]);
+ // Both arms are the BUILTIN, run on the results pane — the same call a
+ // middle or right click on that row would make, so a stepped row and a
+ // clicked row can never drift apart. A command row goes whole (its
+ // argument is the tail after the name); a location row is cut to the
+ // leading file-ish word, since the rest of it is the matched text.
+ if (tr.executes) {
+ p.runBuiltin(config.exec_cmd, rid, "", std.mem.trim(u8, ln, " \t\r"));
+ } else {
+ var hi: usize = 0;
+ while (hi < ln.len and config.isFileChar(ln[hi])) hi += 1;
+ p.runBuiltin(config.look_cmd, rid, "", ln[0..hi]);
+ }
// the look may focus what it opened — a Find row opens a whole new
// file pane every time — so focus comes back to the pane that owns the
// search and the next n keeps stepping. A `/` row looks at the
@@ -5125,15 +5094,15 @@ pub const Pardes = struct {
defer p.exec_depth -= 1;
// The builtins that take an ARGUMENT match their name with a TAIL:
// `Restore <path>`, `Find <pat>`, `Grep <pat>`, `Rename <name>`,
- // `WsSymbols <sym>`, and the two verbs themselves — `Look <word>`,
- // `Exec <cmd>`, which is what makes `` @`Look .` `` nest (the tail goes
- // straight back through here). Every other name must match WHOLE, so
- // `Kill foo` is a shell command and not Kill.
+ // `WsSymbols <sym>`, `Theme <name>`, and the two verbs themselves —
+ // `Look <word>`, `Exec <cmd>`, which is what makes `` @`Look .` `` nest
+ // (the tail goes straight back through here). Every other name must
+ // match WHOLE, so `Kill foo` is a shell command and not Kill.
const sp = std.mem.indexOfAny(u8, cmd, " \t");
const bi: ?Builtin = std.meta.stringToEnum(Builtin, cmd) orelse blk: {
const head = std.meta.stringToEnum(Builtin, cmd[0 .. sp orelse break :blk null]) orelse break :blk null;
break :blk switch (head) {
- .Restore, .Find, .Grep, .Rename, .WsSymbols, .Look, .Exec => head,
+ .Restore, .Find, .Grep, .Rename, .WsSymbols, .Theme, .Look, .Exec => head,
else => null,
};
};
diff --git a/src/themes/acme.zig b/src/themes/acme.zig
new file mode 100644
index 00000000..925cacfb
--- /dev/null
+++ b/src/themes/acme.zig
@@ -0,0 +1,29 @@
+//! acme's yellow page — the light theme, and the one that has to resolve
+//! `palette` itself. A theme that only dresses the page can leave a child's
+//! ANSI indices alone and let the host answer them; on #ffffea that is how you
+//! get pale yellow `ls` output on pale yellow paper. So the sixteen below are
+//! darkened for paper: index 7 (the "white" a shell writes most of its output
+//! in) is a mid grey, and 15 (bright white) is BLACK, because a program asking
+//! for the brightest color on a light page means "make this stand out".
+pub const theme = .{
+ .name = "acme",
+ .bg = .{ 0xff, 0xff, 0xea },
+ .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 },
+ .str = .{ 0x00, 0x77, 0x33 },
+ .num = .{ 0xa0, 0x52, 0x00 },
+ .comment = .{ 0x55, 0x55, 0x55 },
+ .lineno = .{ 0x99, 0x99, 0x80 },
+ .scroll_track = .{ 0x99, 0x99, 0x4c },
+ .scroll_thumb = .{ 0xff, 0xff, 0xea },
+ .palette = .{
+ .{ 0x00, 0x00, 0x00 }, .{ 0xaa, 0x22, 0x22 }, .{ 0x22, 0x80, 0x22 }, .{ 0x88, 0x66, 0x00 },
+ .{ 0x22, 0x44, 0xbb }, .{ 0x99, 0x33, 0x99 }, .{ 0x11, 0x88, 0x88 }, .{ 0x55, 0x55, 0x55 },
+ .{ 0x88, 0x88, 0x88 }, .{ 0xcc, 0x33, 0x33 }, .{ 0x33, 0xaa, 0x33 }, .{ 0xaa, 0x77, 0x00 },
+ .{ 0x33, 0x55, 0xdd }, .{ 0xbb, 0x44, 0xbb }, .{ 0x22, 0xaa, 0xaa }, .{ 0x00, 0x00, 0x00 },
+ },
+};
diff --git a/src/themes/dark.zig b/src/themes/dark.zig
new file mode 100644
index 00000000..df418982
--- /dev/null
+++ b/src/themes/dark.zig
@@ -0,0 +1,22 @@
+//! pardes's own dark theme, and the boot default until the helix capture
+//! displaced it. It stays in the ring because it is the only one that leaves
+//! `bg`/`fg` NULL: the page keeps the host terminal's own default cell, so
+//! pardes sits inside whatever the surrounding terminal already looks like and
+//! only paints its chrome. Every other theme here dresses the whole page.
+pub const theme = .{
+ .name = "dark",
+ .bg = null,
+ .fg = null,
+ .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 },
+ .comment = .{ 0xf0, 0xf0, 0xf0 },
+ .lineno = .{ 0x5a, 0x5a, 0x5a },
+ .scroll_track = .{ 0x2a, 0x2a, 0x2a },
+ .scroll_thumb = .{ 0x52, 0x52, 0x52 },
+ .palette = null,
+};
diff --git a/src/themes/helix.zig b/src/themes/helix.zig
new file mode 100644
index 00000000..26c156ff
--- /dev/null
+++ b/src/themes/helix.zig
@@ -0,0 +1,47 @@
+//! Copied off the helix the user actually runs: config.toml says
+//! theme = "pardes", i.e. ~/.config/helix/themes/pardes.toml. Every value below
+//! was then PINNED against a real `hx sample.zig` render captured through a
+//! pty, not read off the TOML and hoped for — the SGR runs were 48;2;8;8;8
+//! page, 38;2;189;189;189 text, 38;2;98;98;98 line numbers, 1;38;2;207;135;232
+//! keywords, 38;2;227;199;138 strings AND numeric constants, 38;2;255;255;255
+//! comments. `zig build snap` holds them there (test/snapshots/theme.snap), so
+//! these fifteen numbers are a golden and not taste.
+//!
+//! "dark and minimal" is the half that is not a table. That theme sets no
+//! ui.statusline at all, so helix paints its status row in the page's own
+//! colors: in the capture, rows 8..23 carry one unbroken 48;2;8;8;8 run and the
+//! statusline is told apart from the file by its words alone. pardes cannot go
+//! quite that far — its tag bar is a click target and the gutter is a drag
+//! handle — so chrome lifts by the smallest honest step instead: the theme's
+//! greys are all xterm grayscale-ramp entries (232 #080808, 241 #626262,
+//! 246 #949494, 250 #bdbdbd), so the chrome takes ramp entries too — 233
+//! #121212 for the bars, 238 #444444 for the thumb, 235 #262626 for an
+//! unfocused box. No hue anywhere outside the syntax colors.
+//!
+//! Shape: bg/fg are explicit rather than null, because "just really dark" must
+//! not depend on whose terminal it lands in — but `palette` stays null so a
+//! shell's ANSI indices still resolve to the host's own 16 colors. helix does
+//! the same: it dresses the page it draws and leaves the colors a program emits
+//! alone. Only acme, which has to stay readable on yellow, resolves the palette
+//! itself.
+//!
+//! No imports, and neither has any other theme file: a theme is DATA. The
+//! `.{...}` becomes a pardes.Theme in pardes.zig's `fold`, which is also what
+//! turns a missing field here into a compile error naming it.
+pub const theme = .{
+ .name = "helix",
+ .bg = .{ 0x08, 0x08, 0x08 },
+ .fg = .{ 0xbd, 0xbd, 0xbd },
+ .tag_bg = .{ 0x12, 0x12, 0x12 },
+ .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 },
+ .comment = .{ 0xff, 0xff, 0xff },
+ .lineno = .{ 0x62, 0x62, 0x62 },
+ .scroll_track = .{ 0x12, 0x12, 0x12 },
+ .scroll_thumb = .{ 0x44, 0x44, 0x44 },
+ .palette = null,
+};