diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 23:04:26 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 074f113fa95a875a6bf17196f8e46e69806a247c (patch) | |
| tree | 6ad2c8c3894c6f52a759dabd8850f1a7b6574331 | |
| parent | fb3f5ab5a7758a628a6e343f24a6510a58802599 (diff) | |
| download | pardes-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.md | 185 | ||||
| -rw-r--r-- | docs/typ/scripting.typ | 153 | ||||
| -rw-r--r-- | src/fs-help.txt | 56 |
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. |
