summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 05:04:57 -0300
committerGabriel Schneider <[email protected]>2026-10-01 05:18:09 -0300
commit51ddaaf3917ad7968df877d10580d1eadb926688 (patch)
treebedad9f6f2c1b04a34c76599ed126fc61b2b5ac9 /src
parent250db41743f77ce3d8421f729bdea0065aa6bc79 (diff)
downloadpardes-51ddaaf3917ad7968df877d10580d1eadb926688.tar.gz
pardes-51ddaaf3917ad7968df877d10580d1eadb926688.zip
Restore and ThemeFile failures say why in words, a ZON one with its line: never FileTooLarge, ParseZon or a bare `parse zon`
Restore read failures named the Zig error (FileTooLarge, NotAFile, ReadFailed), a file that is no dump said `not a pardes dump (ParseZon)`, and a ThemeFile that did not parse said `parse zon`. Messages.errorWords says an error in words (the few that read badly spelled out plainly, the rest spaced and lower case, as reportError did), and zonWhy gives the parser's first error with its line: `not a pardes dump: line 1: expected ...`, `theme file x.zon: line 3: ...`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/Messages.zig48
-rw-r--r--src/builtins.zig11
-rw-r--r--src/colors.zig18
-rw-r--r--src/dump.zig13
-rw-r--r--src/ninep/ctl.zig2
5 files changed, 78 insertions, 14 deletions
diff --git a/src/Messages.zig b/src/Messages.zig
index a316377a..366445c7 100644
--- a/src/Messages.zig
+++ b/src/Messages.zig
@@ -486,13 +486,53 @@ pub fn reportError(p: *Pardes, id: usize, operation: []const u8, err: anyerror)
p.fs.no_pane_slot_len = @intCast(w.buffered().len);
return reportFailure(p, id, w.buffered());
}
- for (@errorName(err), 0..) |c, i| {
- if (std.ascii.isUpper(c) and i > 0) w.writeByte(' ') catch break;
- w.writeByte(std.ascii.toLower(c)) catch break;
- }
+ var words: [128]u8 = undefined;
+ w.writeAll(errorWords(err, &words)) catch {};
reportFailure(p, id, w.buffered());
}
+/// An error in a person's words, never its Zig name: the few that read
+/// badly spaced out said plainly, the rest spaced and lower case
+/// (`FileNotFound`, "file not found").
+pub fn errorWords(err: anyerror, buf: []u8) []const u8 {
+ return switch (err) {
+ error.FileTooLarge => "too large",
+ error.NotAFile => "not a regular file",
+ error.ReadFailed => "could not be read",
+ error.ParseZon => "not valid ZON",
+ error.ThemeFileTooLarge => "too large: a theme file is at most 1 MiB",
+ error.InvalidThemeName => "its name is empty or over 255 bytes",
+ else => {
+ var w = std.Io.Writer.fixed(buf);
+ for (@errorName(err), 0..) |c, i| {
+ if (std.ascii.isUpper(c) and i > 0) w.writeByte(' ') catch break;
+ w.writeByte(std.ascii.toLower(c)) catch break;
+ }
+ return w.buffered();
+ },
+ };
+}
+
+/// A ZON parse failure in a person's words: the first error the parser
+/// found and its line, `line 3: expected ','`.
+pub fn zonWhy(diag: *const std.zon.parse.Diagnostics, buf: []u8) []const u8 {
+ var errors = diag.iterateErrors();
+ const first = errors.next() orelse return "not valid ZON";
+ const loc = first.getLocation(diag);
+ return std.fmt.bufPrint(buf, "line {d}: {f}", .{ loc.line + 1, first.fmtMessage(diag) }) catch "not valid ZON";
+}
+
+test "errors are said in words, and a ZON failure names its line" {
+ var buf: [128]u8 = undefined;
+ try std.testing.expectEqualStrings("file not found", errorWords(error.FileNotFound, &buf));
+ try std.testing.expectEqualStrings("too large", errorWords(error.FileTooLarge, &buf));
+ var diag: std.zon.parse.Diagnostics = .{};
+ defer diag.deinit(std.testing.allocator);
+ const T = struct { a: u8 };
+ try std.testing.expectError(error.ParseZon, std.zon.parse.fromSliceAlloc(T, std.testing.allocator, ".{\n .a = 1,\n .b = 2 }", &diag, .{}));
+ try std.testing.expect(std.mem.startsWith(u8, zonWhy(&diag, &buf), "line 3: "));
+}
+
/// The longest start of `text` that fits `max` bytes and ends between
/// words (or, with no space to cut at, between characters).
pub fn clip(text: []const u8, max: usize) []const u8 {
diff --git a/src/builtins.zig b/src/builtins.zig
index f51409aa..55779c8a 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -715,19 +715,22 @@ pub const Restore = struct {
if (comptime pardes.hosted) {
const bytes = @import("fs.zig").readRestore(c.p.gpa, path, c.p.settings.dump_dir.get()) catch |err| {
var buf: [limits.host_path_cap + 64]u8 = undefined;
+ var words: [128]u8 = undefined;
const why = switch (err) {
error.FileNotFound => "no such file",
error.PermissionDenied => "permission denied",
error.IsDirectory => "a directory, not a dump",
- else => @errorName(err),
+ else => pardes.Messages.errorWords(err, &words),
};
return c.p.reportFailure(c.id, std.fmt.bufPrint(&buf, "Restore: {s}: {s}", .{ path, why }) catch "Restore: cannot read the dump");
};
defer c.p.gpa.free(bytes);
// Nor does a file that is not a dump, before any warning.
- pardes.dump.check(c.p.gpa, bytes) catch |err| {
- var buf: [limits.host_path_cap + 64]u8 = undefined;
- return c.p.reportFailure(c.id, std.fmt.bufPrint(&buf, "Restore: {s}: not a pardes dump ({s})", .{ path, @errorName(err) }) catch "Restore: not a pardes dump");
+ var why_buf: [256]u8 = undefined;
+ pardes.dump.check(c.p.gpa, bytes, &why_buf) catch |err| {
+ var buf: [limits.host_path_cap + 320]u8 = undefined;
+ const why = if (err == error.ParseZon) why_buf[0..std.mem.indexOfScalar(u8, &why_buf, 0).?] else pardes.Messages.errorWords(err, &why_buf);
+ return c.p.reportFailure(c.id, std.fmt.bufPrint(&buf, "Restore: {s}: not a pardes dump: {s}", .{ path, why }) catch "Restore: not a pardes dump");
};
}
// acme's Load adds a dump's windows to the ones there; a Restore
diff --git a/src/colors.zig b/src/colors.zig
index d04c009e..a45184c8 100644
--- a/src/colors.zig
+++ b/src/colors.zig
@@ -766,10 +766,15 @@ pub fn failThemeFile(p: *Pardes, generation: u32, err: anyerror) void {
/// A theme file's failure, said with its path: `theme file x.zon: file
/// not found`, not which file left to guess.
fn themeFileError(p: *Pardes, request: ThemeFileRequest, err: anyerror) void {
- var said: [300]u8 = undefined;
+ var words: [128]u8 = undefined;
+ themeFileFailure(p, request, pardes.Messages.errorWords(err, &words));
+}
+
+fn themeFileFailure(p: *Pardes, request: ThemeFileRequest, why: []const u8) void {
+ var said: [520]u8 = undefined;
var cut: [200]u8 = undefined;
const path = @import("ninep/tree.zig").fitErr(request.path, &cut);
- p.reportError(request.pane, std.fmt.bufPrint(&said, "theme file {s}", .{path}) catch "theme file", err);
+ p.reportFailure(request.pane, std.fmt.bufPrint(&said, "theme file {s}: {s}", .{ path, why }) catch "theme file: not loaded");
revertThemeFile(p);
}
@@ -802,7 +807,11 @@ pub fn loadThemeFile(p: *Pardes, generation: u32, bytes: []const u8, animate: bo
var diagnostics: std.zon.parse.Diagnostics = .{};
defer diagnostics.deinit(p.gpa);
const parsed = std.zon.parse.fromSliceAlloc(Theme, p.gpa, source, &diagnostics, .{}) catch |err| {
- themeFileError(p, request, err);
+ if (err != error.ParseZon) themeFileError(p, request, err) else {
+ // Which line, and what the parser wanted there.
+ var why: [200]u8 = undefined;
+ themeFileFailure(p, request, pardes.Messages.zonWhy(&diagnostics, &why));
+ }
return false;
};
if (parsed.name.len == 0 or parsed.name.len > 255) {
@@ -983,6 +992,9 @@ test "ThemeFile parses ZON atomically and Theme returns to the compiled ring" {
// A half-written save is rejected without disturbing the last good value.
try std.testing.expect(!loadThemeFile(p, request_effect.generation, ".{ .name = ", true));
+ // Said with its line, in words: never a bare `parse zon`.
+ const said = p.panes[0].?.msg[0..p.panes[0].?.msg_len];
+ try std.testing.expect(std.mem.indexOf(u8, said, ": line 1: ") != null and std.mem.indexOf(u8, said, "parse zon") == null);
try std.testing.expectEqualStrings("mine", p.theme().name);
try std.testing.expectEqual(@as(?[3]u8, .{ 1, 2, 3 }), p.theme().bg);
diff --git a/src/dump.zig b/src/dump.zig
index f8b99ef4..e76bb1a2 100644
--- a/src/dump.zig
+++ b/src/dump.zig
@@ -396,11 +396,20 @@ pub const Parsed = struct {
/// Whether `bytes` are a dump a Restore could load, saying nothing: a
/// Restore asks before it warns about unsaved text or replaces anything.
-pub fn check(gpa: std.mem.Allocator, bytes: []const u8) !void {
+/// Whether `bytes` are a dump; a ZON failure's line and reason are left in
+/// `why`, ended by a 0.
+pub fn check(gpa: std.mem.Allocator, bytes: []const u8, why: []u8) !void {
var arena: std.heap.ArenaAllocator = .init(gpa);
defer arena.deinit();
const source = try arena.allocator().dupeZ(u8, bytes);
- try validate(try std.zon.parse.fromSliceAlloc(State, arena.allocator(), source, null, .{ .free_on_error = false }));
+ var diag: std.zon.parse.Diagnostics = .{};
+ const state = std.zon.parse.fromSliceAlloc(State, arena.allocator(), source, &diag, .{ .free_on_error = false }) catch |err| {
+ const said = pardes.Messages.zonWhy(&diag, why[0 .. why.len - 1]);
+ if (said.ptr != why.ptr) @memcpy(why[0..said.len], said);
+ why[said.len] = 0;
+ return err;
+ };
+ try validate(state);
}
pub fn readZon(gpa: std.mem.Allocator, bytes: []const u8, label: []const u8) !Parsed {
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index d6d7a6ee..7d8a370d 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -2287,7 +2287,7 @@ test "a Restore of a file that is no dump fails the write before any warning, an
const root_ctl = @intFromEnum(tree.TopFile.ctl);
const refused = wr(p, root_ctl, line);
try testing.expectEqual(E.IO, refused.errno());
- try testing.expect(std.mem.indexOf(u8, refused.reply.ename, "not a pardes dump") != null);
+ try testing.expect(std.mem.indexOf(u8, refused.reply.ename, "not a pardes dump: line 1: ") != null);
try testing.expect(p.restore_req == null);
try testing.expect(p.panes[0].?.discard_warned == null);