summaryrefslogtreecommitdiff
path: root/examples/README.md
blob: aaaf5920428873d48e22b796054c62960a5276da (plain) (blame)
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# 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
    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/<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. |
| `pty/` | dir | present only when the pane is a terminal, so `test -d $PARDES_FS/<id>/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 <cols> <rows>`, `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 <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 <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.