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
|
#!/usr/bin/env bash
# A tiny command line over the pardes acme filesystem. bash and coreutils only:
# every subcommand below is one or two ordinary file operations, which is the
# point of serving the editor as a filesystem in the first place.
#
# WHAT IT DEMONSTRATES
#
# panes read `index` -- five %11d numbers (id, tag length, body length,
# isdir, dirty) then the tag text, one line per pane
# body read `<id>/body` tag read `<id>/tag`
# send APPEND to `<id>/body` -- a body write ignores its offset, so there
# is no such thing as a partial overwrite by accident
# exec write an `X`/`x` event record, which makes the editor perform the
# action as though nobody had been listening. This is the remote
# control door: it runs pardes builtins and shell commands alike.
# new LOOKUP under `new/`, which is what creates a pane
# del ctl verb `del`
#
# TWO RULES THIS SCRIPT FOLLOWS, AND YOU SHOULD TOO
#
# Always `>>`, never `>`. A plain `>` opens O_TRUNC, which is a setattr with
# size 0 -- a truncate request against a control file. Appending is what every
# writable file here actually wants; the offset is ignored anyway.
#
# `ctl` and `new/ctl` reads carry no trailing newline (acme prints fields, not
# lines), so `read` returns non-zero at end of file even though it has already
# assigned the fields. Hence the `|| :` on those reads.
#
# HOW `exec` RUNS ARBITRARY TEXT
#
# An event write is only `origin type q0 q1` -- no text. The action is named by
# a range of the pane's own text, lowercase type for the tag and uppercase for
# the body. So to run a command that is not on screen yet, this script appends
# it to the tag (a tag write appends to the editable tail), measures where it
# landed, and executes exactly that range. The command text stays visible in
# the tag afterwards, which is also how acme leaves it, and means the user can
# click it again.
#
# USAGE: run with no arguments.
set -u
self=${0##*/}
mount=${PARDES_FS:-}
usage() {
cat >&2 <<EOF
$self -- drive pardes through its acme filesystem
usage: $self [-m mountdir] command [args]
panes one line per pane: id, dirty flag, body size, tag
body <id> print the pane's text
tag <id> print the pane's tag
send <id> <text...> append a line of text to the pane's body
exec <id> <cmd...> make the editor run <cmd> (builtin or shell command)
new [file] create a pane, optionally loading <file>; prints its id
del <id> delete the pane (refused if it has unsaved changes)
The mount directory comes from \$PARDES_FS, which pardes sets in every pane
shell when started with --fs, or from -m. \$PARDES_PANE is the id of the pane
a shell is running in, so "$self send \$PARDES_PANE hello" talks to itself.
EOF
exit 2
}
die() { printf '%s: %s\n' "$self" "$*" >&2; exit 1; }
if [ "${1:-}" = "-m" ]; then
[ $# -ge 2 ] || usage
mount=$2
shift 2
fi
[ $# -ge 1 ] || usage
[ -n "$mount" ] || die "no mount point: set \$PARDES_FS or pass -m <dir>"
[ -d "$mount" ] || die "$mount is not a directory (has pardes exited?)"
cmd=$1
shift
# Every pane file lives under <mount>/<id>/. Sets $d rather than printing it:
# inside a command substitution `die` would exit only the subshell and the
# caller would sail on with an empty path. Refuses anything that is not a plain
# number, so a typo cannot wander out of the mount.
pane_dir() {
case ${1:-} in
"" | *[!0-9]*) die "expected a pane id (see: $self panes)" ;;
esac
[ -d "$mount/$1" ] || die "no pane $1 (see: $self panes)"
d="$mount/$1"
}
case $cmd in
panes)
printf '%6s %5s %8s %s\n' ID DIRTY BYTES TAG
while read -r id taglen bodylen isdir dirty tag; do
[ -n "${id:-}" ] || continue
printf '%6s %5s %8s %s\n' \
"$id" "$([ "${dirty:-0}" = 0 ] && echo - || echo '*')" \
"${bodylen:-0}" "${tag:-}"
done <"$mount/index"
;;
body)
pane_dir "${1:-}"
cat -- "$d/body"
;;
tag)
# A tag read carries no trailing newline, so supply one for the terminal.
pane_dir "${1:-}"
printf '%s\n' "$(cat -- "$d/tag")"
;;
send)
pane_dir "${1:-}"
shift
[ $# -ge 1 ] || die "send: nothing to send"
printf '%s\n' "$*" >>"$d/body" || die "send: pane went away"
;;
exec)
pane_dir "${1:-}"
shift
[ $# -ge 1 ] || die "exec: no command"
text=$*
# Where the command will land: byte length of the whole tag, plus the one
# space we prefix so it cannot merge with the word before it. Byte length,
# not character length, because addresses are byte offsets -- ${#text}
# would count characters and mis-address any non-ASCII command.
q0=$(($(wc -c <"$d/tag") + 1))
q1=$((q0 + $(printf '%s' "$text" | wc -c)))
printf ' %s' "$text" >>"$d/tag" || die "exec: pane went away"
# Lowercase 'x' is a tag exec; origin 'M' reports it as the mouse action
# this stands in for. The editor now runs it.
printf 'Mx%d %d\n' "$q0" "$q1" >>"$d/event" || die "exec: pane refused the event"
;;
new)
# The lookup itself creates the pane; the ctl read names it.
read -r id _ <"$mount/new/ctl" || :
case ${id:-} in
"" | *[!0-9]*) die "new: unexpected new/ctl contents" ;;
esac
if [ $# -ge 1 ] && [ -n "$1" ]; then
# `name` then `get`: one all-or-nothing ctl write, so the pane is
# never left named after a file it did not load.
printf 'name %s\nget\n' "$1" >>"$mount/$id/ctl" ||
die "new: cannot load $1 into pane $id"
fi
printf '%s\n' "$id"
;;
del)
pane_dir "${1:-}"
printf 'del\n' >>"$d/ctl" ||
die "del: pane $1 refused (unsaved changes; save it, or use: $self exec $1 Delete)"
;;
-h | --help | help)
usage
;;
*)
die "unknown command: $cmd (run with no arguments for usage)"
;;
esac
|