diff options
| -rw-r--r-- | .agents/skills/pardes-9p/SKILL.md | 10 | ||||
| -rw-r--r-- | docs/fs.md | 16 | ||||
| -rw-r--r-- | src/fs-help.txt | 6 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 21 |
4 files changed, 34 insertions, 19 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md index 8abd9fc4..302223e5 100644 --- a/.agents/skills/pardes-9p/SKILL.md +++ b/.agents/skills/pardes-9p/SKILL.md @@ -16,9 +16,10 @@ relative to that root unless linked. Check `$NINE_MOUNT` first. `9ns --mntgen` sets it for every process it starts (an interactive shell on this machine runs inside one), so a set `$NINE_MOUNT` means the posted-9P registry is mounted there, usually `/mnt/9p`, and each -running editor is a directory `$NINE_MOUNT/pardes/<pid>/`. Inside a pane, -`$PARDES_9P` is that session's socket (`/run/user/1000/pardes-9p-<pid>.sock`), -which names the directory, and `$PARDES_PANE` is the calling pane's serial: +running editor is a directory `$NINE_MOUNT/pardes/<pid>/` (a `--detach=NAME` +session's is `pardes/NAME/`). Inside a pane, `$PARDES_9P` is that session's +socket (`/run/user/1000/pardes-9p-<pid or NAME>.sock`), which names the +directory either way, and `$PARDES_PANE` is the calling pane's serial: ```sh [ -n "$NINE_MOUNT" ] || echo 'no 9P mount: use the Python client below' @@ -186,7 +187,8 @@ search and reads empty until set; truncate it to lift it. `dirty`, `mark` and `scroll` read `0` or `1` and take `0` or `1`: whether the buffer differs from its file, whether a write pushes an undo point, and whether -a write scrolls. Truncating `tag` clears the part of the tag you may edit. +a write scrolls. `tag` reads the path, then the text you may edit: `> tag` +replaces that text (default words too), `>> tag` appends to it. Address state belongs to the pane, not to a client: it keeps the last range written until someone writes another, so writing an address and reading it back @@ -65,9 +65,10 @@ A `9ns --unix` mount lives in the private namespace of the command it runs, and nothing outside that command sees it. The mount everyone on the machine shares is the registry one, `9ns --mntgen` (default `/mnt/9p`): every running editor posts itself there, so `$NINE_MOUNT/pardes/<pid>/` is that editor's tree -for any process. 9ns exports `$NINE_MOUNT` to everything it starts, so a -script checks that variable to know the mount is there, and takes `<pid>` from -the name of `$PARDES_9P` (`pardes-9p-<pid>.sock`). A new pane made through `pane/new` is a scratch named +for any process, and `$NINE_MOUNT/pardes/NAME/` a `--detach=NAME` session's. +9ns exports `$NINE_MOUNT` to everything it starts, so a script checks that +variable to know the mount is there, and takes the name from `$PARDES_9P` +(`pardes-9p-<pid or NAME>.sock`). A new pane made through `pane/new` is a scratch named `<dir>/+New` until it is given a name, and closing a column's last pane leaves such a `+New` in its place (`Delcol` closes the column). @@ -165,8 +166,8 @@ argument to a builtin that takes none, or none to one that needs it (`Msg`, `bad value in control message "X"` for a setting's value it does not take (for `Theme`, naming the themes that share the name's first letter, since all of them, `ThemeSel`'s list, are too many for an error); -and `not a session control message "X"` or `not a window control message -"X"` for a word of the other ctl. 9ns maps them all to EINVAL, and a write +and `not a session control message "X": write it to pane/<n>/ctl` or `not +a window control message "X": write it to /ctl` for a word of the other ctl. 9ns maps them all to EINVAL, and a write refused here has done nothing. A line that then fails as it runs fails the write with the error the editor reports for it and the line, e.g. `Mount: AlreadyMounted "Mount peer /tmp/s"` (EIO), and `control message needs its @@ -480,7 +481,10 @@ Control characters in a record become spaces, so a record is one line. up to a newline -- bytes here, where acme counts runes. Every offset and count pardes serves is in bytes, `#n` and `q0`/`q1` too; the event count follows them rather than switch alone, so an acme library reads pardes -correctly for ASCII text and not beyond it.) An +correctly for ASCII text and not beyond it. A `#n` that falls inside a +character, a multibyte rune or a grapheme cluster, snaps back to where that +character starts, and `addr` reads back the snapped offset. A click in a +tag gives offsets into the whole tag as `tag` reads it, the path first.) An open freezes the ring's text the way `/screen` freezes a frame: reads walk it and end. Writing `follow` to that same open makes reads past it wait for the next record, one per read; a follower the ring outran reads `lost N` first. diff --git a/src/fs-help.txt b/src/fs-help.txt index 2ae4f360..214986a1 100644 --- a/src/fs-help.txt +++ b/src/fs-help.txt @@ -9,14 +9,14 @@ log events: new/del/rename/save <serial> <name>, run/exit/send <serial> screen rendered screen as JSON, frozen from open to close listeners the session's dial addresses focus the serial of the pane with the keyboard; write a serial to give it the keyboard -ctl settings, one a line; or Newcol, Dump, Exit (quits; asks once), Kill [word] (stops what pardes ran, or what starts with word) +ctl settings, one a line; or Newcol, Dump, Exit (quits, asks once), Kill [w] (stops what pardes ran, w: by first word) commands every builtin: its word, arg if it takes one, and root or pane: which ctl takes it pane/new open it to make a pane (named +New); the read answers that pane's serial pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll errors (write-only: appended to its directory's +Errors pane) event look exec, pty/ on terminals; rmdir closes os/, src/ the host filesystem; the editor's own sources (-Dembed-sources=true builds only) -Below, $m is the mount: $NINE_MOUNT/pardes/<pid> when 9ns --mntgen set $NINE_MOUNT; PARDES_MOUNT in a Tty9p shell. +Below, $m is the mount: $NINE_MOUNT/pardes/<pid, or NAME of --detach=NAME>; PARDES_MOUNT in a Tty9p shell. cat $m/index which panes exist n=$(cat $m/pane/new) make a pane, take its serial @@ -37,7 +37,7 @@ Pitfalls, one each: Each open of pane/new makes another pane (ls, stat, find never do); rmdir closes even a dirty one. addr: #n,#m or /regexp/ from the current address (^ $ per line); > never resets it, 0 does; a miss refuses data. data: > replaces the addr range, : > deletes it, a 2nd > inserts (addr moved); body: > empties all. - tag reads the path, then its own text; writes append; truncating clears it, default words too. + tag reads the path, then its own text; > replaces that text (default words too), >> appends. A terminal's body is a history snapshot frozen per open; pty/data is the live stream. exec: a non-builtin line runs as a command pane (ctl's Shell -c; exit N shown, logged), a shell's typed in. Through a mount a malformed write is EINVAL, a well-formed one that fails EIO: log's err says why. diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 363524f1..d7d50cc6 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -208,6 +208,13 @@ fn refuse(p: *Pardes, req: Req, why: []const u8, line: []const u8) Reply { return tree.failText(req.tag, E.INVAL, text); } +/// `refuse`, saying which ctl takes the message instead. +fn refuseTo(p: *Pardes, req: Req, why: []const u8, line: []const u8, ctl: []const u8) Reply { + const room = p.fs.ename.len -| (why.len + ctl.len + 18); + const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\": write it to {s}", .{ why, line[0..@min(line.len, room)], ctl }) catch why; + return tree.failText(req.tag, E.INVAL, text); +} + /// Checks one line written to a ctl as a builtin of `scope` before any line /// of the write runs: a word the registry knows, of this ctl's scope, given /// an argument if and only if it takes or requires one, and for a setting a @@ -220,7 +227,7 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ? const arg = if (sp) |s| std.mem.trim(u8, line[s + 1 ..], " \t") else ""; const b = std.meta.stringToEnum(Builtin, word) orelse return refuse(p, req, "unknown control message", line); if (builtins.registry.scope(b) != scope) - return refuse(p, req, if (scope == .pane) "not a window control message" else "not a session control message", line); + return refuseTo(p, req, if (scope == .pane) "not a window control message" else "not a session control message", line, if (scope == .pane) "/ctl" else "pane/<n>/ctl"); if (arg.len > 0 and !builtins.registry.takesArg(b)) return refuse(p, req, "wrong #args in control message", line); if (arg.len == 0 and builtins.registry.requiresArg(b)) return refuse(p, req, "wrong #args in control message", line); const setting = config.Runtime.find(word) orelse return null; @@ -459,7 +466,7 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { if (errno != 0) return Reply.fail(req.tag, errno); } else if (std.mem.eql(u8, line, "lock")) { if (mine == null) return tree.failText(req.tag, E.INVAL, tree.e_bad_ctl); - if (other) return tree.failText(req.tag, E.BUSY, tree.e_in_use); + if (other) return tree.failText(req.tag, E.BUSY, e_locked); held = true; if (apply) pane.fs.lock = mine; } else if (std.mem.eql(u8, line, "unlock")) { @@ -477,6 +484,8 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { } const e_not_locked = "window not locked by this open"; +/// Who holds it is another open of this ctl, whose client pardes cannot name. +const e_locked = tree.e_in_use ++ ": another open of this ctl holds the lock"; /// acme's get asks winclean first (exec.c:513): unsaved text is warned /// about once, and the same get again, nothing edited since, discards it. @@ -635,8 +644,8 @@ test "the pane ctl takes acme's verbs and the pane's builtins, and refuses the r var want: [64]u8 = undefined; try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "unknown control message \"{s}\"", .{bad}), refused.reply.ename); } - try testing.expectEqualStrings("not a window control message \"Newcol\"", wr(p, ctl_node, "Newcol").reply.ename); - try testing.expectEqualStrings("not a window control message \"Verbose off\"", wr(p, ctl_node, "Verbose off").reply.ename); + try testing.expectEqualStrings("not a window control message \"Newcol\": write it to /ctl", wr(p, ctl_node, "Newcol").reply.ename); + try testing.expectEqualStrings("not a window control message \"Verbose off\": write it to /ctl", wr(p, ctl_node, "Verbose off").reply.ename); try testing.expectEqualStrings("wrong #args in control message \"Collapse now\"", wr(p, ctl_node, "Collapse now").reply.ename); // Checked whole first: a bad line anywhere and nothing ran. try testing.expectEqual(E.INVAL, wr(p, ctl_node, "Msg first\nbogus").errno()); @@ -688,7 +697,7 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi try testing.expectEqual(Status.ok, wr(p, root_ctl, "Newcol\n").reply.status); try testing.expectEqual(columns + 1, p.ncol); - try testing.expectEqualStrings("not a session control message \"Del\"", wr(p, root_ctl, "Del").reply.ename); + try testing.expectEqualStrings("not a session control message \"Del\": write it to pane/<n>/ctl", wr(p, root_ctl, "Del").reply.ename); try testing.expectEqualStrings("unknown control message \"Nonsense 1\"", wr(p, root_ctl, "Nonsense 1").reply.ename); try testing.expectEqualStrings("bad value in control message \"Verbose maybe\"", wr(p, root_ctl, "Verbose maybe").reply.ename); const no_theme = wr(p, root_ctl, "Theme no-such-theme").reply.ename; @@ -731,7 +740,7 @@ test "a second lock is refused until the holder unlocks or closes, and binds nob try testing.expectEqual(Status.ok, w.ctl(p, ctl_node, a, "lock\n").reply.status); // Another open's lock is refused at once, and so is its unlock; writes // to the pane's other files are not. - try testing.expectEqualStrings(tree.e_in_use, w.ctl(p, ctl_node, b, "lock\n").reply.ename); + try testing.expectEqualStrings(e_locked, w.ctl(p, ctl_node, b, "lock\n").reply.ename); try testing.expectEqual(E.BUSY, w.ctl(p, ctl_node, b, "lock\n").errno()); try testing.expectEqualStrings(e_not_locked, w.ctl(p, ctl_node, b, "unlock\n").reply.ename); try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .addr), "1").reply.status); |
