summaryrefslogtreecommitdiff
path: root/src/output_pane.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-31 02:21:01 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:08 -0300
commit0bfa4615988c0d1bcaa38453375760c60076c5ea (patch)
treefab4f972c10b07739aabc9d772c43e102bce2c07 /src/output_pane.zig
parentbe404e0d4f91548af6ed600b2fd2027ad8a041c7 (diff)
downloadpardes-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.zig54
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;