# examples Programs that drive pardes from the outside. Each subdirectory is one interface; `acmefs/` is the acme control filesystem. ## The acme control filesystem Started with `--fs`, pardes serves a small filesystem describing itself: one directory per pane, holding the pane's text, its tag, its selection, a control file of verbs and an event stream. Reading a file asks the editor a question, writing one gives it an order, and a middle click can be delivered to a script instead of to the editor. That is the whole of plan9 `acme(4)`, which pardes follows closely enough that acme's own manual page is the reference; the divergences are listed at the end. The point is that a text editor becomes scriptable by anything that can open a file. The scripts here are python3 and bash with no dependencies at all, and none of them link, embed, or know anything about pardes. ### Starting it ``` pardes --fs # mount under $XDG_RUNTIME_DIR/pardes/ pardes --fs=/tmp/mypardes # or name the mount point yourself ``` The mount lives in `$XDG_RUNTIME_DIR/pardes/`, or `~/.local/state/pardes/` when there is no runtime directory. It is created at startup, mode 0700, and unmounted and removed on exit; a startup sweep removes directories left by a pardes that died without unmounting. Every shell pardes starts inside a pane inherits two variables: | variable | meaning | |---|---| | `PARDES_FS` | the mount directory | | `PARDES_PANE` | the id of the pane the shell is running in | So a script run from a pane already knows both where the editor is and which pane it is talking from, and every example below defaults to those. One rule the protocol inherits from acme: **a `ctl` verb, an `addr` expression and an `event` record must each arrive in ONE `write(2)`.** acme got that for free (a 9P message IS a write), and every ordinary client has it too — stdio buffers, `echo` and `dd` write whole strings, Python's `os.write` is one call. A client that writes a verb one byte at a time gets EINVAL per byte, because a control file cannot tell a half-finished verb from a wrong one. Write whole lines. ### The tree ``` / index r one line per pane cons w appends to +Errors new/ dir looking up ANY name here creates a pane / dir one per pane; id is the pane serial, never reused addr body ctl data errors event rdsel tag wrsel xdata pty/ dir TERMINAL PANES ONLY; absent on a pane with a document ctl status data ``` | file | mode | semantics | |---|---|---| | `index` | r | one line per pane: five `%11d` fields -- id, tag length, body length, isdir, dirty -- then the tag text. Seekable. | | `cons` | w | appended text goes to the `+Errors` pane for the writing pane's directory, created on first write. | | `new/` | lookup | creates a pane and resolves to that pane's ``, so `echo hi > $PARDES_FS/new/body` opens a pane containing `hi`. Listing `new/` enumerates nothing at all -- every name it could report is a name whose lookup would create a pane, and `ls -l` stats what a listing reports. | | `addr` | rw | read: the current address as two `%11d` byte offsets. Write: an address expression. Never disturbs the user's selection. | | `body` | rw | read at any offset; a write APPENDS, whatever the offset. Replacing text means `addr` + `data`. | | `tag` | rw | read the whole tag; a write appends to the editable tail. (A pardes tag carries a live read-only prefix, so appending to that would be meaningless.) | | `ctl` | rw | read: the five `index` numbers plus `%11d %q %11d` -- width in cells, font name, tab width in cells. Write: newline separated verbs, several per write, applied all or nothing. | | `data` | rw | read: whole graphemes from the start of `addr`, up to the read size, moving `addr` past them. Write: replaces the addressed text and leaves `addr` as the null string after the insertion. The file offset is ignored. | | `xdata` | rw | `data`, except that reads stop at the end of `addr`. | | `errors` | w | appends to `/+Errors` for this pane's directory. | | `rdsel` | r | the pane's current selection. | | `wrsel` | w | replaces the pane's current selection. | | `event` | rw | the pane's action stream, both directions. See below. | | `pty/` | dir | present only when the pane is a terminal, so `test -d $PARDES_FS//pty` is how a script asks. Absent — not empty — on a file pane, and every file under it answers ENOENT there. | | `pty/ctl` | w | newline separated verbs, several per write, applied all or nothing: `winsize `, `sig INT\|TERM\|HUP\|QUIT\|KILL`, `exec`. Anything else is EINVAL and applies nothing. | | `pty/status` | r | three `%11d` fields: columns, rows, and 1 while a program (vim, a pager, a build) holds the tty rather than the shell prompt. Seekable. | | `pty/data` | rw | the raw stream. A write is input to the program, offset ignored, short at a character boundary. A read hands back as much of the pending output as the count allows and keeps the rest — raw bytes have no records, so unlike `event` a small read is served rather than refused — and blocks while there is nothing. Output is only recorded while the file is OPEN. | `ctl` verbs: `addr=dot`, `clean`, `dirty`, `cleartag`, `del`, `delete`, `dot=addr`, `get`, `limit=addr`, `mark`, `nomark`, `name `, `noscroll`, `scroll`, `put`, `show`. An unknown verb fails the whole write with EINVAL and applies nothing, so a batch is safe to send blind. `pty/ctl` verbs in detail. `winsize` is `TIOCSWINSZ` and nothing more: it tells the program a size and does not move the pane, whose grid is its rectangle on screen, so the next time you drag that pane the layout's size wins again. `sig` goes to the tty's foreground process group — what ^C would reach — and not to the shell, which ignores SIGINT while it waits for a job. `exec` respawns the pane's configured shell in the pane's own directory and takes NO argument: `exec /bin/sh` is EINVAL rather than an argument silently dropped. `raw` and `cooked` do not exist, because the termios belongs to the program on the far side of the pty and it never tells us. Addresses, all in bytes: `#n` an offset, `n` a line, `0` the start, `$` the end, `.` the selection, `a,b` a range (`,` alone is the whole body), `+` and `-` with a count or a regex, `/re/` forwards, `?re?` backwards. Anything else is EINVAL. ### Events A record is two characters and four blank separated decimal numbers, then the text: ``` origin type q0 q1 flag length [text] ``` Origin is `E` for a write through the `body` or `tag` file, `F` for an action through one of the pane's other files, `K` for the keyboard and `M` for the mouse. Type is `D`/`d` for a delete, `I`/`i` for an insert, `L`/`l` for a button-3 Look and `X`/`x` for a button-2 Exec -- uppercase for the body, lowercase for the tag, which is the entire addressing convention. Text of 256 bytes or more is elided with a length of 0 and can be fetched from `data`. Deletes carry no text. Two behaviours make this the interesting file: * **While a pane's event file is open, its Look and Exec are reported and not performed.** Chorded Cut and Paste still work normally. That is what lets a script put its own words in the tag and mean its own things by them -- `acmefs/life.py` is nothing but that trick. * **Writing a record back performs the action**, as though the event file had never been open. The write is only `origin type q0 q1` and a newline: the action is named by a range of the pane's own text. Passing on the records you do not implement is how a script stays a good citizen of somebody else's editor. ### Deliberate divergences from acme acme counts runes; pardes counts bytes, clamped to grapheme boundaries. pardes is byte-addressed end to end -- selections, look spots, LSP offsets -- and a second coordinate system would add an O(n) scan at every boundary and make `addr=dot` and `dot=addr` lossy. For ASCII the two are identical. `acme`, `draw`, `consctl`, `label` and `editout` are not served. The first four are rio and plan9 compatibility stubs with nothing behind them here, and `editout` is the output sink of acme's `Edit` language, which pardes does not have. ## The examples All four take the mount from `$PARDES_FS` and accept an override, and all four exit quietly when the pane or the mount goes away, because "the editor exited" is a normal ending for a program living inside it. ### `acmefs/clock.py` -- a pane that is a clock Creating a pane, naming it, and replacing its body in place once a second. ``` $ examples/acmefs/clock.py & ``` A pane named `/+clock` appears and fills with the time in doubled-width block digits. Ctrl-C removes it. Demonstrates: `new/ctl` as the creation handshake, batched `ctl` verbs, and `addr` + `data` as the only way to replace body text. ### `acmefs/life.py` -- a game whose buttons are the tag ``` $ examples/acmefs/life.py & ``` A pane named `/+life` appears with `Step Run Stop Clear Random` in its tag and a random 40x20 board in its body. Middle-click the words: they are not pardes commands and pardes has never heard of them, but the script has the event file open, so the clicks arrive here as `x` records naming the text. Button 3 on a cell toggles it. Middle-clicking anything the script does not implement -- the pane's own `Del`, say -- is written back to the event file and performed by the editor as usual. Ctrl-C removes the pane. ### `acmefs/pardesctl` -- the editor from the command line ``` $ examples/acmefs/pardesctl panes ID DIRTY BYTES TAG 1 - 4213 src/pardes.zig 3 * 118 /+clock $ examples/acmefs/pardesctl send 3 'hello from the shell' $ examples/acmefs/pardesctl body 3 | wc -l $ examples/acmefs/pardesctl tag 1 $ id=$(examples/acmefs/pardesctl new src/pardes.zig) $ examples/acmefs/pardesctl exec "$id" Help $ examples/acmefs/pardesctl exec "$id" 'date >/tmp/from-pardes' $ examples/acmefs/pardesctl del "$id" ``` `exec` is the remote control door: it appends the command to the pane's tag, works out the byte range it landed in, and writes an `x` record naming that range -- which is exactly what a middle click on the same text would have sent. The command stays in the tag afterwards, where acme leaves it too, so it can be clicked again. A pardes builtin (`Help`, `New`, `Changelog`, ...) runs as a builtin; anything else runs as a shell command with its output going to `+Errors`, the same as if you had typed it into a tag and clicked it. `-m ` overrides `$PARDES_FS`. With no arguments it prints its usage. ### `acmefs/eventlog` -- watch the protocol ``` $ examples/acmefs/eventlog 3 ORIGIN ACTION WHERE Q0 Q1 FLAG TEXT mouse exec tag 41 45 builtin Help fs-write insert body 0 0 - (no text...) ``` Every record spelled out in words, flag bits included. Point it at a pane and type in it, click in it, write to it from `pardesctl`, and watch what the editor reports. ## WARNING **Opening a pane's `event` file suppresses that pane's Look and Exec.** Button 2 and button 3 in a watched pane are reported to the reader and are *not* performed by the editor, so a watched pane feels broken until the reader exits (chorded Cut and Paste are exempt). This is a feature -- it is what makes a script's own tag words possible -- but `eventlog` inherits it, so do not leave it attached to a pane you are trying to work in. **A record is consumed by whoever reads it first.** Two programs on one pane's event file split the stream between them and both misbehave. Do not point `eventlog` at the pane `life.py` is driving. **Writing an `X` or `x` record executes arbitrary commands, by design.** So does `Look` reaching an executable name. `pardesctl exec` is four lines of shell for a reason: the filesystem is a remote control, and anything that can write into the mount directory can run commands as you. The mount is mode 0700 under your own runtime directory, and that is the only thing standing between the two facts. Do not put it on a shared filesystem, and do not serve it to anything you would not hand a shell to.