summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 13:31:20 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit238e35e89d387f3c266b40f107e9419ea75d77bb (patch)
treed531d6aaa608c4e1f025969e4d4a1428efce3e70
parent917e9a099f3a5048ac436aafcfabbf3d40d1c24f (diff)
downloadpardes-238e35e89d387f3c266b40f107e9419ea75d77bb.tar.gz
pardes-238e35e89d387f3c266b40f107e9419ea75d77bb.zip
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 <[email protected]>
-rw-r--r--docs/fs.md4
-rw-r--r--src/builtins.zig3
-rw-r--r--src/colors.zig13
-rw-r--r--src/exec.zig6
-rw-r--r--src/ninep/ctl.zig30
5 files changed, 53 insertions, 3 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 4673e042..41b960f2 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -146,7 +146,9 @@ in Plan 9's words for a ctl (kernel/misc/parse.c:82-97), quoting the line:
`unknown control message "X"`; `wrong #args in control message "X"` for an
argument to a builtin that takes none, or none to one that needs it (`Msg`,
`Mount`, `Find`, a setting's value but a switch's, which flips bare);
-`bad value in control message "X"` for a setting's value it does not take;
+`bad value in control message "X"` for a setting's value it does not take
+(for `Theme`, naming the themes that share the name's first letter, since
+all of them, `ThemeSel`'s list, are too many for an error);
and `not a session control message "X"` or `not a window control message
"X"` for a word of the other ctl. 9ns maps them all to EINVAL, and a write
refused here has done nothing. A line that then fails as it runs fails the
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);
+}