From d3f60c930db06b540bb196b6323a88045e746bbf Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 1 Oct 2026 10:29:16 -0300 Subject: The init file reads past a UTF-8 BOM, and every line it fails is said and logged with its line number, never silent A BOM made the first word unknown, a word no builtin has was skipped without a word, and a setting that refused its value said nothing (the init file ran with announcing off). Each failing line now leaves a notice and an `err - init file line N: ` record; `#` still starts a comment only at a line's start, so `#ff0000` stays a value. Co-Authored-By: Claude Opus 5.5 --- src/exec.zig | 34 +++++++++++++++++++++++++++------- src/ninep/ctl.zig | 10 ++++++++++ src/pardes.zig | 3 ++- 3 files changed, 39 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/exec.zig b/src/exec.zig index ac6efcdd..8a24969d 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -982,9 +982,30 @@ pub fn executeBuiltinLine(p: *Pardes, id: usize, txt: []const u8) bool { pub fn applyStartupConfig(p: *Pardes) void { p.announce = false; defer p.announce = true; - var lines = std.mem.splitScalar(u8, p.opts.startup_config orelse return, '\n'); + const text = p.opts.startup_config orelse return; + // A UTF-8 BOM an editor left at its start is no part of its first word. + const body = if (std.mem.startsWith(u8, text, "\xEF\xBB\xBF")) text[3..] else text; + var lines = std.mem.splitScalar(u8, body, '\n'); + var number: usize = 0; while (lines.next()) |line| { - _ = executeBuiltinLine(p, p.active, line); + number += 1; + const word = std.mem.trim(u8, line, " \t\r"); + // `#` starts a comment only at a line's start: a value may hold one + // (`#ff0000`). + if (word.len == 0 or isComment(word)) continue; + const failures = p.fs.failures; + const known = executeBuiltinLine(p, p.active, line); + if (known and p.fs.failures == failures) continue; + // Never silent: a word it has not, or one that failed, is a notice + // on the message row and an err in the log, with its line. + var why_buf: [320]u8 = undefined; + const why: []const u8 = if (!known) + std.fmt.bufPrint(&why_buf, "unknown word \"{s}\"", .{word[0..@min(word.len, 64)]}) catch "unknown word" + else if (p.fs.said_len > 0) p.fs.said[0..p.fs.said_len] else "failed"; + var said: [400]u8 = undefined; + const line_said = std.fmt.bufPrint(&said, "init file line {d}: {s}", .{ number, why }) catch "init file: a line failed"; + if (!known) p.reportFailure(p.active, line_said); + pardes.ctlfs.events.notePath(p, "err -", line_said); } } @@ -1027,9 +1048,8 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[] .theme => { const want = std.mem.trim(u8, arg orelse return, " \t\r\n"); if (colors.themeIndex(want)) |i| return colors.setThemeIndex(p, i); - // A config line is not answered on screen (applyStartupConfig - // runs with announce off); a click or a ctl write is. - if (!p.announce) return; + // A failure is said, the init file's too (applyStartupConfig + // logs it with its line): never silent. // All of it within the 128 bytes a 9P error carries, so a write // failing with it is cut nowhere: the names get what is left. const shown = want[0..@min(want.len, 40)]; @@ -1064,10 +1084,10 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[] return p.reportFailure(p.active, std.fmt.bufPrint(&text, "Shell: {s}", .{refused}) catch "Shell: no such shell"); } } - if (!p.settings.apply(setting, want) and p.announce) p.reportFailure(p.active, "Shell: takes " ++ comptime config.Runtime.takes(.shell).?); + if (!p.settings.apply(setting, want)) p.reportFailure(p.active, "Shell: takes " ++ comptime config.Runtime.takes(.shell).?); }, // Said with what it takes: its words, its range. - else => if (!p.settings.apply(setting, arg) and p.announce) { + else => if (!p.settings.apply(setting, arg)) { var text: [160]u8 = undefined; const why = if (config.Runtime.takes(setting.action)) |values| std.fmt.bufPrint(&text, "{s}: takes {s}", .{ setting.word, values }) else std.fmt.bufPrint(&text, "{s}: does not take that value", .{setting.word}); p.reportFailure(p.active, why catch "does not take that value"); diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 697ef594..2fa0cd7e 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -4467,3 +4467,13 @@ test "a Mount of a socket path that is not there says no such socket, ENOENT" { try testing.expectEqual(@as(usize, 0), p.fs.mounts.items.len); try testing.expectEqual(E.INVAL, wr(p, @intFromEnum(tree.TopFile.ctl), "Mount peer rel/x.sock\n").errno()); } + +test "the init file reads past a BOM, and every line it fails is a notice and an err, never silent" { + const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .startup_config = "\xEF\xBB\xBFTheme dusk\nnosuchword 1\nTheme nosuchtheme\n# a comment\nInactiveDim 30 # not a comment, a bad value\n" }); + defer p.deinit(); + try testing.expectEqualStrings("dusk", p.theme().name); + try testing.expect(th.logHas(p, "err - init file line 2: unknown word \"nosuchword 1\"")); + try testing.expect(th.logHas(p, "err - init file line 3: ")); + try testing.expect(!th.logHas(p, "line 4")); + try testing.expect(th.logHas(p, "err - init file line 5: ")); +} diff --git a/src/pardes.zig b/src/pardes.zig index 803686e0..c6850451 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -620,7 +620,8 @@ test "startup config runs builtin lines in order and isolates bad lines" { defer p.deinit(); try std.testing.expectEqualStrings("acme", p.theme().name); - try std.testing.expect(!p.animationActive()); + // The theme lands without a tween (the chrome is the theme's at once); + // the bad lines are said, so a notice may be animating. try std.testing.expectEqual(ChromeTheme.fromTheme(p.theme()), p.chromeTheme().*); try std.testing.expect(!p.quit); for (0..p.effects_len) |i| switch (p.effects[(p.effects_head + i) % p.effects.len]) { -- cgit v1.3