diff options
Diffstat (limited to 'src/ninep/ctl.zig')
| -rw-r--r-- | src/ninep/ctl.zig | 158 |
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(); |
