summaryrefslogtreecommitdiff
path: root/examples/acmefs/pardesctl
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-25 02:07:23 -0300
committerGabriel Schneider <[email protected]>2026-08-25 09:42:07 -0300
commit6f48508aa08396bcf9dd4da2cab1d221bcc53f78 (patch)
tree83daec3db5ac27ea3172651df2b3e9cb62eddb4a /examples/acmefs/pardesctl
parent28c70cabb6ceb7e5fecfd74f6984f5a995269f01 (diff)
downloadpardes-6f48508aa08396bcf9dd4da2cab1d221bcc53f78.tar.gz
pardes-6f48508aa08396bcf9dd4da2cab1d221bcc53f78.zip
acmefs: pardes --fs serves acme's control filesystem over raw Linux FUSE
Diffstat (limited to 'examples/acmefs/pardesctl')
-rwxr-xr-xexamples/acmefs/pardesctl158
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