#!/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 `/body` tag read `/tag` # send APPEND to `/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 < print the pane's text tag print the pane's tag send append a line of text to the pane's body exec make the editor run (builtin or shell command) new [file] create a pane, optionally loading ; prints its id del 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 " [ -d "$mount" ] || die "$mount is not a directory (has pardes exited?)" cmd=$1 shift # Every pane file lives under //. 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