diff options
| -rw-r--r-- | docs/fs.md | 5 | ||||
| -rw-r--r-- | src/colors.zig | 52 | ||||
| -rw-r--r-- | src/exec.zig | 9 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 40 |
4 files changed, 86 insertions, 20 deletions
@@ -279,8 +279,9 @@ in Plan 9's words for a ctl (kernel/misc/parse.c:82-97), quoting the line: 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 -(for `Theme`, naming the themes that share the name's first letter, since -all of them, `Themes`'s list, are too many for an error); +(for `Theme`, whose names are any case, naming every theme that shares +the name's first letter when they fit the 128 bytes a 9P error carries, +else the nearest few, since all of them, `Themes`'s list, are too many); and `not a session control message "X": write it to pane/<n>/ctl` or `not a window control message "X": write it to /ctl` 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/colors.zig b/src/colors.zig index da074765..aa69fe6d 100644 --- a/src/colors.zig +++ b/src/colors.zig @@ -711,19 +711,63 @@ 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 (Themes) are too many -/// for a message or a refused write. +/// Themes near `name`, a line of them, for a Theme given a name there is +/// none of (all of them, Themes, are too many for a refused write): every +/// one sharing its first letter when they fit in `buf`, else the 8 nearest. 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; + } else if (w.buffered().len > 0) return w.buffered(); + // The nearest, most alike first: ponytail, a plain score (shared + // prefix, the name inside, its letters in order), no edit distance. + var best: [8]usize = undefined; + var scores: [8]usize = undefined; + var n: usize = 0; + for (themes, 0..) |t, i| { + const s = likeness(t.name, name); + var at = n; + while (at > 0 and scores[at - 1] < s) at -= 1; + if (at == best.len) continue; + const last = @min(n, best.len - 1); + var k = last; + while (k > at) : (k -= 1) { + best[k] = best[k - 1]; + scores[k] = scores[k - 1]; + } + best[at] = i; + scores[at] = s; + n = @min(n + 1, best.len); } + w = std.Io.Writer.fixed(buf); + for (best[0..n]) |i| w.print("{s}{s}", .{ if (w.buffered().len > 0) " " else "", themes[i].name }) catch break; return w.buffered(); } +fn likeness(theme_name: []const u8, name: []const u8) usize { + var prefix: usize = 0; + while (prefix < @min(theme_name.len, name.len) and std.ascii.toLower(theme_name[prefix]) == std.ascii.toLower(name[prefix])) prefix += 1; + // Its letters found in order, one not there passed over. + var in_order: usize = 0; + var pos: usize = 0; + for (name) |c| { + const at = for (theme_name[pos..], pos..) |t, i| { + if (std.ascii.toLower(t) == std.ascii.toLower(c)) break i; + } else continue; + in_order += 1; + pos = at + 1; + } + const inside: usize = if (name.len > 0 and std.ascii.indexOfIgnoreCase(theme_name, name) != null) name.len * 4 else 0; + return prefix * 3 + in_order + inside; +} + +/// The theme named `name`, whatever its case. +pub fn themeIndex(name: []const u8) ?usize { + for (themes, 0..) |t, i| if (std.ascii.eqlIgnoreCase(t.name, name)) return i; + return null; +} + 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 683b2f04..9ec6a426 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -906,15 +906,12 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[] switch (setting.action) { .theme => { const want = std.mem.trim(u8, arg orelse return, " \t\r\n"); - for (themes, 0..) |t, i| if (std.mem.eql(u8, t.name, want)) { - colors.setThemeIndex(p, i); - return; - }; + 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; - var near: [96]u8 = undefined; - var text: [200]u8 = undefined; + var near: [170]u8 = undefined; + var text: [256]u8 = undefined; return p.reportFailure(p.active, std.fmt.bufPrint(&text, "Theme: no theme \"{s}\"; like it: {s}; Themes lists all", .{ want[0..@min(want.len, 40)], colors.themesNear(&near, want) }) catch "Theme: no such theme"); }, .font => { diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index ea0a0bc6..68b62186 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -408,13 +408,15 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ? if (arg.len == 0 and builtins.registry.requiresArg(b)) return refuse(p, req, "wrong #args in control message", line); const setting = config.Runtime.find(word) orelse return null; const takes = switch (setting.action) { - .theme => for (pardes.themes) |t| { - if (std.mem.eql(u8, t.name, arg)) break true; - } 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}; Themes lists all", .{pardes.colors.themesNear(&near, arg)}) catch "bad value in control message", line); + .theme => if (pardes.colors.themeIndex(arg) != null) true else { + // `refuse` quotes the line after it, all in the 128 bytes an + // Rerror carries: the names get what is left. + const head = "bad value in control message; like it: "; + const tail = "; Themes lists all"; + var near: [128]u8 = undefined; + const room = @import("cloud9").fs.errmax -| (head.len + tail.len + line.len + 3); + var why: [256]u8 = undefined; + return refuse(p, req, std.fmt.bufPrint(&why, head ++ "{s}" ++ tail, .{pardes.colors.themesNear(near[0..@min(room, near.len)], arg)}) catch "bad value in control message", line); }, .font => config.Runtime.FontSpec.parse(arg) != null, else => probe: { @@ -1614,12 +1616,34 @@ test "Joincol with no column to the right and Theme with no such theme say so" { 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.indexOf(u8, themed.reply.ename, ": dr") != 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); + // Within the 128 bytes an Rerror carries, the quoted line kept. + try testing.expect(themed.reply.ename.len <= 128); + // A name in any case is the theme. + try testing.expectEqual(Status.ok, wr(p, root_ctl, "Theme DRACULA\n").reply.status); + try testing.expectEqualStrings("dracula", pardes.themes[p.settings.theme].name); + // A misspelling far from any first letter's list still finds its like. + var near: [170]u8 = undefined; + try testing.expect(std.mem.indexOf(u8, pardes.colors.themesNear(&near, "9dracula"), "dracula") != null); + // Every theme sharing the first letter, when they fit. + var tested = false; + for ("abcdefghijklmnopqrstuvwxyz") |letter| { + var count: usize = 0; + var len: usize = 0; + for (pardes.themes) |t| if (t.name[0] == letter) { + count += 1; + len += t.name.len + 1; + }; + if (count < 2 or len > near.len) continue; + try testing.expectEqual(count, std.mem.count(u8, pardes.colors.themesNear(&near, &.{letter}), " ") + 1); + tested = true; + } + try testing.expect(tested); } test "a look after Newcol fills the empty column the keyboard is on" { |
