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. --- build.zig | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) (limited to 'build.zig') diff --git a/build.zig b/build.zig index c11ccccd..04516cc8 100644 --- a/build.zig +++ b/build.zig @@ -93,7 +93,8 @@ pub fn build(b: *std.Build) void { else null; - // build-time IO, only used to slurp the grammars' highlights.scm queries + // build-time IO: slurps the grammars' highlights.scm queries, and reads + // vendor/themes to find the theme sources var threaded: std.Io.Threaded = .init(b.allocator, .{}); defer threaded.deinit(); const io = threaded.io(); @@ -115,6 +116,40 @@ pub fn build(b: *std.Build) void { .link_libc = true, }); + // THEMES. tools/gen_themes.zig turns each vendored helix .toml / zed .json + // into one .zig file per theme plus a list.zig that imports them all; + // src/pardes.zig folds that list into the ring beside the three it ships. + // + // Straight into the build cache and handed over as a module, rather than + // written back into src/: the output then has no freshness problem to own + // (zig re-runs the generator only when a vendored file changes, and a + // cached run leaves the compile's inputs byte-identical, so `zig build` + // with nothing touched still does nothing), there is nothing to gitignore + // and no stale .zig can survive a deleted source. + // + // The INPUT list is read from the directory here rather than written out, + // so adding a theme is dropping a file in — and each file goes in as a + // content-hashed argument, which is what makes that new file re-run the + // step and nothing else. + const theme_gen = b.addExecutable(.{ + .name = "pardes-gen-themes", + .root_module = b.createModule(.{ + .target = b.graph.host, + .optimize = .ReleaseSafe, + .root_source_file = b.path("tools/gen_themes.zig"), + }), + }); + const run_theme_gen = b.addRunArtifact(theme_gen); + const themes_out = run_theme_gen.addOutputDirectoryArg("themes"); + for (vendoredThemes(b, io)) |name| run_theme_gen.addFileArg(b.path(b.fmt("vendor/themes/{s}", .{name}))); + const themes_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .root_source_file = themes_out.path(b, "list.zig"), + }); + root_mod.addImport("generated_themes", themes_mod); + hx_core_mod.addImport("generated_themes", themes_mod); + // static archives the emcc link needs beyond the zig object (grammars, SDL) var wasm_libs: std.ArrayList(std.Build.LazyPath) = .empty; @@ -713,6 +748,28 @@ fn compileGlsl(b: *std.Build, src: []const u8, stage: []const u8, out: []const u return cmd.addOutputFileArg(out); } +/// The theme sources, sorted. SORTED because the run step is cached by its +/// argv: readdir order is whatever the filesystem feels like, and an argv that +/// shuffles is a cache miss and a rebuild every time. Anything that is not a +/// .toml or a .json is skipped, which is what lets the upstream LICENSE files +/// sit beside the themes they cover. +fn vendoredThemes(b: *std.Build, io: std.Io) []const []const u8 { + var dir = b.build_root.handle.openDir(io, "vendor/themes", .{ .iterate = true }) catch @panic("open vendor/themes"); + defer dir.close(io); + var names: std.ArrayList([]const u8) = .empty; + var it = dir.iterate(); + while (it.next(io) catch @panic("read vendor/themes")) |e| { + if (!std.mem.endsWith(u8, e.name, ".toml") and !std.mem.endsWith(u8, e.name, ".json")) continue; + names.append(b.allocator, b.dupe(e.name)) catch @panic("OOM"); + } + std.mem.sort([]const u8, names.items, {}, struct { + fn lt(_: void, a: []const u8, c: []const u8) bool { + return std.mem.order(u8, a, c) == .lt; + } + }.lt); + return names.items; +} + fn detectEmscriptenSysroot(b: *std.Build) ?[]const u8 { if (b.option([]const u8, "emscripten-sysroot", "path to the Emscripten sysroot")) |path| return path; if (b.sysroot) |path| return path; -- cgit v1.3