diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 159 |
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, }; }; |
