From 35cecf26b06cf50d9827056da99d883ca975c39b Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 2 Aug 2026 18:17:37 -0300 Subject: load builtin commands from the user config before rendering --- src/pardes.zig | 118 ++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 23 deletions(-) (limited to 'src/pardes.zig') 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 `, `Find `, `Grep `, `Rename `, // `WsSymbols `, `Theme `, `Font ` (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; } -- cgit v1.3