1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
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.
|