summaryrefslogtreecommitdiff
path: root/src/main.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.zig')
-rw-r--r--src/main.zig123
1 files changed, 118 insertions, 5 deletions
diff --git a/src/main.zig b/src/main.zig
index aaa237f6..ab6f1527 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=")) {
@@ -324,10 +408,31 @@ fn nativeMain(init: std.process.Init) !void {
if (std.mem.indexOfAny(u8, word, "\r\n") != null) break :forwarding;
const target = @import("look.zig").parsePathLine(word);
var realbuf: [4096]u8 = undefined;
- const path = if (pardes.filesystem.isVirtual(target.path)) target.path else (pardes.filesystem.resolveOs(target.path, &realbuf) orelse break :forwarding).path;
- 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;
+ var newbuf: [4096]u8 = undefined;
+ const found_path: ?[]const u8 = if (pardes.filesystem.isVirtual(target.path)) target.path else if (pardes.filesystem.resolveOs(target.path, &realbuf)) |r| r.path else null;
+ const path = found_path orelse named: {
+ // A name not there yet, as acme's B takes one: its directory
+ // resolved and the name kept, a pane made for it that Save
+ // creates the file from.
+ const base = std.fs.path.basename(target.path);
+ if (base.len == 0 or std.mem.eql(u8, base, ".") or std.mem.eql(u8, base, "..")) break :forwarding;
+ const dir = pardes.filesystem.resolveOs(std.fs.path.dirname(target.path) orelse ".", &realbuf) orelse break :forwarding;
+ break :named std.fmt.bufPrint(&newbuf, "{s}/{s}", .{ std.mem.trimEnd(u8, dir.path, "/"), base }) catch break :forwarding;
+ };
+ if (found_path != null) {
+ 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;
+ } else {
+ const made = ninep_io.Client.read(arena, parent.dial, "/pane/new", "/pane/new") catch break :forwarding;
+ const serial = std.fmt.parseInt(u32, std.mem.trim(u8, made, " \n"), 10) catch break :forwarding;
+ var name_buf: [64]u8 = undefined;
+ const name = try std.fmt.bufPrint(&name_buf, "/pane/{d}/name", .{serial});
+ var line_buf: [4200]u8 = undefined;
+ const line = std.fmt.bufPrint(&line_buf, "{s}\n", .{path}) catch break :forwarding;
+ ninep_io.Client.write(arena, parent.dial, name, line) catch break :forwarding;
+ }
+ if (wait) waitForDel(init.io, arena, parent.dial, path);
return;
}
if (positional) |a| {
@@ -383,7 +488,15 @@ fn nativeMain(init: std.process.Init) !void {
std.process.exit(1);
}
switch (pardes.platform) {
- .tty => try @import("tty/tty.zig").run(init, opts, attach),
+ .tty => @import("tty/tty.zig").run(init, opts, attach) catch |err| switch (err) {
+ // No controlling terminal (a detached pty, a daemon): said, not
+ // an error trace.
+ error.NoDevice => {
+ std.Io.File.stderr().writeStreamingAll(init.io, "pardes: no terminal to draw on: /dev/tty will not open; run it in a terminal, or --detach\n") catch {};
+ std.process.exit(1);
+ },
+ else => return err,
+ },
.gui => try @import("gui/gui.zig").run(init, opts, attach),
.web, .macos, .esp32p4 => unreachable,
}