#!/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 /ctl `name /+clock`, `clean` /addr `,` (whole body) before each frame /data the frame itself /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))