summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 23:04:26 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit074f113fa95a875a6bf17196f8e46e69806a247c (patch)
tree6ad2c8c3894c6f52a759dabd8850f1a7b6574331
parentfb3f5ab5a7758a628a6e343f24a6510a58802599 (diff)
downloadpardes-074f113fa95a875a6bf17196f8e46e69806a247c.tar.gz
pardes-074f113fa95a875a6bf17196f8e46e69806a247c.zip
Scripting over 9P has one chapter, and the served README and the 9P skill shrink to a summary, the traps and a pointer to it
Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--.agents/skills/pardes-9p/SKILL.md185
-rw-r--r--docs/typ/scripting.typ153
-rw-r--r--src/fs-help.txt56
3 files changed, 201 insertions, 193 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md
index 83675b74..51e4f2f7 100644
--- a/.agents/skills/pardes-9p/SKILL.md
+++ b/.agents/skills/pardes-9p/SKILL.md
@@ -6,8 +6,12 @@ description: Inspect and drive a running Pardes editor through its 9P control fi
# Pardes over 9P
Pardes is files: `cat`, `echo >`, `ls` and shell scripts are the whole
-interface. [docs/fs.md](../../../docs/fs.md) is the reference for every
-file; this page is recipes. Paths are relative to the repository root.
+interface. The recipes are in the Scripting chapter
+([docs/typ/scripting.typ](../../../docs/typ/scripting.typ)), every file's
+semantics in the Reference chapter
+([docs/typ/reference.typ](../../../docs/typ/reference.typ)), and a running
+session serves a summary at `$m/README`. Paths are relative to the
+repository root.
## Find the session
@@ -21,167 +25,24 @@ cat "$m/index"
the session socket of the pane you run in, `$PARDES_PANE` that pane's
serial. Under `9ns --unix SOCK -- cmd` the session is the mount itself:
`m=$NINE_MOUNT`. In a `Tty9p` shell: `m=$PARDES_MOUNT`. A dead session's
-entry answers `Input/output error`: name the session, never glob. A running
-session serves the binary that started it.
-
-## Look around
-
-```sh
-cat "$m/index" # serial kind dirty name column, a pane a line
-cat "$m/layout" # columns, then `active <serial>`
-cat "$m/focus" # the pane with the keyboard
-cat "$m/README" # the one-screen guide
-cat "$m/commands" # every builtin and where it goes
-```
-
-Parse an index row with `rsplit(maxsplit=1)` first: names may hold blanks.
-Re-read the index after anything that opens or closes panes.
-
-## Open a file, go to a place
-
-```sh
-echo "$PWD/main.zig:120" > "$m/look"; cat "$m/look" # serial it landed in
-echo "main.zig:0/fn main/" > "$m/look" # first match; without 0, the next after dot
-```
-
-A miss opens nothing, reads back empty and logs an `err <serial> look: ...`
-line; the write itself succeeds. `L:C` columns are bytes.
-
-## Make, fill, name and save a pane
-
-```sh
-n=$(cat "$m/pane/new") # each open makes one: read it once
-printf 'hello\n' > "$m/pane/$n/body" # > replaces the text
-printf 'more\n' >> "$m/pane/$n/body" # >> appends
-echo "$PWD/notes.txt" > "$m/pane/$n/name"
-echo Save > "$m/pane/$n/ctl"
-rmdir "$m/pane/$n" # close it, saved or not
-```
-
-## Edit text
-
-```sh
-p=$m/pane/$n
-echo 'Edit ,x/foo/c/bar/' > $p/ctl # sam edit, one undo step
-echo '/old/' > $p/addr; printf 'new' > $p/data # replace the next match
-echo 0 > $p/addr # back to the top
-echo '3' > $p/addr; : > $p/data # delete line 3
-cp $p/addr $p/dot; cat $p/sel # select it, read the selection
-```
-
-`addr` searches on from the last address (it is the pane's, shared by all
-clients); after a `data` write it sits just past the text, so write `addr`
-before each replacement. A failed `addr` leaves none and `data` refuses:
-check the write's status. An Edit block of several lines goes in one open:
-`printf 'Edit ,x/foo/{\ni/</\na/>/\n}\n' > $p/ctl`.
-
-## Run a command
-
-```sh
-echo 'make test' > "$m/exec"; c=$(cat "$m/exec") # a command pane; its serial
-grep "^exit $c " "$m/log" | tail -1 # `exit <serial> <N>` once done
-```
-
-At a terminal's prompt, run and get the status and output in one go:
-
-```sh
-t=$(awk '$2=="term"{print $1; exit}' "$m/index")
-exec 3<>"$m/pane/$t/pty/run"; echo 'ls' >&3; cat <&3; exec 3<&- # `exit N`, then output
-printf 'q' > "$m/pane/$t/pty/data" # raw keystrokes (\r Enter, \x03 Ctrl-C)
-echo 'sig INT' > "$m/pane/$t/pty/ctl" # interrupt
-```
-
-`busy: <program> is running` means the prompt is not free: a program holds
-the terminal, so talk to it through `pty/data`.
-
-Ask the language server at a file pane's cursor (`addr` then `dot=addr`):
-
-```sh
-echo '/myFunc/' > "$m/pane/$n/addr"; echo 'dot=addr' > "$m/pane/$n/ctl"
-echo Hover > "$m/pane/$n/exec" # +Hover; Rename new, Diagnostics, Symbols
-echo Lspwhy > "$m/pane/$n/exec" # +Lsp: why the last query found what it did
-echo Lspinfo > "$m/pane/$n/exec" # which server, and its state
-```
-
-## Follow what happens
-
-```sh
-exec 3<>"$m/log"; echo 'follow new' >&3
-timeout 30 cat <&3 # one record a line, as they come
-exec 3<&-
-```
-
-`follow` (without `new`) replays the ring first. `tail -f` sees nothing new.
-Through FUSE `read -t` never times out: use `timeout`. After a `Restore`
-every connection is cut: dial again and restart the mount.
+entry answers `Input/output error`: name the session, never glob. Without a
+mount, `test/ninep.py`'s `Client` speaks 9P itself (Scripting, "No mount").
## Traps
- A refused write says only `Invalid argument` or `Input/output error`; the
- reason is the `err` line in `$m/log` (`tail -1 "$m/log"`).
-- Only writes log: a refused open, truncation (`> data` after a failed
- `addr`) or `rmdir` has its errno alone.
-- Settings and session words go to `$m/ctl`; pane words (`Undo`, `Save`,
- `Del`) to `$m/pane/<n>/ctl`. The wrong one is refused naming the right one.
-- `Exit`, `Restore`, `Del`, `get` refuse once over unsaved text; the same word
- again discards.
-- A word no builtin knows is a shell command (`exit 127` if a typo).
-- `lock` needs a held fd: `exec 3>$p/ctl; echo lock >&3; ...; exec 3>&-`.
-- plan9port `9p write` truncates: it replaces a whole `body`.
-- Through a mount, bash's `printf 'a\nb\n' > ctl` arrives one write per
- line, so a block that should be refused whole runs its good lines before
- the bad one fails. Send a multi-line block as one write: `cat block >
- "$p/ctl"`.
-
-## No mount: the Python client
-
-Use [test/ninep.py](../../../test/ninep.py) when nothing is mounted, or when
-a fid must stay open (`event`, `pty/data`, a followed `log`). Its paths are
-the served root (`/index`, `/pane/2/body`).
-
-```sh
-PYTHONPATH=test python3 -B - "$PARDES_9P" <<'PY'
-import sys
-from ninep import Client
-with Client(sys.argv[1]) as c:
- print(c.read('/index').decode(), end='')
- n = int(c.read('/pane/new'))
- c.write(f'/pane/{n}/body', b'hello\n', truncate=True)
- fid = c.open(f'/pane/{n}/event', 0) # hold event: clicks come here
- c.write(f'/pane/{n}/exec', b'Msg hi\n')
- print(c.read_fid(fid, 0, 4096)) # b'FX0 0 1 6 Msg hi\n'
- c.close(fid)
- c.remove(f'/pane/{n}')
-PY
-```
-
-`Client` takes a Unix socket path or `(ip, port)`; `client.screen()` returns
-the parsed `/screen`. For `event` records and writing them back, see
-[fs.md#event](../../../docs/fs.md#event).
-
-## An isolated session
-
-Never experiment on a session someone is using. Build a binary with
-`zig build install -Dplatform=tty` (a bare `zig build`, or `--prefix
-zig-out`, installs over `~/.local/bin`). `test/fs.py` starts a private
-session and cleans it up:
-
-```sh
-PYTHONPATH=test python3 -B - "$(realpath zig-out/bin/pardes)" <<'PY'
-import sys, tempfile
-from pathlib import Path
-from fs import session, new_pane, execute
-with tempfile.TemporaryDirectory(prefix='pardes-9p-') as d:
- with session(sys.argv[1], Path(d), 'probe') as (client, address):
- n = new_pane(client, b'fn main() void {}\n')
- execute(client, n, 'Msg ready')
- assert 'ready' in ''.join(c[0] for c in client.screen()['cells'])
-PY
-```
-
-By hand: `pardes --detach=NAME &` with its own `HOME` and `XDG_*`
-directories, then `9ns --mntgen` (the session is `$NINE_MOUNT/pardes/NAME`)
-or `9ns --unix $XDG_RUNTIME_DIR/pardes-9p-NAME.sock -- sh`. Strip every
-`PARDES_*` variable first, or a file argument goes to the session you are
-inside. `session(..., tty=True)` and
-[test/agent_session.py](../../../test/agent_session.py) drive terminals.
+ reason is the last `err` record in `$m/log`.
+- `addr` is the pane's and moves on: each `/re/` searches from the last
+ address, and a `data` write leaves it just past the text. Write `addr`
+ before each replacement; a failed address leaves none.
+- An Edit `x` that matches nothing succeeds: check the text afterwards.
+- Each open of `pane/new` makes a pane: read it once.
+- Through a mount, `printf 'a\nb\n' > ctl` arrives one write per line: send
+ a block as one write (`cat block > $p/ctl`), ending in a newline.
+- Pane words (`Save`, `Undo`, `Del`) go to `pane/<n>/ctl`, settings and
+ session words to `$m/ctl`.
+- `Exit`, `Restore`, `Del` and `get` refuse once over unsaved text.
+- Never experiment on a session someone is using: strip every `PARDES_*`
+ variable and start your own (Scripting, "An isolated session"); build
+ with `-Dplatform=tty`, since a bare `zig build` installs over
+ `~/.local/bin`.
diff --git a/docs/typ/scripting.typ b/docs/typ/scripting.typ
new file mode 100644
index 00000000..a00ee612
--- /dev/null
+++ b/docs/typ/scripting.typ
@@ -0,0 +1,153 @@
+// Scripting pardes over 9P: finding the session, the recipes, and the
+// traps. Every file's full semantics are in the reference; this is the
+// five-minute path to using them.
+#import "style.typ": key, keys, btn, chord, word, tag, addr, file, cmd, doc, pairs
+
+Every session serves its panes, columns and tags as files, as acme does:
+`cat`, `echo >` and `ls` are the whole interface, and a program that opens
+files is an extension. The reference (#doc("fs")) has every file; this
+chapter is how to reach them and what to do with them.
+
+= Finding the session <find-the-session>
+
+Each session listens on a Unix socket,
+`$XDG_RUNTIME_DIR/pardes-9p-<name>.sock` (else under
+`~/.local/state/pardes`), `<name>` being the pid, the `--detach=NAME` or
+`--9p=NAME`, and posts it in the 9P registry as
+`$XDG_RUNTIME_DIR/9p/pardes/<name>`. Shells in its panes get `PARDES_PID`
+(the editor's pid), `PARDES_9P` (the socket) and `PARDES_PANE` (their
+pane's serial).
+
+Two clients do the rest. `9ns` comes from cloud9
+(`git.sr.ht/~gbrls/cloud9`; its `zig build` installs `9ns` on Linux) and
+mounts a 9P tree through FUSE in a private namespace, no root needed;
+`9p` is plan9port's, and reads and writes without a mount.
+
+#cmd("9ns --mntgen # every posted session, under $NINE_MOUNT")
+#cmd("9ns --unix \"$PARDES_9P\" -- sh -c 'cat \"$NINE_MOUNT/index\"' # one session, for that command")
+#cmd("9p -a \"unix!$PARDES_9P\" read index # no mount")
+
+Under `9ns --mntgen` find this pane's session from its socket:
+
+#cmd("[ -n \"$NINE_MOUNT\" ] || echo 'no 9P mount: use 9p or the Python client'")
+#cmd("s=${PARDES_9P##*/pardes-9p-}; m=$NINE_MOUNT/pardes/${s%.sock}")
+#cmd("cat \"$m/index\"")
+
+Under `9ns --unix` the session is `$NINE_MOUNT` itself (`m=$NINE_MOUNT`),
+and in a #word("Tty9p") terminal (#key("SPC n 9")) it is `$PARDES_MOUNT`.
+A dead session's registry entry stays listed and answers `Input/output error`: name the session, never glob. A running session serves the binary
+that started it.
+
+#word("Tty9p") opens a terminal with the session kernel-mounted (Linux
+v9fs): it asks for your sudo password in the pane, mounts the socket in a
+private mount namespace and starts your shell as you, with
+`PARDES_MOUNT` set. Only that shell sees the mount, and each takes one of
+the session's 16 connections. It needs the `9p` and `9pnet_fd` kernel
+modules and the `pardes-v9fs` helper the build installs beside `pardes`.
+
+= Recipes
+
+The paths below are under `$m`; `p=$m/pane/$n` for a pane.
+
+#pairs(
+ [look around], [#cmd("cat $m/index\ncat $m/layout\ncat $m/focus\ncat $m/commands")],
+ [open a file at a place], [#cmd("echo \"$PWD/main.zig:120\" > $m/look; cat $m/look # where it went\necho 'main.zig:0/fn main/' > $m/look # the first match")],
+ [make, fill, name, save], [#cmd("n=$(cat $m/pane/new) # one pane per open\nprintf 'hello\\n' > $m/pane/$n/body # > replaces, >> appends\necho \"$PWD/notes.txt\" > $m/pane/$n/name\necho Save > $m/pane/$n/ctl\nrmdir $m/pane/$n # close it")],
+ [say something], [#cmd("echo 'Msg hello' > $m/exec")],
+ [replace everywhere], [#cmd("echo 'Edit ,x/foo/c/bar/' > $p/ctl && grep -c foo $p/body")],
+ [replace one match], [#cmd("echo /old/ > $p/addr && printf new > $p/data")],
+ [delete line 3], [#cmd("echo 3 > $p/addr; : > $p/data")],
+ [select line 3, read it], [#cmd("echo 3 > $p/addr; cp $p/addr $p/dot; cat $p/sel")],
+ [run a command], [#cmd("echo 'make test' > $m/exec; c=$(cat $m/exec) # its pane\ngrep \"^exit $c \" $m/log | tail -1 # exit c N")],
+ [run at a prompt], [#cmd("t=$(awk '$2==\"term\"{print $1; exit}' $m/index)\nexec 3<>$m/pane/$t/pty/run; echo ls >&3; cat <&3; exec 3<&-")],
+ [type, interrupt], [#cmd("printf 'q' > $m/pane/$t/pty/data # \\r Enter, \\x03 Ctrl-C\necho 'sig INT' > $m/pane/$t/pty/ctl")],
+ [ask the language server], [#cmd("echo /myFunc/ > $p/addr; echo dot=addr > $p/ctl\necho Hover > $p/exec # also Rename new, Symbols")],
+ [follow what happens], [#cmd("exec 3<>$m/log; echo 'follow new' >&3\ntimeout 30 cat <&3; exec 3<&-")],
+)
+
+`follow` without `new` replays the ring first. #file("pty/run") answers
+`busy: <program> is running` when a program holds the terminal: talk to it
+through #file("pty/data") instead.
+
+== Event helpers
+
+Holding a pane's #file("event") open takes its #btn("B2") and #btn("B3")
+clicks: they arrive as acme's records instead of acting, so the pane's tag
+can carry a helper's own words, and writing a record back has pardes do
+it. The keyboard is never taken, and the clicks come back when the helper
+closes the file. One open at a time; the record format is in the reference
+(#doc("fs", section: "event")).
+
+== Waiting for an edit
+
+`pardes --wait FILE` in a pane's shell returns when the pane showing FILE
+is closed (#doc("tags", section: "editor")): `EDITOR='pardes --wait'` makes
+pardes the editor of every program run there.
+
+= Traps <traps>
+
+- A refused write says only `Invalid argument` or `Input/output error`; the
+ reason is the `err` record in the log (`tail -1 $m/log`). Only writes
+ log: a refused open, truncation or `rmdir` has its errno alone.
+- #file("addr") belongs to the pane, not to you, and moves on: each
+ `/re/` searches from the last address, a #file("data") write leaves it
+ just past what it wrote, and a read moves it too. Write #file("addr")
+ before each replacement, `0` to start at the top. A failed address
+ leaves none, and #file("data") refuses until you write one.
+- An Edit `x` that matches nothing succeeds: check the result
+ (`grep -c foo body`), not the exit status.
+- Each open of #file("pane/new") makes a pane. `ls`, `stat` and `find`
+ never do.
+- Through a mount, bash's `printf 'a\nb\n' > ctl` arrives one write per
+ line, so a block that should be refused whole runs its good lines before
+ the bad one fails. Send a block as one write (`cat block > $p/ctl`), and
+ end every write whose result matters with a newline.
+- plan9port's `9p write` opens with OTRUNC: `echo x | 9p write pane/3/body` replaces the whole body. Append with `>>` through a mount.
+- `tail -f log` never sees anything new: write `follow` on the open you
+ read. Through a FUSE mount bash's `read -t` cannot time out: wrap the
+ loop in `timeout`.
+- Settings and session words go to #file("/ctl"), pane words
+ (#word("Undo"), #word("Save"), #word("Del")) to #file("pane/<n>/ctl");
+ the wrong one is refused, naming the right one.
+- #word("Exit"), #word("Restore"), #word("Del") and `get` refuse once over
+ unsaved text; the same word again discards.
+- A word no builtin knows runs as a shell command: a typo ends `exit 127`.
+- `lock` needs a held file descriptor:
+ `exec 3>$p/ctl; echo lock >&3; ...; exec 3>&-`.
+- Names in #file("index") may hold blanks: split a row with
+ `rsplit(maxsplit=1)` first, and read #file("index") again after anything
+ that opens or closes panes.
+- A #word("Restore") hangs up every connection: dial again and restart the
+ mount.
+
+= No mount: the Python client
+
+`test/ninep.py` in the source tree speaks 9P itself, for when nothing is
+mounted or a fid must stay open (#file("event"), #file("pty/data"), a
+followed #file("log")). Its paths are the served root:
+
+```python
+import sys
+from ninep import Client # PYTHONPATH=test
+with Client(sys.argv[1]) as c: # a socket path, or (ip, port)
+ print(c.read('/index').decode(), end='')
+ n = int(c.read('/pane/new'))
+ c.write(f'/pane/{n}/body', b'hello\n', truncate=True)
+ fid = c.open(f'/pane/{n}/event', 0) # hold event: clicks come here
+ c.write(f'/pane/{n}/exec', b'Msg hi\n')
+ print(c.read_fid(fid, 0, 4096)) # b'FX0 0 1 6 Msg hi\n'
+ c.close(fid)
+ c.remove(f'/pane/{n}')
+```
+
+`client.screen()` returns the parsed #file("/screen").
+
+= An isolated session
+
+Never experiment on a session someone is using. Strip every `PARDES_*`
+variable first, or a file argument goes to the session you are inside;
+then `pardes --detach=NAME &` with its own `HOME` and `XDG_*` directories,
+and mount it with `9ns --mntgen` (the session is `$NINE_MOUNT/pardes/NAME`)
+or `9ns --unix $XDG_RUNTIME_DIR/pardes-9p-NAME.sock -- sh`. Kill it when
+done. From the source tree, `test/fs.py`'s `session()` starts a private
+session and cleans it up, and `test/agent_session.py` drives terminals.
diff --git a/src/fs-help.txt b/src/fs-help.txt
index 62d4ae48..83aa536f 100644
--- a/src/fs-help.txt
+++ b/src/fs-help.txt
@@ -1,45 +1,39 @@
Pardes control filesystem
=========================
-README this guide
-index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name (<dir>/+New if unnamed), column serial; rsplit(maxsplit=1) first, names hold spaces
+README this summary; the pardes book's Scripting and Reference chapters have the rest
+index a line per pane: serial kind(text|term|pdf|image) dirty name column; names hold blanks: rsplit(maxsplit=1)
status pid, version and pane count
look write a line: a right click on it at the active pane; read: the serials it touched (this open's)
-exec write a line: a middle click, an editor command word or a shell line; read the same (every exec file)
+exec write a line: a middle click, a builtin or a shell line; read the same (every exec file)
log events: new del rename save newcol delcol run exit send ask answer changed dump restore restored restoredcol msg err
-screen rendered screen as JSON, frozen from open to close
+screen the rendered screen as JSON, frozen from open to close
listeners the session's dial addresses
-focus the serial of the pane with the keyboard (empty on a column tag); write a serial to give it the keyboard
-ctl settings a line; or Newcol, Dump, Restore [f], Exit (asks once), Kill [w], Placement acme|pardes, size C R (--detach)
-commands every builtin: word, arg if it takes one, root|pane|both (the ctl that takes it), -- what it does
-recent files opened lately, closed ones too, most recent first: open|closed <path>
-layout a line per column: serial index x width current|notcurrent empty|full pane-serials; active <serial>
-tag the workspace tag, tagexec a word as a click in it; col/<serial>/ a column's tag, ctl (Delcol Joincol New Tty), exec; rmdir: empty only
-pane/new open it to make a pane (<session dir>/+New) in the active column, filling it if empty; the read answers its serial
-pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll errors (write-only, to +Errors) event look exec tagexec, pty/ on terminals; rmdir closes
+focus the serial of the pane with the keyboard (empty on a column tag); write one to give it the keyboard
+ctl settings a line; or Newcol, Dump, Restore [f], Exit (refuses once over unsaved panes), Kill [w], size C R
+commands every builtin: word, arg, root|pane|both (the ctl that takes it), -- what it does
+recent files opened lately, closed ones too: open|closed <path>
+layout a line per column: serial index x width current|notcurrent empty|full pane-serials; then active <serial>
+tag the workspace tag, tagexec a word as a click in it; col/<serial>/ a column's tag, ctl, exec; rmdir: empty only
+pane/new open it to make a pane (<session dir>/+New) in the active column; the read answers its serial
+pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll errors event look exec tagexec; rmdir closes
+ on terminals pty/data (keys in, output out), pty/status, pty/ctl (winsize, sig, exec), pty/run (a line, its exit and output)
os/, src/ the host filesystem; the editor's own sources
-$m below: $NINE_MOUNT/pardes/<pid|NAME>; $NINE_MOUNT under 9ns --unix; PARDES_MOUNT in a Tty9p shell.
+$m below: $NINE_MOUNT/pardes/<pid|NAME> under 9ns --mntgen; $NINE_MOUNT under 9ns --unix; $PARDES_MOUNT in a Tty9p shell.
cat $m/index which panes exist
- n=$(cat $m/pane/new) make a pane, take its serial
echo /etc/hosts:3 > $m/look; cat $m/look open a file, see the pane it went to
- printf 'text\n' >> $m/pane/$n/body append to a pane (> replaces it all)
- echo notes.txt > $m/pane/$n/name; echo Save > $m/pane/$n/ctl rename, then save it
+ n=$(cat $m/pane/new) make a pane (each open makes one), take its serial
+ printf 'text\n' >> $m/pane/$n/body append to it (> replaces it all)
echo 'Msg hello' > $m/exec show text in the editor
- echo 'Edit ,x/foo/c/bar/' > $m/pane/$n/ctl rename every foo, one undo step (sam's commands)
+ echo 'Edit ,x/foo/c/bar/' > $m/pane/$n/ctl sam's Edit, one undo step; an x that matches nothing succeeds
+ echo /re/ > $m/pane/$n/addr; printf new > $m/pane/$n/data replace the next match
exec 3<>$m/log; echo follow >&3; cat <&3 what happened lately, then wait for more
- echo Tty > $m/pane/$n/ctl; cat $m/index a terminal in that pane's directory
- exec 3<>$m/pane/$n/pty/run; echo make >&3; cat <&3 exit N then its output; or busy: <program> is running, or error
- printf 'ls\r' > $m/pane/$n/pty/data type into a terminal; pty/status: cols rows busy
- echo 'sig INT' > $m/pane/$n/pty/ctl interrupt it; also winsize C R, and exec to restart
+ exec 3<>$m/pane/$n/pty/run; echo make >&3; cat <&3 exit N, then the output
-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, 12:5 or /re/ from the current address (^ $ per line), bytes snapped to runes; 0 resets; 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; > replaces that text (default words too), >> appends.
- A terminal's body is a history snapshot frozen per open, wrapped rows joined into lines; pty/data is the live stream.
- exec: a non-builtin line (at most 1024 bytes) runs as a command pane (Shell -c), a shell's typed in; once whole.
- Through a mount a malformed write is EINVAL, a failed one EIO or an errno that fits (ENOENT): log's err says why.
- lock/unlock need a held ctl fd (exec 3>ctl); a held lock fails: retry. fs.md has the rest.
- Repl python on a terminal's ctl: a .py body's clicks go to it (Repl - unbinds, Repl says it); tags stay commands.
+Traps:
+ A refused write says only EINVAL or EIO: the log's last err record says why.
+ addr is the pane's and moves on: each /re/ searches from the last, a data write leaves it past the text.
+ Through a mount, printf 'a\nb\n' arrives a line a write: send a block with one write, ending in a newline.
+ Pane words (Save Undo Del) go to pane/<n>/ctl, settings and session words to /ctl.
+ Exit, Restore, Del and get refuse once over unsaved text; the same word again discards.