summaryrefslogtreecommitdiff
path: root/examples/acmefs
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-06 18:11:36 -0300
committerGabriel Schneider <[email protected]>2026-09-07 13:59:12 -0300
commit60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch)
tree310fc734173cf771881f4691c71909135fadde97 /examples/acmefs
parentfa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff)
downloadpardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.tar.gz
pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.zip
Refactor panes and filesystem; replace FUSE with 9P
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples. Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
Diffstat (limited to 'examples/acmefs')
-rwxr-xr-xexamples/acmefs/clock.py163
-rwxr-xr-xexamples/acmefs/eventlog161
-rwxr-xr-xexamples/acmefs/life.py350
-rwxr-xr-xexamples/acmefs/pardesctl158
4 files changed, 0 insertions, 832 deletions
diff --git a/examples/acmefs/clock.py b/examples/acmefs/clock.py
deleted file mode 100755
index d976544b..00000000
--- a/examples/acmefs/clock.py
+++ /dev/null
@@ -1,163 +0,0 @@
-#!/usr/bin/env python3
-"""A pardes pane that becomes a live clock, driven only through the acme
-control filesystem. python3 stdlib, nothing else.
-
-WHAT IT DEMONSTRATES
-
- * Creating a pane is a LOOKUP, not a write: naming any file under `new/`
- makes a pane and resolves to that pane's copy of the file. Opening
- `new/ctl` is therefore the whole creation handshake, because the ctl read
- hands back the new pane's id as its first field. Nothing else in the tree
- can create a pane, and READDIR of `new/` creates nothing.
-
- * The `ctl` verb stream: `name` and `clean` go out in ONE write, newline
- separated. ctl writes are all-or-nothing, so a batch either applies whole
- or leaves the pane untouched -- which is why sending the pair together is
- safer than two writes that could half-fail.
-
- * `addr` + `data` as a whole-body REPLACE. A `body` write always appends
- (the offset is ignored), so redrawing a frame in place needs the address
- machinery: write `,` to `addr` to select the entire body, then write the
- frame to `data`, which substitutes the addressed text. After that write
- `addr` is the null string just past the insertion, so if the kernel splits
- a big frame across several write(2) calls the pieces still land in order:
- the first replaces, the rest append at the growing end.
-
-FILES TOUCHED
-
- new/ctl create the pane, read its id back
- <id>/ctl `name /+clock`, `clean`
- <id>/addr `,` (whole body) before each frame
- <id>/data the frame itself
- <id>/ctl `clean` again after each frame, see below
-
-Every frame ends with `clean` because a data write marks the pane dirty, and a
-generated clock face is not user data: a dirty pane refuses `del` and nags on
-exit. One extra ctl round trip per second is not a cost worth optimising.
-
-USAGE
-
- clock.py [mountdir] default: $PARDES_FS (set in every pane shell)
-
-Ctrl-C removes the pane and exits. So does the pane being deleted from the
-editor: the next addr/data write fails with an OSError, which is the only
-"the other end is gone" signal the filesystem gives us, and it is enough.
-"""
-
-import os
-import sys
-import time
-
-# 3x5 cells per glyph, doubled horizontally below so the face is legible in a
-# character grid, where cells are about twice as tall as they are wide.
-FONT = {
- "0": ("###", "# #", "# #", "# #", "###"),
- "1": (" #", " #", " #", " #", " #"),
- "2": ("###", " #", "###", "# ", "###"),
- "3": ("###", " #", "###", " #", "###"),
- "4": ("# #", "# #", "###", " #", " #"),
- "5": ("###", "# ", "###", " #", "###"),
- "6": ("###", "# ", "###", "# #", "###"),
- "7": ("###", " #", " #", " #", " #"),
- "8": ("###", "# #", "###", "# #", "###"),
- "9": ("###", "# #", "###", " #", "###"),
- ":": (" ", " # ", " ", " # ", " "),
-}
-BLANK = (" ",) * 5
-XSCALE = 2
-
-
-def art(text):
- """Render `text` as five rows of doubled-width block characters."""
- rows = []
- for row in range(5):
- line = " ".join(FONT.get(ch, BLANK)[row] for ch in text)
- rows.append("".join(ch * XSCALE for ch in line).rstrip())
- return rows
-
-
-def write_all(fd, data):
- """One logical fs write. Short writes are looped over rather than trusted
- away: see the addr/data note in the module comment for why the tail of a
- split frame still lands in the right place."""
- view = memoryview(data)
- while view:
- view = view[os.write(fd, view) :]
-
-
-def main(argv):
- mount = argv[1] if len(argv) > 1 else os.environ.get("PARDES_FS", "")
- if not mount:
- sys.stderr.write(
- "clock.py: no mount point. Pass one, or run inside a pardes pane\n"
- " shell where $PARDES_FS is set (start pardes with --fs).\n"
- )
- return 1
- if not os.path.isdir(mount):
- sys.stderr.write("clock.py: %s is not a directory\n" % mount)
- return 1
-
- # The lookup of `new/ctl` is the creation. Read it back for the id, which
- # is the first of the five index numbers (id, tag len, body len, isdir,
- # dirty) that a ctl read starts with. acme's ctl read has no trailing
- # newline, so read the lot and split on whitespace rather than a line.
- try:
- with open(os.path.join(mount, "new", "ctl"), "rb") as f:
- fields = f.read(256).split()
- except OSError as e:
- sys.stderr.write("clock.py: cannot create a pane: %s\n" % e)
- return 1
- if not fields or not fields[0].isdigit():
- sys.stderr.write("clock.py: unexpected new/ctl contents: %r\n" % fields[:1])
- return 1
- pane = fields[0].decode()
-
- d = os.path.join(mount, pane)
- ctl = addr = data = None
- try:
- # O_WRONLY, never O_TRUNC: truncating a control file is a setattr the
- # server has no reason to honour, and `open(..., "wb")` would send one.
- ctl = os.open(os.path.join(d, "ctl"), os.O_WRONLY)
- write_all(ctl, b"name /+clock\nclean\n")
- addr = os.open(os.path.join(d, "addr"), os.O_WRONLY)
- data = os.open(os.path.join(d, "data"), os.O_WRONLY)
-
- while True:
- now = time.localtime()
- frame = art(time.strftime("%H:%M:%S", now))
- frame.append("")
- frame.append(time.strftime("%A %d %B %Y", now))
- payload = ("\n".join(frame) + "\n").encode()
- write_all(addr, b",")
- write_all(data, payload)
- write_all(ctl, b"clean\n")
- # Sleep to the next second boundary so the face never skips or
- # stutters, and so this loop is never a spin.
- time.sleep(1.0 - (time.time() % 1.0))
- except KeyboardInterrupt:
- pass
- except OSError:
- # The pane (or the whole mount) went away. That is a normal ending for
- # a script that lives inside someone else's editor, not a crash.
- return 0
- finally:
- for fd in (addr, data):
- if fd is not None:
- try:
- os.close(fd)
- except OSError:
- pass
- if ctl is not None:
- try:
- write_all(ctl, b"clean\ndel\n")
- except OSError:
- pass
- try:
- os.close(ctl)
- except OSError:
- pass
- return 0
-
-
-if __name__ == "__main__":
- sys.exit(main(sys.argv))
diff --git a/examples/acmefs/eventlog b/examples/acmefs/eventlog
deleted file mode 100755
index b7c7596a..00000000
--- a/examples/acmefs/eventlog
+++ /dev/null
@@ -1,161 +0,0 @@
-#!/usr/bin/env bash
-# Stream one pane's `event` file and print every record in words, so that the
-# protocol can be watched instead of guessed at. bash and coreutils only.
-#
-# WARNING -- THIS IS NOT A PASSIVE OBSERVER
-#
-# Two things change the moment this script starts.
-#
-# While a pane's event file is open, that pane's button-2 (Exec) and button-3
-# (Look) actions are REPORTED and NOT PERFORMED. Middle-clicking Del in the
-# tag of a watched pane will print a record here and do nothing to the pane.
-# (Chorded Cut and Paste are exempt and behave normally.) That suppression is
-# the feature -- it is what lets a script define its own tag commands -- but
-# while you are only watching, it makes the pane feel broken.
-#
-# And a record is consumed by whoever reads it first. If another program is
-# driving that pane through its event file, do not point this at the same pane:
-# the two readers will split the stream and both will misbehave. Watch a pane
-# nobody owns, or watch the script instead.
-#
-# WHAT IT DEMONSTRATES
-#
-# A record is two characters -- origin and type -- then four blank separated
-# decimal numbers (q0, q1, flag, text length) and the text. Uppercase types
-# refer to the body, lowercase to the tag; that single bit of case is the whole
-# addressing scheme. This script spells all of it out: `M X` prints as
-# "mouse exec body", and the flag bits print as the words they stand for.
-#
-# The event file is read through `cat` rather than opened by the shell. bash's
-# `read` buffers from a seekable fd and then seeks back to correct the file
-# position -- fine on a real file, silently lossy on a stream whose server
-# ignores offsets. `cat` reads strictly forward, and the pipe it writes into is
-# not seekable, so nothing can be skipped.
-#
-# FILES TOUCHED: <id>/event (read only, but see the warning).
-#
-# USAGE: eventlog [-m mountdir] [pane-id] default id: $PARDES_PANE
-set -u
-LC_ALL=C # so ${#text} counts BYTES: event offsets are byte offsets
-
-self=${0##*/}
-mount=${PARDES_FS:-}
-
-if [ "${1:-}" = "-m" ]; then
- [ $# -ge 2 ] || { echo "$self: -m needs a directory" >&2; exit 2; }
- mount=$2
- shift 2
-fi
-pane=${1:-${PARDES_PANE:-}}
-
-if [ -z "$mount" ] || [ -z "$pane" ]; then
- cat >&2 <<EOF
-usage: $self [-m mountdir] [pane-id]
-
-Prints one line per event record: origin, type, target, q0, q1, flag, text.
-The mount comes from \$PARDES_FS and the pane id from \$PARDES_PANE, both of
-which pardes sets in every pane shell when started with --fs.
-
-Opening a pane's event file suppresses that pane's Look and Exec while this
-runs, and consumes records any other client of the same pane needs.
-EOF
- exit 2
-fi
-case $pane in
-*[!0-9]*) echo "$self: '$pane' is not a pane id" >&2; exit 2 ;;
-esac
-ev="$mount/$pane/event"
-[ -r "$ev" ] || { echo "$self: cannot read $ev (no such pane?)" >&2; exit 1; }
-
-origin_word() {
- case $1 in
- E) echo "fs-write" ;; # a write to this pane's body or tag
- F) echo "fs-action" ;; # an action taken through another of its files
- K) echo "keyboard" ;;
- M) echo "mouse" ;;
- *) echo "origin?$1" ;;
- esac
-}
-
-# Uppercase = body, lowercase = tag. Nothing else distinguishes the two.
-type_word() {
- case $1 in
- D | I | L | X) echo "body" ;;
- d | i | l | x) echo "tag" ;;
- *) echo "?" ;;
- esac
-}
-
-action_word() {
- case $1 in
- D | d) echo "delete" ;;
- I | i) echo "insert" ;;
- L | l) echo "look" ;; # button 3
- X | x) echo "exec" ;; # button 2
- *) echo "type?$1" ;;
- esac
-}
-
-# The flag is a bitwise OR whose meaning depends on the type. Deletes and
-# inserts always carry 0, so only look and exec decode to anything.
-flag_words() {
- local t=$1 f=$2 out=""
- case $t in
- X | x)
- (((f & 1) != 0)) && out="$out,builtin"
- (((f & 2) != 0)) && out="$out,expanded(record follows)"
- (((f & 8) != 0)) && out="$out,chorded-arg(2 records follow)"
- ;;
- L | l)
- (((f & 1) != 0)) && out="$out,no-load-needed"
- (((f & 2) != 0)) && out="$out,expanded(record follows)"
- (((f & 4) != 0)) && out="$out,file-or-pane-name"
- ;;
- esac
- [ -n "$out" ] && printf '%s' "${out#,}" || printf -- '-'
-}
-
-printf '%-10s %-7s %-7s %8s %8s %-24s %s\n' ORIGIN ACTION WHERE Q0 Q1 FLAG TEXT
-cat -- "$ev" 2>/dev/null | while IFS= read -r line; do
- # Blank lines are a record terminator, not a record: skip them. This is
- # also what keeps the reader in step with either text layout below.
- [ -n "$line" ] || continue
- o=${line:0:1}
- t=${line:1:1}
- rest=${line:2}
- # shellcheck disable=SC2034
- read -r q0 q1 flag n text <<<"$rest" || :
- q0=${q0:-0} q1=${q1:-0} flag=${flag:-0} n=${n:-0} text=${text:-}
- case $n in *[!0-9]*) n=0 ;; esac
- if [ "$n" -gt 0 ] && [ -z "$text" ]; then
- # The counted bytes follow the newline.
- IFS= read -r -N "$n" text || :
- elif [ "$n" -gt 0 ] && [ "${#text}" -lt "$n" ]; then
- # The text sat on the record line and contained a newline of its
- # own, which the line read above swallowed. Take the remainder.
- want=$((n - ${#text} - 1))
- more=""
- [ "$want" -gt 0 ] && { IFS= read -r -N "$want" more || :; }
- text="$text
-$more"
- fi
- if [ -n "$text" ]; then
- shown=$(printf '%q' "$text")
- elif [ "$n" -gt 0 ]; then
- shown="(short by $n bytes: the stream ended mid-record)"
- else
- # Count 0 means "no text was sent". For a delete that is the rule;
- # for a look or an exec it means the text was 256 bytes or longer and
- # was elided, or the selection was null and an expansion follows.
- case $t in
- X | x | L | l) shown="(no text: elided, or null -- read $pane/data)" ;;
- *) shown="" ;;
- esac
- fi
- printf '%-10s %-7s %-7s %8s %8s %-24s %s\n' \
- "$(origin_word "$o")" "$(action_word "$t")" "$(type_word "$t")" \
- "$q0" "$q1" "$(flag_words "$t" "$flag")" "$shown"
-done
-# cat ends when the pane or the whole mount goes away. That is the editor
-# exiting, not a failure, so say nothing and leave with 0.
-exit 0
diff --git a/examples/acmefs/life.py b/examples/acmefs/life.py
deleted file mode 100755
index 09318f6d..00000000
--- a/examples/acmefs/life.py
+++ /dev/null
@@ -1,350 +0,0 @@
-#!/usr/bin/env python3
-"""Conway's Game of Life whose entire user interface is the pane's TAG.
-python3 stdlib, nothing else.
-
-WHAT IT DEMONSTRATES
-
-This is the acme trick that makes the filesystem worth having: a script can
-define its own commands without the editor knowing anything about them.
-
- 1. Write words into the pane's `tag`. They are now just text.
- 2. Open the pane's `event` file. While it is open, button-2 (Exec) and
- button-3 (Look) on that pane are REPORTED to us and NOT performed by the
- editor. (Chorded Cut/Paste keep working, so the tag stays editable.)
- 3. A middle click on `Run` therefore arrives here as an `x` record naming
- that text, and "Run" means whatever this script decides it means.
-
-Words we do not recognise are WRITTEN BACK to the event file unchanged, which
-makes the editor perform the action as though the event file had never been
-open. So the pane's own tag entries -- Del, Put, whatever the editor puts
-there -- still work while we are attached. A script that swallowed them would
-be a black hole; passing them through is the whole etiquette of the protocol.
-
-A button-3 click in the BODY (an `L` record) toggles the cell under the click.
-The body is rendered as exactly H lines of W cells plus a newline each, so the
-click offset q0 maps to a cell by plain division -- no coordinate lookup, no
-round trip. Anything decorative goes BELOW the grid, where it cannot disturb
-that arithmetic.
-
-FILES TOUCHED
-
- new/ctl create the pane, read its id back
- <id>/ctl `name /+life`, `clean`
- <id>/tag the command words -- a tag write appends to the editable tail
- <id>/event O_RDWR: blocking reads for records, writes to pass records on
- <id>/addr `,` (whole body) before each generation
- <id>/data the generation itself
-
-USAGE
-
- life.py [mountdir] default: $PARDES_FS (set in every pane shell)
-
- Step one generation Clear empty the grid
- Run animate Random fill the grid at random
- Stop stop animating button 3 in the grid: toggle that cell
-
-Ctrl-C removes the pane and exits. So does the pane being deleted: the next
-write fails, or the event reader hits end of file, and either is a clean end.
-
-WHY A THREAD
-
-Event reads BLOCK -- the server holds the request until a record exists -- and
-a FUSE-backed regular file always polls readable, so select() cannot be used to
-wait on one. Life also has to advance on a timer. So one daemon thread does
-nothing but blocking reads and hands records to a Queue, and the main loop
-waits on the Queue with a deadline. That keeps the blocking read where it
-belongs and leaves the main loop free of spin.
-"""
-
-import os
-import queue
-import random
-import sys
-import threading
-import time
-
-W, H = 40, 20
-TICK = 0.15
-LIVE, DEAD = "#", "."
-COMMANDS = ("Step", "Run", "Stop", "Clear", "Random")
-
-
-class Records:
- """Counted event records off a blocking fd, kept in step byte-exactly.
-
- The record is two characters (origin, type), then four blank separated
- decimal numbers -- q0, q1, flag, text length -- then the text.
-
- Two layouts exist in the wild: plan9 acme puts the text before the
- record's terminating newline, while the pardes design note writes the
- newline after the four numbers and the counted bytes after it. Both are
- accepted here. Guessing wrong would not mangle one record, it would
- desynchronise the stream forever, so this reader takes whatever the line
- still holds as text and only goes back to the fd for bytes the count says
- are missing. Blank lines are skipped, which absorbs either layout's
- record terminator.
- """
-
- def __init__(self, fd):
- self.fd = fd
- self.buf = b""
-
- def _fill(self):
- chunk = os.read(self.fd, 4096) # blocks in the server until a record
- if not chunk:
- raise EOFError("event file closed")
- self.buf += chunk
-
- def _line(self):
- while True:
- nl = self.buf.find(b"\n")
- if nl >= 0:
- line, self.buf = self.buf[:nl], self.buf[nl + 1 :]
- if line:
- return line
- continue
- self._fill()
-
- def _take(self, n):
- while len(self.buf) < n:
- self._fill()
- out, self.buf = self.buf[:n], self.buf[n:]
- return out
-
- def next(self):
- line = self._line()
- while len(line) < 2:
- line = self._line()
- origin, typ = chr(line[0]), chr(line[1])
- rest, nums, i = line[2:], [], 0
- for _ in range(4):
- while i < len(rest) and rest[i : i + 1] == b" ":
- i += 1
- j = i
- while j < len(rest) and rest[j : j + 1].isdigit():
- j += 1
- nums.append(int(rest[i:j]) if j > i else 0)
- i = j
- q0, q1, flag, count = nums
- tail = rest[i + 1 :] if rest[i : i + 1] == b" " else rest[i:]
- if count == 0:
- # Text of 256 bytes or more is elided: count 0 and no bytes. The
- # reader is meant to fetch it from `data` if it cares; we do not.
- text = tail
- elif tail:
- text = tail
- if len(text) < count:
- text += b"\n" # the newline we stopped on belongs to the text
- text += self._take(count - len(text))
- else:
- text = self._take(count)
- return (origin, typ, q0, q1, flag, text.decode("utf-8", "replace"))
-
-
-def write_all(fd, data):
- view = memoryview(data)
- while view:
- view = view[os.write(fd, view) :]
-
-
-class Life:
- def __init__(self, mount):
- with open(os.path.join(mount, "new", "ctl"), "rb") as f:
- fields = f.read(256).split()
- if not fields or not fields[0].isdigit():
- raise OSError("unexpected new/ctl contents: %r" % fields[:1])
- self.pane = fields[0].decode()
- d = os.path.join(mount, self.pane)
- self.ctl = os.open(os.path.join(d, "ctl"), os.O_WRONLY)
- write_all(self.ctl, b"name /+life\nclean\n")
- self.tagfd = os.open(os.path.join(d, "tag"), os.O_RDWR)
- # O_RDWR on one fd: reading records and writing them back are the two
- # halves of one conversation, and the editor's "someone is listening"
- # state follows the open, so a second open would be a second listener.
- self.event = os.open(os.path.join(d, "event"), os.O_RDWR)
- self.addr = os.open(os.path.join(d, "addr"), os.O_WRONLY)
- self.data = os.open(os.path.join(d, "data"), os.O_WRONLY)
- write_all(self.tagfd, (" " + " ".join(COMMANDS)).encode())
- self.tagtext = None
- self.passed = None
- self.cells = set()
- self.gen = 0
- self.running = False
-
- def close(self):
- for fd in (self.tagfd, self.event, self.addr, self.data):
- try:
- os.close(fd)
- except OSError:
- pass
- try:
- write_all(self.ctl, b"clean\ndel\n")
- except OSError:
- pass
- try:
- os.close(self.ctl)
- except OSError:
- pass
-
- # --- the fs side -----------------------------------------------------
-
- def render(self):
- rows = []
- for y in range(H):
- rows.append("".join(LIVE if (x, y) in self.cells else DEAD for x in range(W)))
- rows.append("")
- rows.append(
- "generation %d %s %d alive button 3 in the grid toggles a cell"
- % (self.gen, "running" if self.running else "stopped", len(self.cells))
- )
- write_all(self.addr, b",")
- write_all(self.data, ("\n".join(rows) + "\n").encode())
- write_all(self.ctl, b"clean\n")
-
- def tag_slice(self, q0, q1):
- """The text of a tag click, for the case where the record carried none.
- Read once and cached: the tag only changes when we or the user change
- it, and a wrong guess here costs an ignored click, not a corruption."""
- if self.tagtext is None:
- self.tagtext = os.pread(self.tagfd, 8192, 0).decode("utf-8", "replace")
- return self.tagtext[q0:q1]
-
- def passthrough(self, origin, typ, q0, q1):
- """Hand a record we do not implement back to the editor, which then
- performs it exactly as if nobody had been listening. Flag, count and
- text are omitted: the two characters and two numbers are the whole
- identity of the action.
-
- The coordinates are remembered so that a record coming straight back
- at us can be recognised. A correct server does not re-report an action
- it was asked to perform -- acme marks the write-back path `external`
- precisely to skip its own reporting branch -- but if one ever did, a
- passthrough of a passthrough is an infinite loop, and this is a cheaper
- insurance policy than finding that out in someone's editor."""
- self.passed = (typ, q0, q1)
- write_all(self.event, ("%c%c%d %d\n" % (origin, typ, q0, q1)).encode())
-
- # --- the game side ---------------------------------------------------
-
- def step(self):
- counts = {}
- for (x, y) in self.cells:
- for dx in (-1, 0, 1):
- for dy in (-1, 0, 1):
- if dx or dy:
- n = ((x + dx) % W, (y + dy) % H)
- counts[n] = counts.get(n, 0) + 1
- self.cells = {c for c, n in counts.items() if n == 3 or (n == 2 and c in self.cells)}
- self.gen += 1
-
- def command(self, word):
- if word == "Step":
- self.step()
- elif word == "Run":
- self.running = True
- elif word == "Stop":
- self.running = False
- elif word == "Clear":
- self.cells.clear()
- self.gen = 0
- elif word == "Random":
- self.cells = {
- (x, y) for x in range(W) for y in range(H) if random.random() < 0.28
- }
- self.gen = 0
- else:
- return False
- return True
-
- def toggle(self, q0):
- """Body offset -> cell. Rows are W cells plus a newline, so the row is
- the quotient and the column the remainder; a click on the newline, or
- anywhere in the status line below the grid, lands outside and is
- ignored."""
- row, col = divmod(q0, W + 1)
- if row >= H or col >= W:
- return
- cell = (col, row)
- self.cells.symmetric_difference_update({cell})
-
- def handle(self, rec):
- origin, typ, q0, q1, _flag, text = rec
- if not text and (typ, q0, q1) == self.passed:
- return False # a record we passed on, coming back: see passthrough
- if typ in "xX":
- word = (text or self.tag_slice(q0, q1)).strip()
- if not self.command(word):
- self.passthrough(origin, typ, q0, q1)
- return False
- elif typ == "L":
- self.toggle(q0)
- elif typ == "l":
- self.passthrough(origin, typ, q0, q1)
- return False
- else:
- return False # I/i/D/d: our own writes echoing back
- return True
-
-
-def reader(records, q):
- try:
- while True:
- q.put(records.next())
- except (OSError, EOFError, ValueError):
- pass
- q.put(None) # the pane or the mount is gone
-
-
-def main(argv):
- mount = argv[1] if len(argv) > 1 else os.environ.get("PARDES_FS", "")
- if not mount:
- sys.stderr.write(
- "life.py: no mount point. Pass one, or run inside a pardes pane\n"
- " shell where $PARDES_FS is set (start pardes with --fs).\n"
- )
- return 1
- if not os.path.isdir(mount):
- sys.stderr.write("life.py: %s is not a directory\n" % mount)
- return 1
- try:
- game = Life(mount)
- except OSError as e:
- sys.stderr.write("life.py: cannot set up a pane: %s\n" % e)
- return 1
-
- q = queue.Queue()
- threading.Thread(target=reader, args=(Records(game.event), q), daemon=True).start()
- try:
- game.command("Random")
- game.render()
- deadline = time.monotonic() + TICK
- while True:
- if game.running:
- wait = deadline - time.monotonic()
- if wait <= 0:
- game.step()
- game.render()
- deadline = time.monotonic() + TICK
- wait = TICK
- else:
- wait = 0.25 # a bounded wait, not a spin: clicks stay prompt
- try:
- rec = q.get(timeout=wait)
- except queue.Empty:
- continue
- if rec is None:
- break
- if game.handle(rec):
- game.render()
- deadline = time.monotonic() + TICK
- except KeyboardInterrupt:
- pass
- except OSError:
- return 0 # the pane went away mid-write; nothing to report
- finally:
- game.close()
- return 0
-
-
-if __name__ == "__main__":
- sys.exit(main(sys.argv))
diff --git a/examples/acmefs/pardesctl b/examples/acmefs/pardesctl
deleted file mode 100755
index 0a7bc1a4..00000000
--- a/examples/acmefs/pardesctl
+++ /dev/null
@@ -1,158 +0,0 @@
-#!/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