diff options
Diffstat (limited to 'examples/acmefs/pardesctl')
| -rwxr-xr-x | examples/acmefs/pardesctl | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/examples/acmefs/pardesctl b/examples/acmefs/pardesctl new file mode 100755 index 00000000..0a7bc1a4 --- /dev/null +++ b/examples/acmefs/pardesctl @@ -0,0 +1,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 |
