summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-02 18:17:37 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit35cecf26b06cf50d9827056da99d883ca975c39b (patch)
tree7b228b94313880f2d78fa5f0fb85a0115cfd0e88 /src/pardes.zig
parent4f980de2c7a2ec29c561bca815c37879a18cc2b3 (diff)
downloadpardes-35cecf26b06cf50d9827056da99d883ca975c39b.tar.gz
pardes-35cecf26b06cf50d9827056da99d883ca975c39b.zip
load builtin commands from the user config before rendering
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig118
1 files changed, 95 insertions, 23 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 7b359f0d..6156c56f 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -607,6 +607,47 @@ test "PdfFit builtin and leader path follow the MuPDF feature gate" {
}
}
+test "startup config runs builtin lines in order and isolates bad lines" {
+ const p = try Pardes.init(std.testing.allocator, .{
+ .startup_config =
+ \\Theme dark
+ \\DefinitelyNotABuiltin
+ \\Kill trailing-garbage
+ \\Theme no-such-theme
+ \\Theme acme
+ ,
+ });
+ defer p.deinit();
+
+ // The last valid command wins even after unknown, malformed, and
+ // well-formed-but-failing lines. `Kill trailing-garbage` must not be
+ // accepted as Kill, nor fall through to the shell from config.
+ try std.testing.expectEqualStrings("acme", p.theme().name);
+ try std.testing.expect(!p.quit);
+ for (p.effects.items) |effect| switch (effect) {
+ .write => |w| {
+ try std.testing.expect(std.mem.indexOf(u8, w.bytes.slice(), "DefinitelyNotABuiltin") == null);
+ try std.testing.expect(std.mem.indexOf(u8, w.bytes.slice(), "Kill") == null);
+ },
+ else => {},
+ };
+}
+
+test "startup config overrides restored styling before initFromDump returns" {
+ const source = try Pardes.init(std.testing.allocator, .{ .startup_config = "Theme dark\n" });
+ defer source.deinit();
+ try std.testing.expectEqualStrings("dark", source.theme().name);
+ try source.dumpState();
+
+ const restored = try Pardes.initFromDump(
+ std.testing.allocator,
+ .{ .startup_config = "Theme acme\n" },
+ source.dump_out.?,
+ );
+ defer restored.deinit();
+ try std.testing.expectEqualStrings("acme", restored.theme().name);
+}
+
/// How a `Chord` is SPELLED in the index. The named keys come from Key's OWN
/// declarations rather than a table beside them — a new special key names
/// itself here, and a wrong name is impossible because there is only one. The
@@ -1855,6 +1896,11 @@ pub const Options = struct {
tty_toggle: u21 = config.tty_toggle_default,
/// load a dump of another instance instead of spawning shells (acme -l)
load_path: ?[]const u8 = null,
+ /// Native main fills this with the contents of the per-user config file.
+ /// Keeping discovery out of the core makes constructors and web builds
+ /// deterministic; when present, each line is dispatched as a builtin
+ /// before init returns and therefore before any frontend can render.
+ startup_config: ?[]const u8 = null,
/// Initial grid. Shell contract: for LIVE sessions leave these at the
/// defaults and deliver the real size as the first resize EVENT — the core
/// defers each shell's greeting until after a resize (so `ls` wraps to the
@@ -2035,6 +2081,8 @@ pub const Pardes = struct {
p.col_terms[0][0] = 0;
}
p.sync();
+ p.applyStartupConfig();
+ p.sync();
return p;
}
@@ -8096,16 +8144,44 @@ pub const Pardes = struct {
/// does not.
pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize {
const pane = p.panes[id] orelse return null;
- var cmd = std.mem.trim(u8, txt, " \t\r\n");
- // `` @`ls -la` `` is the same command line wearing the syntax that
- // makes it clickable inside ordinary prose: unwrap and carry on. The
- // loop (rather than one peel) costs nothing and is honest about the
- // form nesting.
- while (config.commandWord(cmd)) |inner| cmd = std.mem.trim(u8, inner, " \t\r\n");
+ const cmd = commandText(txt);
if (cmd.len == 0) return null;
+ if (p.executeBuiltinLine(id, cmd)) return null;
if (p.exec_depth >= max_exec_depth) return null;
p.exec_depth += 1;
defer p.exec_depth -= 1;
+ // Anything not in the builtin vocabulary is a command line typed at
+ // a shell. Startup config calls executeBuiltinLine directly and never
+ // reaches this fallback.
+ // terminal: run in itself. file pane: run in a shell in the file's
+ // directory (found by cwd, else freshly forked).
+ const dst = (if (pane.file) |f|
+ p.ttyForDir(std.fs.path.dirname(f.path) orelse "/")
+ else if (hasPdf(pane))
+ p.ttyForDir(paneDir(pane))
+ else
+ id) orelse return null;
+ term_pane.padOutputBelowEdits(p, dst);
+ p.emitWrite(dst, cmd);
+ p.emitWrite(dst, "\r");
+ return dst;
+ }
+
+ /// Trim and unwrap clickable command-word notation once for every command
+ /// consumer, so startup lines and interactive Exec speak the same syntax.
+ fn commandText(txt: []const u8) []const u8 {
+ var cmd = std.mem.trim(u8, txt, " \t\r\n");
+ while (config.commandWord(cmd)) |inner| cmd = std.mem.trim(u8, inner, " \t\r\n");
+ return cmd;
+ }
+
+ /// Parse and dispatch exactly one builtin command. This is the canonical
+ /// builtin path used both by ordinary Exec and by startup configuration;
+ /// unlike execute(), it deliberately has no external-shell fallback.
+ /// False means blank, malformed, unknown, or recursion-limited.
+ pub fn executeBuiltinLine(p: *Pardes, id: usize, txt: []const u8) bool {
+ const cmd = commandText(txt);
+ if (cmd.len == 0 or p.exec_depth >= max_exec_depth) return false;
// The builtins that take an ARGUMENT match their name with a TAIL:
// `Restore <path>`, `Find <pat>`, `Grep <pat>`, `Rename <name>`,
// `WsSymbols <sym>`, `Theme <name>`, `Font <name>` (gui only), and the
@@ -8128,23 +8204,17 @@ pub const Pardes = struct {
},
};
};
- if (bi) |b| {
- const typed = if (sp) |s| std.mem.trim(u8, cmd[s + 1 ..], " \t") else "";
- p.runBuiltin(b, id, cmd, if (typed.len > 0) typed else null);
- return null;
- }
- // terminal: run in itself. file pane: run in a shell in the file's
- // directory (found by cwd, else freshly forked).
- const dst = (if (pane.file) |f|
- p.ttyForDir(std.fs.path.dirname(f.path) orelse "/")
- else if (hasPdf(pane))
- p.ttyForDir(paneDir(pane))
- else
- id) orelse return null;
- term_pane.padOutputBelowEdits(p, dst);
- p.emitWrite(dst, cmd);
- p.emitWrite(dst, "\r");
- return dst;
+ const b = bi orelse return false;
+ p.exec_depth += 1;
+ defer p.exec_depth -= 1;
+ const typed = if (sp) |s| std.mem.trim(u8, cmd[s + 1 ..], " \t") else "";
+ p.runBuiltin(b, id, cmd, if (typed.len > 0) typed else null);
+ return true;
+ }
+
+ fn applyStartupConfig(p: *Pardes) void {
+ var lines = std.mem.splitScalar(u8, p.opts.startup_config orelse return, '\n');
+ while (lines.next()) |line| _ = p.executeBuiltinLine(p.active, line);
}
/// Run a builtin on pane `id`. `txt` is the executed text (Restore reads
@@ -8508,6 +8578,8 @@ pub const Pardes = struct {
}
p.active = @min(st.active, MAX_PANES - 1);
p.sync();
+ p.applyStartupConfig();
+ p.sync();
return p;
}