From a2e466a874502b665807f620eb8edefb37be1e8c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 13:45:25 -0300 Subject: A ThemeFile that cannot be read or is no theme fails its write, and a bad path says why The theme is read when the host performs the request, after the write had been answered, so a missing file or a bad theme only showed a message; and a path not ending .zon said only invalid theme path. Load failures are now the waiting write's (late, as a Save's), and the path refusal says a theme file is a .zon. Co-Authored-By: Claude Opus 5.5 --- src/builtins.zig | 12 ++++++++---- src/file_watch.zig | 9 ++++++++- test/fs.py | 9 +++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/builtins.zig b/src/builtins.zig index ca01e0fe..ec6afeae 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -808,10 +808,14 @@ pub const ThemeFile = struct { pub const requires_arg = true; pub const enabled = pardes.hosted; pub fn run(c: Ctx) void { - if (comptime enabled) - colors.requestThemeFile(c.p, c.id, c.arg orelse return) - else - unreachable; + if (comptime !enabled) unreachable; + const arg = std.mem.trim(u8, c.arg orelse return, " \t\r\n"); + // Said with why, not "invalid theme path". + if (!std.mem.endsWith(u8, arg, ".zon")) { + var said: [320]u8 = undefined; + return c.p.reportFailure(c.id, std.fmt.bufPrint(&said, "ThemeFile: {s}: invalid theme path: a theme file is a .zon (DumpThemes writes some to start from)", .{arg[0..@min(arg.len, 200)]}) catch "ThemeFile: invalid theme path: a theme file is a .zon"); + } + colors.requestThemeFile(c.p, c.id, arg); } }; diff --git a/src/file_watch.zig b/src/file_watch.zig index 81f4ba87..148bc836 100644 --- a/src/file_watch.zig +++ b/src/file_watch.zig @@ -576,12 +576,19 @@ pub fn applyThemeEffect( watchPath(fd, watches, theme_slot, null, 0, .{ .text = 0 }); if (!on) return false; const request = colors.themeFileRequest(core, generation) orelse return false; + // What fails here is the ThemeFile write's failure too, which waits on + // this (the one-failure rule): late, as a Save's. + const pane = request.pane; const bytes = filesystem.readFile(gpa, request.path) catch |err| { colors.failThemeFile(core, generation, err); + core.noteLateFailure(pane); return false; }; defer gpa.free(bytes); - if (!colors.loadThemeFile(core, generation, bytes, animate)) return false; + if (!colors.loadThemeFile(core, generation, bytes, animate)) { + core.noteLateFailure(pane); + return false; + } const hash = std.hash.Wyhash.hash(0, bytes); watchPath(fd, watches, theme_slot, request.path, generation, .{ .text = hash }); if (watches[theme_slot] == null) return false; diff --git a/test/fs.py b/test/fs.py index a025ec47..8047273d 100644 --- a/test/fs.py +++ b/test/fs.py @@ -836,6 +836,15 @@ def odd_paths(binary): pass assert client.read('/index'), (where, line) client.write('/ctl', b'DumpDir\n') + # A theme file that is not there, or no theme, fails the write. + (root / 'bad.zon').write_bytes(b'.{ .nonsense = 1 }') + for line, why in ((f'ThemeFile {root}/missing.zon', 'theme file'), (f'ThemeFile {root}/bad.zon', 'theme file'), + ('ThemeFile notes.txt', 'a theme file is a .zon')): + try: + client.write('/ctl', (line + '\n').encode()) + raise AssertionError(f'{line} was taken') + except OSError as refused: + assert why in str(refused), (line, refused) def cut_writes(binary): -- cgit v1.3