summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/colors.zig52
-rw-r--r--src/exec.zig9
-rw-r--r--src/ninep/ctl.zig40
3 files changed, 83 insertions, 18 deletions
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" {