diff options
Diffstat (limited to 'examples/README.md')
| -rw-r--r-- | examples/README.md | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..6112a696 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,219 @@ +# 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/<pid> +pardes --fs=/tmp/mypardes # or name the mount point yourself +``` + +The mount lives in `$XDG_RUNTIME_DIR/pardes/<pid>`, or +`~/.local/state/pardes/<pid>` 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 + <id>/ dir one per pane; id is the pane serial, never reused + addr body ctl data errors event rdsel tag wrsel xdata +``` + +| 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/<name>` | lookup | creates a pane and resolves to that pane's `<name>`, 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 `<dir>/+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. | + +`ctl` verbs: `addr=dot`, `clean`, `dirty`, `cleartag`, `del`, `delete`, +`dot=addr`, `get`, `limit=addr`, `mark`, `nomark`, `name <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. + +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 <dir>` 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. |
