diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 18:32:37 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | a22129c343d9236277dcc0353c48cb53bc3417ce (patch) | |
| tree | 6a1d07cfc2881b6701991c2fa167400400341952 /src/main.zig | |
| parent | 1db5ba2b267f030e2de54a2cb4de9fb155c4139d (diff) | |
| download | pardes-a22129c343d9236277dcc0353c48cb53bc3417ce.tar.gz pardes-a22129c343d9236277dcc0353c48cb53bc3417ce.zip | |
pardes --wait (-w) in a pane returns only once the pane its file landed in is deleted, so it can be $EDITOR
A launch inside a pane forwards its file to the session's look and returned at once, so fish's Ctrl-O (edit_command_buffer), git commit and crontab -e read their still-unedited file back and went on. With --wait, as acme's E against B and read the way plan9port's E reads acme's log, it finds the pane /index names the file by (the one already showing it, if open), follows /log on one connection with 'follow new', reads /index once more to catch a Del that came first, and then blocks with no deadline until that pane's del record: exit 0, or 1 when the connection ends with the session. The 9P client's one-shot requests keep their 2 s deadline; its new follow takes it for the setup only. Outside pardes nothing changes. --help, docs/fs.md, the README and the 9P skill say to set EDITOR='pardes --wait', which GIT_EDITOR follows. Tested in fs.py (it returns within 50 ms of the Del, a second -w waits on the same pane, one whose session is killed exits 1) and end to end: fish 4.8 in a detached session, echo hi, Ctrl-O, the waiting launch idle in poll, the line edited over 9P, Save, Del, and fish ran echo edited.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/main.zig')
| -rw-r--r-- | src/main.zig | 85 |
1 files changed, 85 insertions, 0 deletions
diff --git a/src/main.zig b/src/main.zig index aaa237f6..ec2472d7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -58,6 +58,12 @@ const help_text = \\ A FILE argument boots just that file instead. \\ --tty-toggle <key> use Ctrl-<key> to toggle tty/editor mode \\ -l <dump.zon> load a dump of another instance (see Dump) + \\ -w, --wait inside a pardes pane, hand FILE to the outer + \\ session and return only once the pane it opened + \\ (or already had) is deleted: 0 then, 1 if the + \\ session goes away. For $EDITOR (acme's E): + \\ EDITOR='pardes --wait', which GIT_EDITOR follows. + \\ Outside pardes a session blocks anyway. \\ --nested run a full session even inside another pardes. \\ Without it, a pardes started inside a pardes \\ hands its FILE argument to the outer one. This @@ -92,6 +98,81 @@ const help_text = \\ ; +/// `--wait` after a forwarded look, as plan9port's E reads acme's log: the +/// pane `path` landed in (the one it already had, if open), then /log +/// followed on one connection until that pane's `del` -- exit 0 -- or the +/// connection ends with the session -- exit 1. +fn waitForDel(io: std.Io, gpa: std.mem.Allocator, dial: []const u8, path: []const u8) noreturn { + const first = ninep_io.Client.read(gpa, dial, "/index", "/index") catch std.process.exit(1); + const serial = paneShowing(first, path) orelse { + std.Io.File.stderr().writeStreamingAll(io, "pardes: --wait: no pane shows that file\n") catch {}; + std.process.exit(1); + }; + gpa.free(first); + const Wait = struct { + gpa: std.mem.Allocator, + dial: []const u8, + serial: u32, + /// Deleted between the look and the follow: /index, read once the + /// follow stands, says so, and a `del` after it is followed. + fn gone(w: *const @This()) bool { + const index = ninep_io.Client.read(w.gpa, w.dial, "/index", "/index") catch return false; + defer w.gpa.free(index); + return !hasPane(index, w.serial); + } + fn deleted(w: *const @This(), bytes: []const u8) bool { + return isDel(bytes, w.serial); + } + }; + const w: Wait = .{ .gpa = gpa, .dial = dial, .serial = serial }; + ninep_io.Client.follow(gpa, dial, "/log", "follow new", &w, Wait.gone, Wait.deleted) catch std.process.exit(1); + std.process.exit(0); +} + +/// Whether log records `bytes` hold `del <serial> ...`. +fn isDel(bytes: []const u8, serial: u32) bool { + var records = std.mem.splitScalar(u8, bytes, '\n'); + while (records.next()) |rec| { + if (!std.mem.startsWith(u8, rec, "del ")) continue; + const word = rec[4 .. std.mem.indexOfScalarPos(u8, rec, 4, ' ') orelse rec.len]; + if ((std.fmt.parseInt(u32, word, 10) catch continue) == serial) return true; + } + return false; +} + +/// The serial of /index's first row named `path` (`serial kind dirty name col`). +fn paneShowing(index: []const u8, path: []const u8) ?u32 { + var rows = std.mem.splitScalar(u8, index, '\n'); + while (rows.next()) |row| { + var words = std.mem.splitScalar(u8, row, ' '); + const serial = std.fmt.parseInt(u32, words.first(), 10) catch continue; + _ = words.next() orelse continue; // kind + _ = words.next() orelse continue; // dirty + const rest = words.rest(); + const end = std.mem.lastIndexOfScalar(u8, rest, ' ') orelse continue; + if (std.mem.eql(u8, rest[0..end], path)) return serial; + } + return null; +} + +fn hasPane(index: []const u8, serial: u32) bool { + var rows = std.mem.splitScalar(u8, index, '\n'); + while (rows.next()) |row| { + const first = row[0 .. std.mem.indexOfScalar(u8, row, ' ') orelse row.len]; + if ((std.fmt.parseInt(u32, first, 10) catch continue) == serial) return true; + } + return false; +} + +test "--wait finds the pane /index names a file by, spaces and all, and sees it gone" { + const index = "1 term 0 /home/u 1\n4 text 1 /tmp/a b.txt 2\n"; + try std.testing.expectEqual(@as(?u32, 4), paneShowing(index, "/tmp/a b.txt")); + try std.testing.expectEqual(@as(?u32, null), paneShowing(index, "/tmp/a")); + try std.testing.expect(hasPane(index, 4) and !hasPane(index, 2)); + try std.testing.expect(isDel("new 5 /x\ndel 4 /tmp/a b.txt\n", 4)); + try std.testing.expect(!isDel("del 41 /x\n", 4) and !isDel("undel 4\n", 4)); +} + fn badArgs(io: std.Io, comptime fmt: []const u8, args: anytype) noreturn { var buf: [1024]u8 = undefined; const line = std.fmt.bufPrint(&buf, "pardes: " ++ fmt ++ "\nTry 'pardes --help'.\n", args) catch @@ -159,6 +240,7 @@ fn nativeMain(init: std.process.Init) !void { var detach: ?[]const u8 = null; var attach: ?[]const u8 = null; var positional: ?[:0]const u8 = null; + var wait = false; var i: usize = 1; while (i < args.len) : (i += 1) { const a = args[i]; @@ -239,6 +321,8 @@ fn nativeMain(init: std.process.Init) !void { mounts_len += 1; } else if (std.mem.eql(u8, a, "--nested")) { opts.nested = true; + } else if (std.mem.eql(u8, a, "-w") or std.mem.eql(u8, a, "--wait")) { + wait = true; } else if (std.mem.eql(u8, a, "--detach")) { detach = ""; } else if (std.mem.startsWith(u8, a, "--detach=")) { @@ -328,6 +412,7 @@ fn nativeMain(init: std.process.Init) !void { var command_buf: [8192]u8 = undefined; const command = std.fmt.bufPrint(&command_buf, "{s}{s}\n", .{ path, word[target.path.len..] }) catch break :forwarding; ninep_io.Client.write(arena, parent.dial, look, command) catch break :forwarding; + if (wait) waitForDel(init.io, arena, parent.dial, path); return; } if (positional) |a| { |
