diff options
Diffstat (limited to 'src/builtins.zig')
| -rw-r--r-- | src/builtins.zig | 305 |
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 |
