From 238e35e89d387f3c266b40f107e9419ea75d77bb Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 13:31:20 -0300 Subject: Joincol with no column to the right and Theme with no such theme say so Both did nothing without a word, from a click or a ctl write alike, and a script could not tell a no-op from success. Joincol in the rightmost column now fails 'Joincol: no column to the right'. A Theme name that is none says so and names the themes sharing its first letter, since all of them (ThemeSel lists them) are too many for a message or a refused write; a startup config line still fails quietly, as its other bad lines do. Co-Authored-By: Claude Opus 5.5 --- src/builtins.zig | 3 +++ src/colors.zig | 13 +++++++++++++ src/exec.zig | 6 ++++++ src/ninep/ctl.zig | 30 ++++++++++++++++++++++++++++-- 4 files changed, 50 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/builtins.zig b/src/builtins.zig index 6bfc9b26..d6b89cf2 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -806,6 +806,9 @@ pub const Tty9p = struct { pub const Joincol = struct { pub const scope: Scope = .session; pub fn run(c: Ctx) void { + // The keyboard's column joins the one to its right; the last has none. + const at = layout.findPane(c.p, c.p.active) orelse return; + if (at.col + 1 >= c.p.ncol) return c.p.reportFailure(c.id, "Joincol: no column to the right"); layout.joinCol(c.p); } }; diff --git a/src/colors.zig b/src/colors.zig index bb366fc0..8e608590 100644 --- a/src/colors.zig +++ b/src/colors.zig @@ -316,6 +316,19 @@ fn invalidateThemeDependentRasters(p: *Pardes) void { }; } +/// Themes whose names share `name`'s first letter, a line of them, for a +/// Theme given a name there is none of: all of them (ThemeSel) are too many +/// for a message or a refused write. +pub fn themesNear(buf: []u8, name: []const u8) []const u8 { + var w = std.Io.Writer.fixed(buf); + for (themes) |t| { + if (name.len > 0 and std.ascii.toLower(t.name[0]) != std.ascii.toLower(name[0])) continue; + if (w.buffered().len + t.name.len + 1 > buf.len) break; + w.print("{s}{s}", .{ if (w.buffered().len > 0) " " else "", t.name }) catch break; + } + return w.buffered(); +} + pub fn setThemeIndex(p: *Pardes, index: usize) void { if (index >= themes.len or (p.custom_theme == null and p.theme_file_path.get().len == 0 and diff --git a/src/exec.zig b/src/exec.zig index b16c600c..2faf84de 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -467,6 +467,12 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[] colors.setThemeIndex(p, i); return; }; + // A config line is not answered on screen (applyStartupConfig + // runs with announce off); a click or a ctl write is. + if (!p.announce) return; + var near: [96]u8 = undefined; + var text: [200]u8 = undefined; + return p.reportFailure(p.active, std.fmt.bufPrint(&text, "Theme: no theme \"{s}\"; like it: {s}; ThemeSel lists all", .{ want[0..@min(want.len, 40)], colors.themesNear(&near, want) }) catch "Theme: no such theme"); }, .font => { if (comptime !font_picker) return; diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 386fd9a9..1ba78457 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -213,7 +213,12 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ? const takes = switch (setting.action) { .theme => for (pardes.themes) |t| { if (std.mem.eql(u8, t.name, arg)) break true; - } else false, + } else { + // `refuse` quotes the line after it, in what is left of 128. + var near: [40]u8 = undefined; + var why: [104]u8 = undefined; + return refuse(p, req, std.fmt.bufPrint(&why, "bad value in control message; like it: {s}; ThemeSel lists all", .{pardes.colors.themesNear(&near, arg)}) catch "bad value in control message", line); + }, .font => config.Runtime.FontSpec.parse(arg) != null, else => probe: { var probe = p.settings; @@ -638,7 +643,9 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi try testing.expectEqualStrings("not a session control message \"Del\"", wr(p, root_ctl, "Del").reply.ename); try testing.expectEqualStrings("unknown control message \"Nonsense 1\"", wr(p, root_ctl, "Nonsense 1").reply.ename); try testing.expectEqualStrings("bad value in control message \"Verbose maybe\"", wr(p, root_ctl, "Verbose maybe").reply.ename); - try testing.expectEqualStrings("bad value in control message \"Theme no-such-theme\"", wr(p, root_ctl, "Theme no-such-theme").reply.ename); + const no_theme = wr(p, root_ctl, "Theme no-such-theme").reply.ename; + try testing.expectStringStartsWith(no_theme, "bad value in control message; like it: n"); + try testing.expect(std.mem.endsWith(u8, no_theme, "; ThemeSel lists all \"Theme no-such-theme\"")); try testing.expectEqualStrings("wrong #args in control message \"Newcol 2\"", wr(p, root_ctl, "Newcol 2").reply.ename); try testing.expectEqualStrings("wrong #args in control message \"Theme\"", wr(p, root_ctl, "Theme").reply.ename); // A bare required word fails the check, so the line before never runs. @@ -951,3 +958,22 @@ test "an error's words give the errno a mount reads: EINVAL for what is malforme for (other) |w| try testing.expect(std.ascii.findIgnoreCase(c.text, w) == null); } } + +test "Joincol with no column to the right and Theme with no such theme say so" { + const gpa = testing.allocator; + const p = try withFile(gpa, "x\n"); + defer p.deinit(); + const root_ctl = @intFromEnum(tree.TopFile.ctl); + const joined = wr(p, root_ctl, "Joincol\n"); + try testing.expectEqual(tree.Status.err, joined.reply.status); + try testing.expectStringStartsWith(joined.reply.ename, "Joincol: no column to the right"); + const themed = wr(p, root_ctl, "Theme drak\n"); + try testing.expectEqual(E.INVAL, themed.errno()); + try testing.expectStringStartsWith(themed.reply.ename, "bad value in control message; like it: "); + try testing.expect(std.mem.indexOf(u8, themed.reply.ename, " dark") != null or std.mem.indexOf(u8, themed.reply.ename, ": dark") != null); + try testing.expect(std.mem.endsWith(u8, themed.reply.ename, "\"Theme drak\"")); + // A click on the word says it on the message row. + _ = wr(p, Node.of(serialOf(p), .exec), "Theme drak\n"); + const pane = p.panes[p.active].?; + try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "no theme \"drak\"") != null); +} -- cgit v1.3