diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-31 02:21:01 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:08 -0300 |
| commit | 0bfa4615988c0d1bcaa38453375760c60076c5ea (patch) | |
| tree | fab4f972c10b07739aabc9d772c43e102bce2c07 /src/output_pane.zig | |
| parent | be404e0d4f91548af6ed600b2fd2027ad8a041c7 (diff) | |
| download | pardes-0bfa4615988c0d1bcaa38453375760c60076c5ea.tar.gz pardes-0bfa4615988c0d1bcaa38453375760c60076c5ea.zip | |
themes: one file each, generated from helix and zed, picked by stepping a list
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.
Diffstat (limited to 'src/output_pane.zig')
| -rw-r--r-- | src/output_pane.zig | 54 |
1 files changed, 49 insertions, 5 deletions
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; |
