diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-06 18:11:36 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-07 13:59:12 -0300 |
| commit | 60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch) | |
| tree | 310fc734173cf771881f4691c71909135fadde97 /examples/acmefs | |
| parent | fa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff) | |
| download | pardes-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-x | examples/acmefs/clock.py | 163 | ||||
| -rwxr-xr-x | examples/acmefs/eventlog | 161 | ||||
| -rwxr-xr-x | examples/acmefs/life.py | 350 | ||||
| -rwxr-xr-x | examples/acmefs/pardesctl | 158 |
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 |
