summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 11:48:22 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commitfc8e1abf361ab2151109e68e5c88a0ed45650b38 (patch)
treee39977dbf694d185ae5348af5e572c0393320d50
parentf7c22573915cd4ecb98ef400b0914949ed07874e (diff)
downloadpardes-fc8e1abf361ab2151109e68e5c88a0ed45650b38.tar.gz
pardes-fc8e1abf361ab2151109e68e5c88a0ed45650b38.zip
pardes NEWFILE in a pane forwards even when its directory is missing, and Save makes the directories
fs.md said FILE must already exist, while `pardes new.txt` in fact forwarded, making a pane named for it that Save creates. With a missing directory (`pardes notes/new.txt`) the launch gave up forwarding and started a nested editor inside the pane. Now the name is made absolute as written and forwarded the same way. A name written to a pane's name file that goes into a directory not there has its Save make the directories, as Config's pane does. A Save <path> into a missing directory still fails ENOENT. The Forwarding section now says what a new FILE does. fs.py launches one into a missing directory and saves it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md11
-rw-r--r--src/main.zig10
-rw-r--r--src/ninep/pane.zig18
-rw-r--r--test/fs.py16
4 files changed, 50 insertions, 5 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 75f9fd14..2a618173 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -18,9 +18,11 @@ and `PARDES_PANE` (their pane's serial).
**Forwarding.** `pardes FILE` run in a pane (a live `PARDES_PID`, with
`PARDES_9P` and `PARDES_PANE`) writes FILE to that pane's `look` and returns
-at once, as acme's `B` does. FILE must already exist; a name that does not
-resolve, or a missing `PARDES_9P`/`PARDES_PANE`, starts a separate editor
-instead. Bare `pardes` in a pane refuses and names `--nested`.
+at once, as acme's `B` does. A FILE not there yet (`pardes notes/new.txt`)
+opens a new pane named for it, empty, and its Save creates the file, making
+its directories first when they are not there either. A missing
+`PARDES_9P`/`PARDES_PANE` starts a separate editor instead. Bare `pardes` in
+a pane refuses and names `--nested`.
`--wait` (`-w`) returns when the pane that shows FILE is deleted (exit 0) or
the session goes away (exit 1), as acme's `E` does. Use
@@ -342,7 +344,8 @@ are ENOSPC, for this open and for a look, exec, `New` or `Tty` alike.
**`name`** reads the file name (a terminal's directory); a write renames the
buffer (relative to the pane's directory) and marks nothing dirty; `Save`
-then writes under the new name. A name is one line; refused (EINVAL) are a
+then writes under the new name, making its directories first when the name
+went into one not there. A name is one line; refused (EINVAL) are a
second line, a blank at either end, control bytes and non-UTF-8 (`bad
character in file name: a blank at its start`, ...). Up to 255 bytes a
component.
diff --git a/src/main.zig b/src/main.zig
index 0e2ba954..6bdf2bfd 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -416,7 +416,15 @@ fn nativeMain(init: std.process.Init) !void {
// 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;
+ const dir = pardes.filesystem.resolveOs(std.fs.path.dirname(target.path) orelse ".", &realbuf) orelse {
+ // Its directory not there either: the name made absolute as
+ // written, and the pane's Save makes the directories (a
+ // name into a missing one says so, pane.zig writeName).
+ var cwd_buf: [4096]u8 = undefined;
+ if (std.c.getcwd(&cwd_buf, cwd_buf.len) == null) break :forwarding;
+ const cwd = std.mem.sliceTo(&cwd_buf, 0);
+ break :named std.fs.path.resolvePosix(arena, &.{ cwd, target.path }) catch break :forwarding;
+ };
break :named std.fmt.bufPrint(&newbuf, "{s}/{s}", .{ std.mem.trimEnd(u8, dir.path, "/"), base }) catch break :forwarding;
};
if (found_path != null) {
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index de7f7cd4..6a0fbba5 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -867,6 +867,11 @@ fn writeName(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
error.OutOfMemory => E.NOMEM,
else => E.INVAL,
});
+ // A name into a directory not there yet asks for it, as Config's pane
+ // does: its Save makes the directories (`pardes new/dir/f` forwarded).
+ if (comptime pardes.hosted) if (fs.localPath(std.fs.path.dirname(full) orelse "/")) |local| {
+ if (!exec.isDirectory(local)) fileOf(pane).?.make_dirs = true;
+ };
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
@@ -1387,6 +1392,19 @@ test "a name with a component over 255 bytes is refused, and a long path's faile
try testing.expect(std.mem.endsWith(u8, late, "/f.txt: no such directory"));
}
+test "a name into a directory not there has Save make it; one into a directory there does not" {
+ if (comptime !pardes.hosted) return error.SkipZigTest;
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const f = fileOf(p.panes[p.paneBySerial(serial).?].?).?;
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .name), "/tmp/pardes-no-such-dir-mk/sub/f.txt\n").reply.status);
+ try testing.expect(f.make_dirs);
+ f.make_dirs = false;
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .name), "/tmp/f.txt\n").reply.status);
+ try testing.expect(!f.make_dirs);
+}
+
test "a name cut across writes is one name, applied once at its newline or its close" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();
diff --git a/test/fs.py b/test/fs.py
index 6f1165e5..a6cd332e 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -1294,6 +1294,22 @@ def test(binary, quic=False):
client.remove(f'/pane/{named}')
assert made.wait(timeout=5) == 0
assert fresh.read_bytes() == b'new\n'
+ # ...in a directory not there either: forwarded all the same, and
+ # Save makes the directories.
+ deep = root / 'wait-missing' / 'sub' / 'new.txt'
+ before = serials()
+ made = subprocess.Popen([binary, 'wait-missing/sub/new.txt'], cwd=root, env=env,
+ stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
+ assert made.wait(timeout=5) == 0, made.stderr.read()
+ deadline = time.monotonic() + 3
+ while str(deep).encode() not in client.read('/index'):
+ assert time.monotonic() < deadline, 'a name in a missing directory opened no pane named it'
+ time.sleep(.01)
+ named, = serials() - before
+ client.write(f'/pane/{named}/body', b'deep\n')
+ execute(client, named, 'Save')
+ client.remove(f'/pane/{named}')
+ assert deep.read_bytes() == b'deep\n'
orphan = launch('--wait')
assert orphan.poll() is None
assert orphan.wait(timeout=5) == 1