From 0bfa4615988c0d1bcaa38453375760c60076c5ea Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Fri, 31 Jul 2026 02:21:01 -0300 Subject: themes: one file each, generated from helix and zed, picked by stepping a list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each theme is a .zig file of pure data and nothing enumerates them by hand — fold() walks the container's declarations, so a theme is a file and that is the whole registration. Field by field rather than a wholesale coercion, which makes a missing field a compile error that names it. tools/gen_themes.zig reads helix .toml and zed .json out of vendor/themes and emits one .zig each; build.zig reads that directory, so adding a theme is dropping a file in. Output goes to the build cache rather than the tree, so zig owns the freshness check and a deleted source cannot leave a stale theme behind. Vendored, not read from the genizah: the build stays offline. A source it cannot map is a hard error naming the file and the key, never a silently black-on-black theme. ThemeSel is the clever half. Traits gained an `executes` column, so n/N over that buffer hands the whole line to Exec instead of the leading word to Look — both arms the ordinary builtin, so a stepped row does exactly what the matching mouse button on it would. Stepping the list previews each theme live. The trait is a pane property, not an output-pane branch, so any pane whose lines are commands can opt in. Goldens: leader gains SPC t t; theme's fourth NextColor no longer wraps to helix because the ring is fifteen long. New: themesel. --- src/builtins.zig | 32 +++++++++++ src/config.zig | 6 ++ src/output_pane.zig | 54 +++++++++++++++-- src/pardes.zig | 159 +++++++++++++++++++++------------------------------ src/themes/acme.zig | 29 ++++++++++ src/themes/dark.zig | 22 +++++++ src/themes/helix.zig | 47 +++++++++++++++ 7 files changed, 249 insertions(+), 100 deletions(-) create mode 100644 src/themes/acme.zig create mode 100644 src/themes/dark.zig create mode 100644 src/themes/helix.zig (limited to 'src') 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 ` 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 ` 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 ` 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 `, `Find `, `Grep `, `Rename `, - // `WsSymbols `, and the two verbs themselves — `Look `, - // `Exec `, 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 `, `Theme `, and the two verbs themselves — + // `Look `, `Exec `, 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, +}; -- cgit v1.3