summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig159
1 files changed, 64 insertions, 95 deletions
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,
};
};