summaryrefslogtreecommitdiff
path: root/docs/typ
diff options
context:
space:
mode:
Diffstat (limited to 'docs/typ')
-rw-r--r--docs/typ/scripting.typ153
1 files changed, 153 insertions, 0 deletions
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.