summaryrefslogtreecommitdiff
path: root/examples/README.md
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-25 02:07:23 -0300
committerGabriel Schneider <[email protected]>2026-08-25 09:42:07 -0300
commit6f48508aa08396bcf9dd4da2cab1d221bcc53f78 (patch)
tree83daec3db5ac27ea3172651df2b3e9cb62eddb4a /examples/README.md
parent28c70cabb6ceb7e5fecfd74f6984f5a995269f01 (diff)
downloadpardes-6f48508aa08396bcf9dd4da2cab1d221bcc53f78.tar.gz
pardes-6f48508aa08396bcf9dd4da2cab1d221bcc53f78.zip
acmefs: pardes --fs serves acme's control filesystem over raw Linux FUSE
Diffstat (limited to 'examples/README.md')
-rw-r--r--examples/README.md219
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.