summaryrefslogtreecommitdiff
path: root/src/builtins.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/builtins.zig')
-rw-r--r--src/builtins.zig154
1 files changed, 110 insertions, 44 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index ee542e77..3ccbdeed 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -54,36 +54,39 @@ pub const Ctx = struct {
arg: ?[]const u8,
};
-/// Every builtin, in the order they are written below — which is the enum's
-/// numeric order. Nothing reads that order: the topbar picks its own subset by
-/// name and Help sorts by key path, and nothing persists a builtin's integer
-/// (the dump stores tag WORDS), so reordering this file is free.
-///
-/// A FUNCTION and not a const, and so is Builtin below, for one reason: both
-/// walk this file's own declaration list, and a const doing that is a
-/// declaration whose value depends on itself — zig rejects it outright. As
-/// functions they are only ever a signature to the walk, never a value, so
-/// they are not in their own way.
-///
-/// The walk only ever sees `pub` decls, so the imports above are invisible to
-/// it; what it does see and turn away is `all`/`word`/`Builtin` (not types)
-/// and `Ctx` (a type, but with no `run`).
-pub fn all() []const type {
- // the whole body is comptime: a []const type only exists there, and it is
- // what makes the decl walk's `d` a compile-time name rather than a value
+/// Every enabled builtin, in source order. Feature gates are explicit data;
+/// a declaration that claims to be enabled but has the wrong run signature is
+/// a compile error instead of silently disappearing from the command enum.
+fn isEnabled(comptime T: type) bool {
+ return !@hasDecl(T, "enabled") or T.enabled;
+}
+
+fn builtinCount() comptime_int {
comptime {
- @setEvalBranchQuota(4000); // one pass per decl, and @hasDecl builds a map each
- var list: []const type = &.{};
+ var count = 0;
for (@typeInfo(@This()).@"struct".decls) |d| {
- // @TypeOf never evaluates its operand, so this turns away `all`
- // and `Builtin` by their SIGNATURES — asking for either one's
- // VALUE here would be a declaration that depends on itself
if (@TypeOf(@field(@This(), d.name)) != type) continue;
const T = @field(@This(), d.name);
- if (@typeInfo(T) != .@"struct") continue;
- if (!@hasDecl(T, "run")) continue;
- if (@TypeOf(T.run) != fn (Ctx) void) continue;
- list = list ++ &[_]type{T};
+ if (@typeInfo(T) != .@"struct" or !@hasDecl(T, "run") or !isEnabled(T)) continue;
+ if (@TypeOf(T.run) != fn (Ctx) void)
+ @compileError(d.name ++ ".run must have signature fn (Ctx) void");
+ count += 1;
+ }
+ return count;
+ }
+}
+
+fn builtinList() [builtinCount()]type {
+ comptime {
+ @setEvalBranchQuota(4000);
+ var list: [builtinCount()]type = undefined;
+ var count = 0;
+ for (@typeInfo(@This()).@"struct".decls) |d| {
+ if (@TypeOf(@field(@This(), d.name)) != type) continue;
+ const T = @field(@This(), d.name);
+ if (@typeInfo(T) != .@"struct" or !@hasDecl(T, "run") or !isEnabled(T)) continue;
+ list[count] = T;
+ count += 1;
}
return list;
}
@@ -100,16 +103,45 @@ pub fn word(comptime T: type) []const u8 {
return n[dot + 1 ..];
}
+pub const OutputTraits = struct {
+ name: []const u8,
+ steps: bool = false,
+ jumps: bool = false,
+ executes: bool = false,
+};
+
/// The enum: field name = struct name, value = index into `all()`. Everything
/// downstream (leader_path's EnumArray, leader_rows, the topbar's comptime
/// check, stringToEnum) speaks it exactly as it did when it was hand-written.
-pub fn Builtin() type {
- const cmds = all();
- 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));
-}
+///
+/// Registry-dependent APIs live in one namespace so the outer declaration
+/// 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));
+ }
+
+ pub fn takesArg(b: Builtin()) bool {
+ inline for (builtinList(), 0..) |T, i|
+ if (@intFromEnum(b) == i) return @hasDecl(T, "takes_arg") and T.takes_arg;
+ unreachable;
+ }
+
+ pub fn outputTraits(b: Builtin()) ?OutputTraits {
+ inline for (builtinList(), 0..) |T, i|
+ if (@intFromEnum(b) == i) return if (@hasDecl(T, "output")) T.output else null;
+ unreachable;
+ }
+};
// ---- the two acme verbs ----
@@ -128,12 +160,14 @@ pub fn Builtin() type {
// way `Save` on a terminal is inert.
pub const Look = struct {
+ pub const takes_arg = true;
pub fn run(c: Ctx) void {
c.p.lookAt(c.id, c.arg orelse return);
}
};
pub const Exec = struct {
+ pub const takes_arg = true;
pub fn run(c: Ctx) void {
// the destination pane is Look's business (it focuses what answered);
// an execute deliberately leaves you where you were
@@ -157,6 +191,7 @@ pub const Dump = struct {
};
pub const Restore = struct {
+ pub const takes_arg = true;
pub fn run(c: Ctx) void {
var it = std.mem.tokenizeAny(u8, c.txt, " \t");
_ = it.next(); // the word "Restore"
@@ -227,6 +262,7 @@ pub const NextColor = struct {
/// 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| {
@@ -248,6 +284,7 @@ pub const Theme = struct {
/// 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
@@ -265,8 +302,9 @@ pub const Shell = struct {
/// each theme in turn — the picker is the list, and there is nothing to
/// confirm because arriving already applied it.
pub const ThemeSel = struct {
+ pub const output: OutputTraits = .{ .name = config.themes_buffer, .steps = true, .executes = true };
pub fn run(c: Ctx) void {
- output_pane.openThemes(c.p, c.id);
+ output_pane.openThemes(c.p, c.id) catch |err| c.p.reportError(c.id, "themes", err);
}
};
@@ -304,7 +342,11 @@ pub const Crt = struct {
/// — so this asks: the resolved PATH goes in fonts.want, the shell takes it on
/// its next pass and re-rasters. Exactly the shape Restore already has.
pub const Font = struct {
- pub const run = if (pardes.platform == .gui) apply else {};
+ pub const takes_arg = true;
+ pub const enabled = pardes.platform == .gui;
+ 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);
@@ -326,9 +368,13 @@ pub const Font = struct {
/// unreadable one — and this picker EXECUTES what it steps onto, so listing
/// them would mean the list wearing one on the way past. See fonts.monospaced.
pub const FontSel = struct {
- pub const run = if (pardes.platform == .gui) apply else {};
+ pub const output: OutputTraits = .{ .name = config.fonts_buffer, .steps = true, .executes = true };
+ pub const enabled = pardes.platform == .gui;
+ pub fn run(c: Ctx) void {
+ if (comptime enabled) apply(c) else unreachable;
+ }
fn apply(c: Ctx) void {
- output_pane.openFonts(c.p, c.id);
+ output_pane.openFonts(c.p, c.id) catch |err| c.p.reportError(c.id, "fonts", err);
}
};
@@ -338,7 +384,10 @@ pub const FontSel = struct {
/// omits it entirely, so the enum, Help and runtime binary contain no
/// PdfFit.
pub const PdfFit = struct {
- pub const run = if (pardes.pdf_enabled) apply else {};
+ 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);
}
@@ -348,7 +397,10 @@ pub const PdfFit = struct {
/// 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.
pub const PdfTint = struct {
- pub const run = if (pardes.pdf_enabled) apply else {};
+ 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);
}
@@ -358,7 +410,11 @@ pub const PdfTint = struct {
/// command is absent from non-MuPDF builds and deliberately inert on every
/// other pane kind, like the two PDF display toggles above.
pub const PdfSections = struct {
- pub const run = if (pardes.pdf_enabled) apply else {};
+ pub const output: OutputTraits = .{ .name = config.pdf_sections_buffer, .steps = true };
+ 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.openPdfSections(c.id);
}
@@ -474,8 +530,9 @@ pub const Tutor = struct {
};
pub const Help = struct {
+ pub const output: OutputTraits = .{ .name = config.help_buffer };
pub fn run(c: Ctx) void {
- output_pane.openHelp(c.p, c.id, "");
+ output_pane.openHelp(c.p, c.id, "") catch |err| c.p.reportError(c.id, "help", err);
}
};
@@ -488,9 +545,12 @@ pub const Help = struct {
// they arm the same tag input `/` does, and Enter runs it (submitSearch).
pub const Find = struct {
+ pub const takes_arg = true;
+ pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true };
pub fn run(c: Ctx) void {
const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
- if (pat.len > 0) return c.p.runSearch(c.id, pat, .find, .top);
+ if (pat.len > 0) return c.p.runSearch(c.id, pat, .find, .top) catch |err|
+ c.p.reportError(c.id, "find", err);
c.p.startSearch(c.pane, config.find_marker);
}
};
@@ -498,9 +558,12 @@ pub const Find = struct {
/// Find's sibling: Find matches file NAMES under this pane's directory, Grep
/// matches file CONTENTS under every pane's directory at once.
pub const Grep = struct {
+ pub const takes_arg = true;
+ pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true };
pub fn run(c: Ctx) void {
const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
- if (pat.len > 0) return c.p.runSearch(c.id, pat, .grep, .top);
+ if (pat.len > 0) return c.p.runSearch(c.id, pat, .grep, .top) catch |err|
+ c.p.reportError(c.id, "grep", err);
c.p.startSearch(c.pane, config.grep_marker);
}
};
@@ -590,8 +653,9 @@ pub const Last = struct {
/// second list kept in step — the buffer is RENDERED from the stack when you
/// ask, the way +Search is rendered from a walk.
pub const Jumplist = struct {
+ pub const output: OutputTraits = .{ .name = config.jumps_buffer, .steps = true };
pub fn run(c: Ctx) void {
- output_pane.openJumps(c.p, c.id);
+ output_pane.openJumps(c.p, c.id) catch |err| c.p.reportError(c.id, "jumplist", err);
}
};
@@ -649,6 +713,7 @@ pub const WsDiagnostics = struct {
// no argument arms the tag input and Enter submits (submitSearch).
pub const Rename = struct {
+ pub const takes_arg = true;
pub fn run(c: Ctx) void {
const a = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
if (a.len > 0) return c.p.lspRequest(c.id, .rename, a);
@@ -657,6 +722,7 @@ pub const Rename = struct {
};
pub const WsSymbols = struct {
+ pub const takes_arg = true;
pub fn run(c: Ctx) void {
const a = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
if (a.len > 0) return c.p.lspRequest(c.id, .workspace_symbols, a);