summaryrefslogtreecommitdiff
path: root/examples/acmefs/clock.py
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/clock.py
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/clock.py')
-rwxr-xr-xexamples/acmefs/clock.py163
1 files changed, 163 insertions, 0 deletions
diff --git a/examples/acmefs/clock.py b/examples/acmefs/clock.py
new file mode 100755
index 00000000..d976544b
--- /dev/null
+++ b/examples/acmefs/clock.py
@@ -0,0 +1,163 @@
+#!/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))