diff options
Diffstat (limited to 'src/ninep')
| -rw-r--r-- | src/ninep/addr.zig | 22 | ||||
| -rw-r--r-- | src/ninep/cols.zig | 26 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 158 | ||||
| -rw-r--r-- | src/ninep/events.zig | 5 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 52 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 6 |
6 files changed, 259 insertions, 10 deletions
diff --git a/src/ninep/addr.zig b/src/ninep/addr.zig index df2cfaf0..b78c46e7 100644 --- a/src/ninep/addr.zig +++ b/src/ninep/addr.zig @@ -498,6 +498,28 @@ test "regular expressions search lines as sam's do, and a search wraps" { try testing.expectEqual(@as(u32, 5), q.panes[0].?.fs.addr.q0); } +test "a refused write to dot after a truncation changes nothing; a truncation with no write empties it at close" { + const p = try th.withFile(testing.allocator, "one\ntwo\nthree\n"); + defer p.deinit(); + const serial = th.serialOf(p); + const dot = Node.of(serial, .dot); + try testing.expectEqual(Status.ok, th.wr(p, dot, "2").reply.status); + const was = th.rd(p, dot, 0, 64).bytes; + var kept: [64]u8 = undefined; + @memcpy(kept[0..was.len], was); + // A shell's `echo /nomatch/ > dot`: truncate, then the refused write. + const h = th.call(p, .{ .tag = 1, .op = .open, .node = dot, .omode = 1 }).reply.handle; + _ = th.call(p, .{ .tag = 2, .op = .setattr, .node = dot, .truncate = true }); + try testing.expectEqual(Status.err, th.call(p, .{ .tag = 3, .op = .write, .node = dot, .handle = h, .data = "/nomatch/" }).reply.status); + _ = th.call(p, .{ .tag = 4, .op = .release, .node = dot, .handle = h, .opened = true }); + try testing.expectEqualStrings(kept[0..was.len], th.rd(p, dot, 0, 64).bytes); + // `: > dot`: the truncation alone empties it once its open goes. + const h2 = th.call(p, .{ .tag = 5, .op = .open, .node = dot, .omode = 1 }).reply.handle; + _ = th.call(p, .{ .tag = 6, .op = .setattr, .node = dot, .truncate = true }); + _ = th.call(p, .{ .tag = 7, .op = .release, .node = dot, .handle = h2, .opened = true }); + try testing.expectEqualStrings(" 0 0 ", th.rd(p, dot, 0, 64).bytes); +} + test "a failed address leaves none, so data refuses rather than act at the last one" { const p = try th.withFile(testing.allocator, "one\ntwo\n"); defer p.deinit(); diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index a1fe9ecc..7afcfbd8 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -232,6 +232,32 @@ test "New written to a column's exec makes its scratch in the session's director try testing.expectEqualStrings("/project/src/+New", p.panes[p.active].?.file.?.path); } +test "Joincol is refused when the joined column cannot give each pane its tag and 2 rows" { + const p = try pardes.Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 12 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const ctl = @intFromEnum(tree.TopFile.ctl); + const focus = @intFromEnum(tree.TopFile.focus); + // Two panes in the first column, then a second column of two. + const a = try th.newPane(p); + _ = a; + try testing.expectEqual(tree.Status.ok, th.wr(p, ctl, "Newcol\n").reply.status); + const b = try th.newPane(p); + var digits: [16]u8 = undefined; + _ = th.wr(p, focus, try std.fmt.bufPrint(&digits, "{d}\n", .{b})); + _ = try th.newPane(p); + try testing.expectEqual(@as(usize, 2), p.ncol); + try testing.expect(p.col_n[0] >= 2 and p.col_n[1] >= 2); + // Focus in the left column: joined, four panes want 12 rows of 10. + const left = p.panes[p.col_panes[0][0]].?.serial; + _ = th.wr(p, focus, try std.fmt.bufPrint(&digits, "{d}\n", .{left})); + const r = th.wr(p, ctl, "Joincol\n"); + try testing.expectEqual(tree.Status.err, r.reply.status); + try testing.expect(std.mem.indexOf(u8, r.reply.ename, "tag and 2 rows") != null); + try testing.expectEqual(@as(usize, 2), p.ncol); + try testing.expect(pardes.layout.minimaFit(p)); +} + test "pane/new takes rows from a pane in another column before refusing ENOSPC" { const p = try pardes.Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 16 }); defer p.deinit(); 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(); diff --git a/src/ninep/events.zig b/src/ninep/events.zig index a09f7278..bacf3ad6 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -140,8 +140,9 @@ pub fn announce(p: *Pardes) void { if (!pane.fs.unannounced) continue; pane.fs.unannounced = false; noteLog(p, .new, pane); - // A file opened (a look, a Restore, a boot): first in Recent. - if (pane.file) |f| if (f.output == null and pardes.Pardes.recentKeeps(f.path)) p.recent.opened(p.recentGpa(), f.path); + // A file, PDF or image opened (a look, a Restore, a boot): first in + // Recent. + if (pardes.Pardes.recentPlace(pane)) |d| if (pardes.Pardes.recentKeeps(d.path)) p.recent.opened(p.recentGpa(), d.path); } } diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index 78bec09f..ce6d0422 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -44,6 +44,10 @@ pub const State = struct { /// The limit a truncation cleared, kept until the write that follows it /// on that open (a shell's `>`): a write that fails puts it back. limit_truncated: ?Range = null, + /// `dot` was truncated and nothing written since: the write that + /// follows sets it or, refused, leaves it as it was; with none, the + /// open's release empties it (tree.zig releaseHandle). + dot_truncated: bool = false, /// Opens of `event`, which hold the pane scripted. readers: u16 = 0, /// One of `readers` reads `event`; a second reading open is refused. @@ -286,6 +290,9 @@ pub fn spliceBody(p: *Pardes, pane: *Pane, q0: usize, q1: usize, bytes: []const const take = if (bytes.len == 0) 0 else wholeUtf8(bytes); const lo = @min(q0, f.content.len); const hi = @max(lo, @min(q1, f.content.len)); + // An empty range deleted, or a range replaced by what it holds: no + // edit, so no undo step and no new revision. + if (std.mem.eql(u8, f.content[lo..hi], bytes[0..take])) return take; const new = p.gpa.alloc(u8, f.content.len - (hi - lo) + take) catch return null; @memcpy(new[0..lo], f.content[0..lo]); @memcpy(new[lo..][0..take], bytes[0..take]); @@ -297,6 +304,41 @@ pub fn spliceBody(p: *Pardes, pane: *Pane, q0: usize, q1: usize, bytes: []const return take; } +test "a write that changes nothing, an empty range deleted or a range replaced by itself, makes no undo step" { + const p = try withFile(testing.allocator, "abc\ndef\n"); + defer p.deinit(); + const pane = p.panes[0].?; + const serial = pane.serial; + const f = &pane.file.?; + const steps = f.history.undo_len; + const revision = f.revision; + const data = Node.of(serial, .data); + // Each as a shell does it, on an open of its own: `: > data` after + // an empty address, then `echo abc > data` over the line it holds. + _ = wr(p, Node.of(serial, .addr), "#2,#2"); + const h = call(p, .{ .tag = 1, .op = .open, .node = data, .omode = 1 }).reply.handle; + try testing.expectEqual(tree.Status.ok, call(p, .{ .tag = 2, .op = .setattr, .node = data, .truncate = true }).reply.status); + _ = call(p, .{ .tag = 3, .op = .release, .node = data, .handle = h, .opened = true }); + for ([_][]const u8{ "abc\n", "abd\n" }) |text| { + _ = wr(p, Node.of(serial, .addr), "1"); + const w = call(p, .{ .tag = 4, .op = .open, .node = data, .omode = 1 }).reply.handle; + try testing.expectEqual(tree.Status.ok, call(p, .{ .tag = 5, .op = .write, .node = data, .handle = w, .data = text }).reply.status); + _ = call(p, .{ .tag = 6, .op = .release, .node = data, .handle = w, .opened = true }); + if (text[2] == 'c') { + try testing.expectEqualStrings("abc\ndef\n", f.content); + try testing.expectEqual(steps, f.history.undo_len); + try testing.expectEqual(revision, f.revision); + } + } + // Edit's `s/d/d/` is the same nothing. + const undone = f.history.undo_len; + try testing.expectEqual(tree.Status.ok, wr(p, Node.of(serial, .ctl), "Edit ,s/d/d/\n").reply.status); + try testing.expectEqual(undone, f.history.undo_len); + // A real change is still one. + try testing.expectEqualStrings("abd\ndef\n", f.content); + try testing.expectEqual(steps + 1, f.history.undo_len); +} + test "rows through the line index are the ones counting newlines gives, at every offset" { const p = try withFile(testing.allocator, "héllo\nwo\u{301}rld\n\n last line é\ntail"); defer p.deinit(); @@ -800,9 +842,11 @@ fn writeRange(req: Req, pane: *Pane, file: PaneFile) Reply { pf.limit = kept; pf.limit_truncated = null; }; + if (file == .dot) pf.dot_truncated = false; return tree.failText(req.tag, E.INVAL, a.err); }; if (file == .limit) pf.limit_truncated = null; + if (file == .dot) pf.dot_truncated = false; switch (file) { .addr => { pf.addr = range; @@ -1000,7 +1044,7 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status { pf.limit_truncated = pf.limit; pf.limit = null; }, - .dot => if (fileOf(pane) != null) setDot(pane, .{}), + .dot => pf.dot_truncated = fileOf(pane) != null, else => {}, } return .ok; @@ -1211,8 +1255,12 @@ test "sel reads the selection and writing sel replaces it" { try testing.expectEqual(@as(u32, 0), p.panes[0].?.fs.addr.q0); try testing.expectEqual(@as(u32, 3), p.panes[0].?.fs.addr.q1); - // A truncation empties dot; an address expression addresses it. + // A truncation empties dot once its open goes, nothing written after + // it; an address expression addresses it. + const h = call(p, .{ .tag = 8, .op = .open, .node = dot, .omode = 1 }).reply.handle; _ = call(p, .{ .tag = 9, .op = .setattr, .node = dot, .truncate = true }); + try testing.expectEqualStrings("one", rd(p, sel, 0, 100).bytes); + _ = call(p, .{ .tag = 10, .op = .release, .node = dot, .handle = h, .opened = true }); try testing.expectEqualStrings("", rd(p, sel, 0, 100).bytes); try testing.expectEqual(Status.ok, wr(p, dot, "#0,#3").reply.status); diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 4300d2b5..c0ace5be 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -949,6 +949,12 @@ fn releaseHandle(p: *Pardes, req: Req) void { if (t == .pane and t.pane.file == .limit) if (p.paneBySerial(t.pane.serial)) |id| { p.panes[id].?.fs.limit_truncated = null; }; + // `: > dot` empties it once its open goes, no write having set it. + if (t == .pane and t.pane.file == .dot) if (p.paneBySerial(t.pane.serial)) |id| { + const pn = p.panes[id].?; + if (pn.fs.dot_truncated) pane.setDot(pn, .{}); + pn.fs.dot_truncated = false; + }; if (t == .col and t.col.file == .tag) cols.released(p, t.col.serial); if (t == .top and t.top == .tag) cols.released(p, null); const o = openOf(p, req) orelse return; |
