summaryrefslogtreecommitdiff
path: root/src/ninep/tree.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 10:30:38 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit2d0c506583d18d6f004fc117282deb2faea1d17e (patch)
tree683d1dd2820df31222360c0308fb3333b64aae1e /src/ninep/tree.zig
parentf207f1d4d56d1012766e6206e713f2b3da70579d (diff)
downloadpardes-2d0c506583d18d6f004fc117282deb2faea1d17e.tar.gz
pardes-2d0c506583d18d6f004fc117282deb2faea1d17e.zip
Recent lists the files opened lately, closed ones too, and a jump back to a closed file reopens it
A file closed by accident, its path forgotten, could not be found again: the jumplist dropped closed panes. Every file opened (a look, a Save of a scratch, a Restore, a rename) goes first in a list of 200, kept once, its dot noted when it closes, kept across sessions in $XDG_STATE_HOME/pardes/ recent. `Recent` (SPC f r) shows it in a reused +Recent, each row `path:line:col open|closed` a look reopens; /recent reads `open|closed <path>`. The jumplist keeps a closed file's entries, +Jumps marks them `(closed)`, and Back to one opens the file at its place. acme has no such thing; its dump and Load are the nearest. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep/tree.zig')
-rw-r--r--src/ninep/tree.zig31
1 files changed, 24 insertions, 7 deletions
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index 9d2027bf..64089c01 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -219,6 +219,9 @@ pub const TopFile = enum(u5) {
tag,
col,
tagexec,
+ /// The files opened lately, closed ones too, a line each:
+ /// `open|closed <path>`, most recent first.
+ recent,
pub fn fileName(f: TopFile) []const u8 {
return if (f == .root) "/" else @tagName(f);
@@ -228,7 +231,7 @@ pub const TopFile = enum(u5) {
return switch (f) {
.root, .pane, .col => 0o755,
.look, .exec, .log, .focus, .ctl, .tag, .tagexec => 0o666,
- .README, .index, .status, .screen, .listeners, .new, .commands, .layout => 0o444,
+ .README, .index, .status, .screen, .listeners, .new, .commands, .layout, .recent => 0o444,
};
}
@@ -239,6 +242,13 @@ pub const TopFile = enum(u5) {
pub const root: u64 = @intFromEnum(TopFile.root);
+fn recentLen(p: *Pardes) u64 {
+ var buf: [64]u8 = undefined;
+ var counting: std.Io.Writer.Discarding = .init(&buf);
+ pardes.panes.Output.recentText(p, &counting.writer, false) catch return 0;
+ return counting.fullCount();
+}
+
/// Declaration order is readdir order.
pub const PaneFile = enum(u5) {
dir = 0,
@@ -556,6 +566,7 @@ fn topSize(p: *Pardes, f: TopFile) u64 {
},
.ctl => ctl.rootLen(p),
.commands => ctl.commandsLen(),
+ .recent => recentLen(p),
.focus => focus: {
if (p.header_focus) break :focus 0;
const pn = p.panes[p.active] orelse break :focus 0;
@@ -653,7 +664,7 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply {
.root => {
// The guide, then what describes the session, then the
// files that act, then the directories.
- inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.focus, TopFile.ctl, TopFile.commands, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.layout, TopFile.tag, TopFile.tagexec, TopFile.pane, TopFile.col }) |t| {
+ inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.focus, TopFile.ctl, TopFile.commands, TopFile.recent, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.layout, TopFile.tag, TopFile.tagexec, TopFile.pane, TopFile.col }) |t| {
if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(t), t.dir(), t.fileName());
}
if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, fs.os_root, true, "os");
@@ -933,6 +944,12 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply {
.focus => ctl.readFocus(p, req),
.ctl => ctl.readRoot(p, req),
.commands => ctl.readCommands(p, req),
+ .recent => recent: {
+ var text: std.Io.Writer.Allocating = .init(p.scratch.allocator());
+ pardes.panes.Output.recentText(p, &text.writer, false) catch break :recent Reply.fail(req.tag, E.NOMEM);
+ p.fs.stage(p.gpa).appendSlice(p.gpa, text.written()) catch break :recent Reply.fail(req.tag, E.NOMEM);
+ break :recent stagedReply(p, req);
+ },
.layout => layout: {
var text: std.Io.Writer.Allocating = .init(p.scratch.allocator());
cols.writeLayout(p, &text.writer) catch break :layout Reply.fail(req.tag, E.NOMEM);
@@ -1201,13 +1218,13 @@ test "readdir lists the root and a pane directory without creating anything" {
@memcpy(top_bytes[0..top_reply.bytes.len], top_reply.bytes);
const top = th.dirents(top_bytes[0..top_reply.bytes.len], &buf);
try testing.expect(top.len >= 10);
- for ([_][]const u8{ "README", "index", "status", "focus", "ctl", "commands", "look", "exec", "log", "screen", "listeners", "layout", "tag", "tagexec", "pane", "col", "os" }, 0..) |name, i| {
+ for ([_][]const u8{ "README", "index", "status", "focus", "ctl", "commands", "recent", "look", "exec", "log", "screen", "listeners", "layout", "tag", "tagexec", "pane", "col", "os" }, 0..) |name, i| {
try testing.expectEqualStrings(name, top[i].name);
- try testing.expectEqual(i >= 14, top[i].dir);
+ try testing.expectEqual(i >= 15, top[i].dir);
}
- try testing.expectEqual(@intFromEnum(TopFile.pane), top[14].node);
- try testing.expectEqual(@intFromEnum(TopFile.col), top[15].node);
- try testing.expectEqual(fs.os_root, top[16].node);
+ try testing.expectEqual(@intFromEnum(TopFile.pane), top[15].node);
+ try testing.expectEqual(@intFromEnum(TopFile.col), top[16].node);
+ try testing.expectEqual(fs.os_root, top[17].node);
var idbuf: [16]u8 = undefined;
try testing.expect(th.nameAt(top, try std.fmt.bufPrint(&idbuf, "{d}", .{serial})) == null);
try testing.expect(th.nameAt(top, "self") == null);