summaryrefslogtreecommitdiff
path: root/src/ninep/ctl.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/ctl.zig')
-rw-r--r--src/ninep/ctl.zig158
1 files changed, 152 insertions, 6 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 118b6488..75a7eb9a 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -491,8 +491,8 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ?
},
};
if (takes) return null;
- var why: [104]u8 = undefined;
- return refuse(p, req, if (config.Runtime.choices(setting.action)) |values|
+ var why: [160]u8 = undefined;
+ return refuse(p, req, if (config.Runtime.takes(setting.action)) |values|
std.fmt.bufPrint(&why, "bad value in control message; takes {s}", .{values}) catch "bad value in control message"
else
"bad value in control message", line);
@@ -554,7 +554,9 @@ pub fn writeSettingsOf(p: *Pardes, s: *const config.Runtime, w: *std.Io.Writer)
const word = setting.word;
switch (setting.action) {
.toggle => |field| try w.print("{s} {s}\n", .{ word, if (@field(s, @tagName(field))) "on" else "off" }),
- .theme => try w.print("{s} {s}\n", .{ word, if (s == &p.settings) p.theme().name else pardes.themes[s.theme].name }),
+ .theme => if (s == &p.settings) {
+ if (pardes.colors.loadedThemeFile(p)) |path| try w.print("ThemeFile {s}\n", .{path}) else try w.print("{s} {s}\n", .{ word, p.theme().name });
+ } else try w.print("{s} {s}\n", .{ word, pardes.themes[s.theme].name }),
.shell => {
const chosen = s.shell.requested.get();
try w.print("{s} {s}\n", .{ word, if (chosen.len > 0) chosen else config.defaultShell() });
@@ -1254,6 +1256,11 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi
try testing.expectEqualStrings("unknown control message \"Nonsense 1\"", wr(p, root_ctl, "Nonsense 1").reply.ename);
try testing.expectEqualStrings("bad value in control message; takes on, off \"Verbose maybe\"", wr(p, root_ctl, "Verbose maybe").reply.ename);
try testing.expectEqualStrings("bad value in control message; takes acme, pardes \"Placement east\"", wr(p, root_ctl, "Placement east").reply.ename);
+ // A number's refusal names its range, a path's what it takes.
+ try testing.expectEqualStrings("bad value in control message; takes 0-100 (a percentage) \"InactiveDim 200\"", wr(p, root_ctl, "InactiveDim 200").reply.ename);
+ try testing.expectEqualStrings("bad value in control message; takes 0-60000 (milliseconds) \"MessageLinger x\"", wr(p, root_ctl, "MessageLinger x").reply.ename);
+ for (config.Runtime.settings) |setting| if (setting.action != .theme)
+ try testing.expect(config.Runtime.takes(setting.action) != null);
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, "; Themes lists all \"Theme no-such-theme\""));
@@ -1447,7 +1454,7 @@ test "the root look's file:/re/ goes on from the dot, and a miss leaves the dot
try testing.expectEqualStrings(" 0 3 ", rd(p, dot, 0, 64).bytes);
}
-test "a look's miss repeats the path whole up to 256 bytes, then cuts it with an ellipsis" {
+test "a look's miss repeats the path whole up to 256 bytes, then gives up its middle to an ellipsis" {
const gpa = testing.allocator;
const p = try withFile(gpa, "x\n");
defer p.deinit();
@@ -1456,9 +1463,9 @@ test "a look's miss repeats the path whole up to 256 bytes, then cuts it with an
var line: [512]u8 = undefined;
_ = wr(p, root, try std.fmt.bufPrint(&line, "{s}\n", .{long}));
try testing.expect(th.logHas(p, try std.fmt.bufPrint(&line, "look: no match for \"{s}\"\n", .{long})));
- const huge = "é" ** 150; // 300 bytes: cut at 256, between characters
+ const huge = "é" ** 150; // 300 bytes: its middle cut to fit 256, between characters
_ = wr(p, root, huge ++ "\n");
- try testing.expect(th.logHas(p, "look: no match for \"" ++ "é" ** 128 ++ "…\"\n"));
+ try testing.expect(th.logHas(p, "look: no match for \"" ++ "é" ** 63 ++ "…" ++ "é" ** 63 ++ "\"\n"));
}
test "exec runs a builtin at the pane and records the pane it acted on" {
@@ -1658,6 +1665,49 @@ test "Save path of a file pane logs a save record naming that path" {
try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "save {d} /tmp/pardes-save-copy.txt\n", .{serial})));
}
+test "Save and Save path on an image pane are refused with why, never a silent success" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const id = p.freeSlot().?;
+ const doc = try p.newDocPane(id);
+ doc.image = .{ .path = try p.gpa.dupe(u8, "/tmp/pardes-pic.ppm") };
+ for ([_][]const u8{ "Save\n", "Save /tmp/pardes-copy.ppm\n" }) |line| {
+ const r = wr(p, Node.of(doc.serial, .ctl), line);
+ try testing.expectEqual(Status.err, r.reply.status);
+ try testing.expectEqualStrings("Save: an image pane has no text to save", r.reply.ename);
+ }
+}
+
+test "a builtin for one kind of pane is refused on another, and still acts on its own kind" {
+ const gpa = testing.allocator;
+ const p = try withFile(gpa, "text\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const cases = [_]struct { []const u8, []const u8 }{
+ .{ "Petscii\n", "Petscii: only an image pane takes it" },
+ .{ "Palette\n", "Palette: only an image pane takes it" },
+ .{ "Ascii\n", "Ascii: only an image pane takes it" },
+ .{ "Filter\n", "Filter: only a terminal pane takes it" },
+ .{ "Togglettymode\n", "Togglettymode: only a terminal pane takes it" },
+ };
+ for (cases) |case| {
+ const r = wr(p, Node.of(serial, .ctl), case[0]);
+ try testing.expectEqual(Status.err, r.reply.status);
+ try testing.expectEqualStrings(case[1], r.reply.ename);
+ }
+ if (pardes.pdf_enabled) for ([_][]const u8{ "PdfFit\n", "PdfTint\n", "PdfSections\n" }) |line| {
+ try testing.expectEqual(Status.err, wr(p, Node.of(serial, .ctl), line).reply.status);
+ };
+ const id = p.freeSlot().?;
+ const doc = try p.newDocPane(id);
+ doc.image = .{ .path = try p.gpa.dupe(u8, "/tmp/pardes-pic.ppm") };
+ const r = wr(p, Node.of(doc.serial, .ctl), "Mode\n");
+ try testing.expectEqualStrings("Mode: only a text or terminal pane has modes", r.reply.ename);
+ const glyphs = doc.image.?.glyph_art;
+ try testing.expectEqual(Status.ok, wr(p, Node.of(doc.serial, .ctl), "Petscii\n").reply.status);
+ try testing.expect(doc.image.?.glyph_art != glyphs);
+}
+
test "exec Save writes the named file through the host" {
const gpa = testing.allocator;
const p = try withFile(gpa, "one\ntwo\n");
@@ -1786,6 +1836,64 @@ test "an error's words give the errno a mount reads: EINVAL for what is malforme
}
}
+test "DumpConfig's whole report writes back to the root ctl, line by line, with no error" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const root_ctl = @intFromEnum(tree.TopFile.ctl);
+ try testing.expectEqual(Status.ok, wr(p, root_ctl, "DumpConfig\n").reply.status);
+ const report = for (p.panes) |slot| {
+ const pane = slot orelse continue;
+ const f = pane.file orelse continue;
+ const o = f.output orelse continue;
+ if (std.meta.eql(o.from, panes.Output.Origin{ .cmd = .DumpConfig })) break try testing.allocator.dupe(u8, f.content);
+ } else return error.NoDumpConfigPane;
+ defer testing.allocator.free(report);
+ // What this shell cannot show is a comment, never a value it refuses.
+ if (pardes.platform == .tty) try testing.expect(std.mem.indexOf(u8, report, "# GripWidth unsupported\n") != null);
+ var lines = std.mem.splitScalar(u8, report, '\n');
+ while (lines.next()) |line| {
+ if (line.len == 0) continue;
+ const r = wr(p, root_ctl, line);
+ if (r.reply.status != .ok) std.debug.print("refused: {s}: {s}\n", .{ line, r.reply.ename });
+ try testing.expectEqual(Status.ok, r.reply.status);
+ }
+}
+
+test "after a ThemeFile the root ctl and DumpConfig say ThemeFile and its path, which writes back" {
+ if (comptime !pardes.hosted) return;
+ const gpa = testing.allocator;
+ const p = try withFile(gpa, "x\n");
+ defer p.deinit();
+ const root_ctl = @intFromEnum(tree.TopFile.ctl);
+ const path = "/tmp/pardes-mine-theme.zon";
+ try testing.expectEqual(Status.ok, wr(p, root_ctl, "ThemeFile " ++ path ++ "\n").reply.status);
+ var custom = pardes.themes[0];
+ custom.name = "mine";
+ var encoded: std.Io.Writer.Allocating = .init(gpa);
+ defer encoded.deinit();
+ try std.zon.stringify.serialize(custom, .{}, &encoded.writer);
+ try testing.expect(pardes.colors.loadThemeFile(p, p.theme_file_generation, encoded.written(), false));
+ const settings = rd(p, root_ctl, 0, 1 << 16).bytes;
+ try testing.expect(std.mem.indexOf(u8, settings, "ThemeFile " ++ path ++ "\n") != null);
+ try testing.expect(std.mem.indexOf(u8, settings, "Theme mine") == null);
+ var storage: [16384]u8 = undefined;
+ var out: std.Io.Writer = .fixed(&storage);
+ try config.Runtime.writeReport(&out, .{
+ .startup_config_path = null,
+ .platform = @tagName(pardes.platform),
+ .theme_name = p.theme().name,
+ .theme_file = pardes.colors.loadedThemeFile(p),
+ .compiled_default_shell = config.defaultShell(),
+ .hover_delay_frames = null,
+ .native_images = false,
+ .capabilities = builtins.capabilities,
+ .state = &p.settings,
+ });
+ try testing.expect(std.mem.indexOf(u8, out.buffered(), "ThemeFile " ++ path ++ "\n") != null);
+ // Written back, it is the same request again, not "no theme mine".
+ try testing.expectEqual(Status.ok, wr(p, root_ctl, "ThemeFile " ++ path ++ "\n").reply.status);
+}
+
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");
@@ -2909,6 +3017,23 @@ test "a column takes panes until their documented minimums fill it, not until no
}
}
+test "Jumplist fills its one +Jumps again, as Recent does, never a second" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ var count: [2]usize = undefined;
+ for (&count) |*n| {
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .exec), "Jumplist\n").reply.status);
+ p.sync();
+ n.* = 0;
+ for (p.panes) |slot| if (slot) |pane| if (pane.file) |f| if (f.output) |o| {
+ n.* += @intFromBool(std.meta.eql(o.from, panes.Output.Origin{ .cmd = .Jumplist }));
+ };
+ }
+ try testing.expectEqual(@as(usize, 1), count[0]);
+ try testing.expectEqual(@as(usize, 1), count[1]);
+}
+
test "a Jumplist entry is (closed) only while no pane has its file open" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();
@@ -2935,6 +3060,27 @@ test "a Jumplist entry is (closed) only while no pane has its file open" {
for (p.jumps[0..p.njumps]) |j| try testing.expect(j.serial != 0);
}
+test "Recent's row for a file still open is where its dot is now, not where it last closed" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "a.txt", .data = "one\ntwo\nthree\n" });
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)];
+ var line: [4200]u8 = undefined;
+ try testing.expectEqual(Status.ok, wr(p, root_look, try std.fmt.bufPrint(&line, "{s}/a.txt:3\n", .{dir})).reply.status);
+ p.sync();
+ const pane = p.panes[p.active].?;
+ pane.body.vsel.active = false; // the look's selection of line 3 let go
+ pane.body.cur_row = 1;
+ pane.body.cur_col = 2;
+ try testing.expectEqual(Status.ok, wr(p, root_exec, "Recent\n").reply.status);
+ const rows = p.panes[p.paneBySerial(try std.fmt.parseInt(u32, std.mem.trimEnd(u8, rd(p, root_exec, 0, 64).bytes, "\n"), 10)).?].?;
+ var want: [4300]u8 = undefined;
+ try testing.expect(std.mem.indexOf(u8, rows.file.?.content, try std.fmt.bufPrint(&want, "{s}/a.txt:2:3 open\n", .{dir})) != null);
+}
+
test "a closed file is found again: Recent and /recent list it closed, a look at its row reopens it at its dot, and a jump back does too" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();