summaryrefslogtreecommitdiff
path: root/src/builtins.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-16 15:49:12 -0300
committerGabriel Schneider <[email protected]>2026-08-18 23:44:42 -0300
commit1551e409c31992437cb2fa864f576d45c8433801 (patch)
treee2fae8451f87b735a1360c7c2e383fdc40165789 /src/builtins.zig
parentbe2a9957708cbf0c478ca861c4a1f0f227bbfe10 (diff)
downloadpardes-1551e409c31992437cb2fa864f576d45c8433801.tar.gz
pardes-1551e409c31992437cb2fa864f576d45c8433801.zip
big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh
Diffstat (limited to 'src/builtins.zig')
-rw-r--r--src/builtins.zig305
1 files changed, 132 insertions, 173 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index bb1a1273..d42aeafc 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -1,4 +1,5 @@
-//! The builtins: one struct each, and nothing hand-maintained about them.
+//! The builtins: one struct each, plus setting commands generated from one
+//! runtime_config table.
//!
//! Executing a builtin's NAME (middle-click / Tab) runs it through the one
//! dispatcher (Pardes.runBuiltin), no matter where the name appears. The
@@ -6,20 +7,14 @@
//! one sitting in a tag, the one Help prints, the one you execute — so
//! `std.meta.stringToEnum` is the lookup and there is no name table to sync.
//!
-//! A zig file IS a struct, so THIS FILE'S declarations are the list: `all`
-//! walks them at comptime and `Builtin` folds an enum out of that, in source
-//! order. Adding a builtin is writing one struct here — there is no list to
-//! append to and no switch prong to add, so there is nothing to forget. The
-//! hand-written enum and the four-hundred-line-away switch this replaces were
-//! two lists that had to agree; now the code IS the data.
+//! A zig file IS a struct, so THIS FILE'S declarations are the manual list:
+//! the registry walks them at comptime and appends the enabled settings from
+//! runtime_config.settings. There is no hand-maintained enum or dispatcher
+//! switch to keep in sync; declarations and setting descriptors are the data.
//!
-//! What makes a declaration a builtin is its SHAPE: a struct declaring
-//! `pub fn run(Ctx) void`. That is strict enough that Ctx and the two folds
-//! below fall out by construction rather than by a blocklist — a helper can
-//! never accidentally become a builtin, and a builtin whose run has the wrong
-//! signature silently vanishes instead of half-working, which the leader
-//! table catches immediately (see leader_path: a missing key path is a
-//! compile error).
+//! A manual builtin is a struct declaring `pub fn run(Ctx) void`; an optional
+//! explicit `enabled` declaration gates it. Helpers have no `run`, and a
+//! claimed builtin with the wrong signature is a compile error.
//!
//! What is NOT here: the key bindings. `leader_path` is ONE table in
//! config.zig next to `topbar_str` and every other syntactic choice — the
@@ -30,12 +25,19 @@ const pardes = @import("pardes.zig");
const Pardes = pardes.Pardes;
const Pane = pardes.Pane;
const output_pane = @import("output_pane.zig");
+const image_pane = @import("image_pane.zig");
const config = @import("config.zig");
-/// The installed fonts, for the two builtins at the bottom of this file. A
-/// GUI-only file behind a comptime branch, the way look.zig imports the web's
-/// source archive: the tty and web builds evaluate the other arm and compile
-/// none of it.
-const fonts = if (pardes.font_picker) @import("fonts.zig") else struct {};
+const runtime_config = @import("runtime_config.zig");
+
+/// The platform's runtime-setting facilities, stated once as plain data.
+/// Registry generation, leader paths, Config, and EffectCode all consume this
+/// exact value rather than rebuilding equivalent-looking boolean expressions.
+pub const capabilities: runtime_config.Capabilities = .{
+ .font_picker = pardes.font_picker,
+ .panel_transitions = pardes.platform != .web,
+ .scene_shaders = pardes.platform == .gui or pardes.platform == .macos,
+ .tagline_font_size = pardes.platform != .tty,
+};
/// What a builtin gets to act on. One bundle rather than five parameters
/// because most builtins want two of them and zig rejects the unused rest.
@@ -61,7 +63,7 @@ fn isEnabled(comptime T: type) bool {
return !@hasDecl(T, "enabled") or T.enabled;
}
-fn builtinCount() comptime_int {
+fn manualBuiltinCount() comptime_int {
comptime {
var count = 0;
for (@typeInfo(@This()).@"struct".decls) |d| {
@@ -76,10 +78,10 @@ fn builtinCount() comptime_int {
}
}
-fn builtinList() [builtinCount()]type {
+fn manualBuiltinList() [manualBuiltinCount()]type {
comptime {
@setEvalBranchQuota(4000);
- var list: [builtinCount()]type = undefined;
+ var list: [manualBuiltinCount()]type = undefined;
var count = 0;
for (@typeInfo(@This()).@"struct".decls) |d| {
if (@TypeOf(@field(@This(), d.name)) != type) continue;
@@ -92,6 +94,28 @@ fn builtinList() [builtinCount()]type {
}
}
+fn settingCount() comptime_int {
+ comptime {
+ var count = 0;
+ for (runtime_config.settings) |setting| if (setting.enabled(capabilities)) {
+ count += 1;
+ };
+ return count;
+ }
+}
+
+fn settingList() [settingCount()]runtime_config.Setting {
+ comptime {
+ var list: [settingCount()]runtime_config.Setting = undefined;
+ var count = 0;
+ for (runtime_config.settings) |setting| if (setting.enabled(capabilities)) {
+ list[count] = setting;
+ count += 1;
+ };
+ return list;
+ }
+}
+
/// A builtin's user-visible word: the LAST dotted segment of `@typeName`,
/// because @typeName spells a file-scope struct fully qualified
/// ("builtins.Kill"). Deriving it beats a `pub const name` field per struct,
@@ -108,6 +132,8 @@ pub const OutputTraits = struct {
steps: bool = false,
jumps: bool = false,
commands: bool = false,
+ doc: bool = false,
+ saves: bool = false,
};
/// The enum: field name = struct name, value = index into `all()`. Everything
@@ -118,31 +144,55 @@ pub const OutputTraits = struct {
/// walk only sees this namespace's type, not functions whose signatures depend
/// on the builtin enum being constructed.
pub const registry = struct {
- pub fn all() [builtinCount()]type {
- return builtinList();
- }
-
pub fn Builtin() type {
- const cmds = builtinList();
- const Tag = std.math.IntFittingRange(0, cmds.len - 1);
- var names: [cmds.len][]const u8 = undefined;
- for (cmds, 0..) |T, i| names[i] = word(T);
- return @Enum(Tag, .exhaustive, &names, &std.simd.iota(Tag, cmds.len));
+ @setEvalBranchQuota(20_000);
+ const manual = manualBuiltinList();
+ const generated = settingList();
+ const count = manual.len + generated.len;
+ const Tag = std.math.IntFittingRange(0, count - 1);
+ var names: [count][]const u8 = undefined;
+ for (manual, 0..) |T, i| names[i] = word(T);
+ for (generated, manual.len..) |setting, i| names[i] = setting.word;
+ for (names, 0..) |name, i| for (names[i + 1 ..]) |later|
+ if (std.mem.eql(u8, name, later)) @compileError("duplicate builtin name: " ++ name);
+ return @Enum(Tag, .exhaustive, &names, &std.simd.iota(Tag, count));
}
pub fn takesArg(b: Builtin()) bool {
- inline for (builtinList(), 0..) |T, i|
+ inline for (manualBuiltinList(), 0..) |T, i|
if (@intFromEnum(b) == i) return @hasDecl(T, "takes_arg") and T.takes_arg;
+ inline for (comptime settingList(), manualBuiltinCount()..) |setting, i|
+ if (@intFromEnum(b) == i) return setting.takesArg();
unreachable;
}
pub fn outputTraits(b: Builtin()) ?OutputTraits {
- inline for (builtinList(), 0..) |T, i|
+ inline for (manualBuiltinList(), 0..) |T, i|
if (@intFromEnum(b) == i) return if (@hasDecl(T, "output")) T.output else null;
+ inline for (comptime settingList(), manualBuiltinCount()..) |_, i|
+ if (@intFromEnum(b) == i) return null;
+ unreachable;
+ }
+
+ pub fn dispatch(b: Builtin(), c: Ctx) void {
+ inline for (manualBuiltinList(), 0..) |T, i|
+ if (@intFromEnum(b) == i) return T.run(c);
+ inline for (comptime settingList(), manualBuiltinCount()..) |setting, i|
+ if (@intFromEnum(b) == i) return c.p.applySettingBuiltin(setting, c.arg);
unreachable;
}
};
+test "capabilities exactly gate setting and effect-source builtins" {
+ const Builtin = registry.Builtin();
+ for (runtime_config.settings) |setting| {
+ const registered = std.meta.stringToEnum(Builtin, setting.word) != null;
+ try std.testing.expectEqual(setting.enabled(capabilities), registered);
+ }
+ const effect_code_registered = std.meta.stringToEnum(Builtin, "EffectCode") != null;
+ try std.testing.expectEqual(EffectCode.enabled, effect_code_registered);
+}
+
// ---- the two acme verbs ----
// Look and Execute are the verbs the whole environment is built on, and they
@@ -232,42 +282,12 @@ pub const Msg = struct {
}
};
-// ---- display toggles ----
-
-pub const Debug = struct {
- pub fn run(c: Ctx) void {
- c.p.show_debug = !c.p.show_debug;
- }
-};
-
-pub const Colors = struct {
- pub fn run(c: Ctx) void {
- c.p.colors_on = !c.p.colors_on;
- }
-};
-
-/// Soft wrap: a file line too long for the pane renders across several body
-/// rows instead of running off the right edge. Global, the way Colors is —
-/// it changes what every file pane looks like, so it is a switch you flip
-/// once rather than something a pane carries. ON by default (Pardes.wrap_on),
-/// so this is the word that turns wrapping OFF and gives hscroll back.
-pub const Wrap = struct {
- pub fn run(c: Ctx) void {
- c.p.wrap_on = !c.p.wrap_on;
- }
-};
-
-/// Taglines at the BOTTOM of every pane instead of the top. Global like Wrap
-/// and for the same reason — a layout where some panes label themselves above
-/// and others below is unreadable, so it is one switch for the whole screen.
-/// The transient message row gives that row up and takes the one above it (see
-/// render), so it is the pane's last BODY row either way and a prompt stays
-/// beside the tag it is typed into.
-pub const Tagbottom = struct {
- pub fn run(c: Ctx) void {
- c.p.tag_bottom = !c.p.tag_bottom;
- }
-};
+// ---- display choices ----
+//
+// The plain toggles, named theme/shell/font setters and animation effects are
+// generated from runtime_config.settings. Keeping their command metadata and
+// their query order in the same value table is what prevents a settable choice
+// from disappearing from Config. Hand-written commands continue below.
/// One step along the ring. With 228 themes in it this is no longer a way to
/// REACH a theme — ThemeSel is — but it is still the way to browse one, and the
@@ -278,7 +298,7 @@ pub const Tagbottom = struct {
/// clicks was never what made it useful.
pub const NextColor = struct {
pub fn run(c: Ctx) void {
- c.p.setThemeIndex((c.p.theme_idx + 1) % pardes.themes.len);
+ c.p.setThemeIndex((@as(usize, c.p.settings.theme) + 1) % pardes.themes.len);
}
};
@@ -292,41 +312,6 @@ pub const NextColor = struct {
/// name->index map, which is a second copy of the ring to build for a lookup
/// nobody will ever measure. 228 short string compares is microseconds, and it
/// happens once per theme change, not once per frame.
-pub const Theme = struct {
- pub const takes_arg = true;
- pub fn run(c: Ctx) void {
- const want = std.mem.trim(u8, c.arg orelse return, " \t\r\n");
- for (pardes.themes, 0..) |t, i| {
- if (std.mem.eql(u8, t.name, want)) {
- c.p.setThemeIndex(i);
- return;
- }
- }
- }
-};
-
-/// The binary a NEW terminal execs: `Shell fish`, `Shell zsh`,
-/// `Shell /opt/homebrew/bin/bash`. A bare name is looked up on the shells'
-/// side against the handful of directories a shell actually lives in — not
-/// $PATH, because the lookup has to finish BEFORE the fork (a PATH search
-/// allocates, and nothing between fork and exec may).
-///
-/// Takes a NAME, so it has no leader path and none in the topbar, for the same
-/// reason Theme has neither. Panes already open keep the shell they are
-/// running; this is what the next one starts.
-pub const Shell = struct {
- pub const takes_arg = true;
- pub fn run(c: Ctx) void {
- const want = std.mem.trim(u8, c.arg orelse return, " \t\r\n");
- // A name too long for the buffer is not silently truncated into a
- // DIFFERENT binary — `/very/long/path/to/fish` clipped is a path that
- // either does not exist or, worse, does.
- if (want.len == 0 or want.len > c.p.shell_buf.len) return;
- @memcpy(c.p.shell_buf[0..want.len], want);
- c.p.shell_len = @intCast(want.len);
- }
-};
-
/// ...and the list of what Theme takes, as a buffer you walk. Its rows are
/// `Theme <name>` COMMANDS rather than locations, which is one flag on the
/// buffer (output_pane.Traits.commands) and changes what a step SELECTS: the
@@ -340,28 +325,12 @@ pub const ThemeSel = struct {
}
};
-pub const Crt = struct {
- pub fn run(c: Ctx) void {
- c.p.crt_on = !c.p.crt_on;
- }
-};
-
-// ---- the GUI's font, and NOTHING on any other platform ----
-//
-// Theme and ThemeSel again, one layer down: a word that takes a name, and the
-// list of what it takes. What is different is that these two only EXIST in a
-// gui build, and the mechanism is the one this file's header describes rather
-// than a new one — `all()` folds the enum out of the SHAPE of each decl, so a
-// struct whose `run` is not `fn (Ctx) void` is not a builtin. Here `run` is a
-// void const on tty and web: the enum has no field, `SPC ?` has no row, the
-// dispatcher has no prong, and nothing in a tty binary ever opens a font
-// directory. The body sits inside the struct as an ordinary private decl,
-// which the walk never sees (it only reads pub, file-scope declarations) and
-// which nothing on those platforms ever analyses.
+// ---- the GUI's font list, and NOTHING on any other platform ----
//
-// A `pub const Font = if (gui) struct {...} else struct {}` would read better
-// and is WRONG: zig names a struct born inside an if-expression
-// "builtins.Font__struct_32751", and that name is the user-visible word.
+// Runtime settings such as Font are generated from runtime_config.settings.
+// FontSel remains hand-written because it opens a result pane. Its explicit
+// `enabled` bit is the same feature gate the registry uses for PDF commands:
+// disabled commands have no enum field, help row, or dispatcher case.
/// The GUI font BY NAME — `Font DejaVuSansMono-Regular`, the way `Theme <name>`
/// takes a theme, and inert without an argument for the same reason (there is
@@ -371,26 +340,8 @@ pub const Crt = struct {
/// findable.
///
/// The core cannot load a font — it has no rasterizer, no atlas and no window
-/// — so this asks: the resolved PATH goes in fonts.want, the shell takes it on
+/// — so this asks: the resolved PATH goes in runtime config, the shell takes it on
/// its next pass and re-rasters. Exactly the shape Restore already has.
-pub const Font = struct {
- pub const takes_arg = true;
- pub const enabled = pardes.font_picker;
- pub fn run(c: Ctx) void {
- if (comptime enabled) apply(c) else unreachable;
- }
- fn apply(c: Ctx) void {
- const want = std.mem.trim(u8, c.arg orelse return, " \t\r\n");
- const hit = fonts.list(c.p.scratch.allocator(), want);
- if (hit.len == 0) return;
- const path = hit[0].path;
- if (path.len > fonts.want_buf.len) return;
- @memcpy(fonts.want_buf[0..path.len], path);
- fonts.want = fonts.want_buf[0..path.len];
- fonts.chosen = std.fs.path.stem(fonts.want.?);
- }
-};
-
/// ...and the list of what Font takes: every MONOSPACE font on the machine,
/// one `Font <name>` row each, in the picker ThemeSel already is (rows that
/// are commands, so n/N select each one WHOLE and Tab runs it — walking with
@@ -403,7 +354,7 @@ pub const Font = struct {
/// them would mean the list wearing one on the way past. See fonts.monospaced.
pub const FontSel = struct {
pub const output: OutputTraits = .{ .name = config.fonts_buffer, .steps = true, .commands = true };
- pub const enabled = pardes.font_picker;
+ pub const enabled = capabilities.font_picker;
pub fn run(c: Ctx) void {
if (comptime enabled) apply(c) else unreachable;
}
@@ -413,30 +364,28 @@ pub const FontSel = struct {
};
/// Toggle a native PDF between the reading-oriented fit-width view and the
-/// whole-page-height view. Like Font on non-GUI builds, this declaration's
-/// `run` deliberately has the wrong shape when MuPDF is disabled: `all()` then
-/// omits it entirely, so the enum, Help and runtime binary contain no
-/// PdfFit.
+/// whole-page-height view. The explicit feature gate omits the command, help
+/// row, and dispatcher case when MuPDF is disabled.
pub const PdfFit = struct {
pub const enabled = pardes.pdf_enabled;
pub fn run(c: Ctx) void {
if (comptime enabled) apply(c) else unreachable;
}
fn apply(c: Ctx) void {
- c.p.togglePdfFit(c.pane);
+ pardes.pdf_pane.toggleFit(c.pane);
}
};
/// Cycle a native PDF through original pixels, a chroma-preserving themed
-/// filter, and a full theme duotone. It has the same compile-time and
-/// pane-local shape as PdfFit: absent without MuPDF and inert off a PDF pane.
+/// filter, and a full theme duotone. It has the same explicit feature gate as
+/// PdfFit: absent without MuPDF and inert off a PDF pane.
pub const PdfTint = struct {
pub const enabled = pardes.pdf_enabled;
pub fn run(c: Ctx) void {
if (comptime enabled) apply(c) else unreachable;
}
fn apply(c: Ctx) void {
- c.p.togglePdfTint(c.pane);
+ pardes.pdf_pane.toggleTint(c.pane);
}
};
@@ -450,42 +399,34 @@ pub const PdfSections = struct {
if (comptime enabled) apply(c) else unreachable;
}
fn apply(c: Ctx) void {
- c.p.openPdfSections(c.id);
+ pardes.pdf_pane.openSections(c.p, c.id);
}
};
-// The image pane's three renderer toggles. They used to be words the image tag
-// printed and the execute dispatcher matched by hand; as builtins they are
-// executable anywhere, pressable under SPC and listed by `SPC ?`, the whole
-// reason the tag no longer carries them. Each acts on the pane it runs in and
-// is inert anywhere else, the way Save is on a terminal — flipping the field is
-// the whole toggle: drawImage re-matches the glyph grid when it sees
-// grid_mode/grid_ascii disagree with the live ones.
+// The image pane's three renderer toggles. They used to be executable words
+// interpreted by a special tag dispatcher; as ordinary builtins they are
+// pressable under SPC and listed by `SPC ?`. The tag now reports their plain
+// live values without becoming a second mutation path. Each acts on the pane
+// it runs in and is inert elsewhere, the way Save is on a terminal.
/// glyph art over the host's pixels
pub const Petscii = struct {
pub fn run(c: Ctx) void {
- if (c.pane.image) |*iv| {
- iv.petscii = !iv.petscii;
- }
+ if (c.pane.image) |*state| image_pane.togglePetscii(state);
}
};
/// the C64 palette or the terminal's own 16
pub const Palette = struct {
pub fn run(c: Ctx) void {
- if (c.pane.image) |*iv| {
- iv.pmode = if (iv.pmode == .commodore) .terminal else .commodore;
- }
+ if (c.pane.image) |*state| image_pane.togglePalette(state);
}
};
/// add the printable ASCII bitmaps to the matcher's glyph set
pub const Ascii = struct {
pub fn run(c: Ctx) void {
- if (c.pane.image) |*iv| {
- iv.ascii = !iv.ascii;
- }
+ if (c.pane.image) |*state| image_pane.toggleAscii(state);
}
};
@@ -573,10 +514,12 @@ pub const New = struct {
pub const Newcol = struct {
pub fn run(c: Ctx) void {
const free = c.p.freeSlot() orelse return;
- if (c.p.ncol >= pardes.MAX_COLS) return;
+ if (!c.p.layoutCanSplitColumn(c.id)) return;
const nt = c.p.newShell(free, "") catch return;
nt.greet = true;
- c.p.layoutAppendColumn(free);
+ // The shared preflight above is the complete geometry refusal set.
+ // Pane creation does not alter layout, so the same split is infallible.
+ std.debug.assert(c.p.layoutSplitColumn(c.id, free, false));
c.p.active = free;
}
};
@@ -647,6 +590,22 @@ pub const Config = struct {
}
};
+/// Source code for the concrete backend implementation of a Panel*/scene
+/// effect. The bytes are embedded at build time, so this works from an
+/// installed executable rather than depending on a source checkout.
+pub const EffectCode = struct {
+ pub const takes_arg = true;
+ pub const enabled = capabilities.panel_transitions or capabilities.scene_shaders;
+ pub const output: OutputTraits = .{ .name = config.effect_code_buffer };
+ pub fn run(c: Ctx) void {
+ if (comptime enabled)
+ output_pane.openEffectCode(c.p, c.id, c.arg orelse return) catch |err|
+ c.p.reportError(c.id, "effect code", err)
+ else
+ unreachable;
+ }
+};
+
// ---- search ----
// The two builtins that ASK for something — Find walks file NAMES under this