summaryrefslogtreecommitdiff
path: root/src/main.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.zig')
-rw-r--r--src/main.zig85
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| {