Display: a 16-colour grid sink with a leased frame source
Query parameters, presets, the wire protocol, limits, and what the page refuses to be
Table of Contents
- 1. Scope
- 2. Rendering contract: 16 colours
- 3. Query parameters
- 4. Presets
- 5. Discovery
- 6. Wire protocol
- 7. Source recipes
- 8. Reduction contract (the sink's fold)
- 9. Semantic DOM
- 10. Limits
- 11. Experiments
- 12. Non-requirements
- 13. Conformance fixtures
- 14. Refutation conditions
- 15. Open questions
- 16. Changelog
1. Scope
The display is a sink. It draws the frames it receives and does nothing else: no engine, no animations, no content of its own. Anything that produces frames (Emacs gamegrid, a Python engine, an ESP32 relaying a sensor, a 16-colour movie player, the building rig) is a source and lives elsewhere. This page specifies what the sink accepts on its URL, what it advertises, what it speaks on the wire, and the numbers it will not exceed.
Three parts:
| Part | Where | Role |
|---|---|---|
index.org page |
static, /tools/display/ |
browser sink; draws frames it receives |
capabilities.json |
static, /tools/display/capabilities.json |
advertisement: presets, palettes, fps, frame formats, endpoints |
relay |
one process | WebSocket endpoint; holds one lease per display and fans frames out to viewers |
The relay is one possible transport. A named pipe, a UDP port, a serial
line, or the building rig can replace it as long as it delivers the
same frames; the page and the advertisement stay the same. The page
also accepts frames from a source in the same browser through
window.postMessage({frame}) and exposes window.display = {w, h,
makeframe, send}, the SPEC section 2.3 surface, so a local source
needs no socket at all.
Built as a browser tool under docs/specs/tool-maintenance.org v1.0.0:
src/wal_sh/tools/display/core.cljc holds the pure reduction and
projection, browser.cljs holds the socket, the DOM, and nothing that
decides.
2. Rendering contract: 16 colours
The rendered surface is w by h cells. Every cell shows exactly one of
16 palette entries, index 0 to 15. Index 0 is unlit (the background).
The palette is a property of the display (chosen by preset, by the
palette parameter, or announced in caps), not of the frame.
A cell on the wire is a palette index. That is the whole rendering contract: the wire carries what the display can show, four bits of information per cell, and nothing the display would discard. Sources that hold colours rather than indices quantize before the wire (see 7); the sink never quantizes.
Default palette is cga, the IBM PC CGA sixteen:
| idx | name | hex | idx | name | hex |
|---|---|---|---|---|---|
| 0 | black | #000000 | 8 | dark gray | #555555 |
| 1 | blue | #0000AA | 9 | light blue | #5555FF |
| 2 | green | #00AA00 | 10 | light green | #55FF55 |
| 3 | cyan | #00AAAA | 11 | light cyan | #55FFFF |
| 4 | red | #AA0000 | 12 | light red | #FF5555 |
| 5 | magenta | #AA00AA | 13 | light magenta | #FF55FF |
| 6 | brown | #AA5500 | 14 | yellow | #FFFF55 |
| 7 | light gray | #AAAAAA | 15 | white | #FFFFFF |
Named palettes in v0.2: cga (default), c64, pico8 (16 entries
each), gb (4), grey8 (8), mono (2). A palette with fewer than 16
entries is a display with fewer levels: a lamp behind a window is on
or off, a relay-dimmed one has eight steps. A frame is still 16-valued
on the wire; the sink reduces each index to the palette's n levels
by the display-contract rule
level(idx) = 0 when idx = 0
= max(1, round(idx * (n - 1) / 15)) otherwise
so index 0 is always unlit and any lit index stays lit. mono and
grey8 are mono palettes: their entries are brightness, drawn as a
grey ramp; the others are colours. levels and mono are advertised
per preset in capabilities.json. A relay may announce a 16-entry
palette in caps; the page adopts it. Custom palettes are not accepted
on the URL in v0.2 (see 10).
3. Query parameters
The page takes d, w, h, px, gap, aspect, src, palette,
fps. d selects a preset; the explicit parameters override the
preset's fields one by one. Unknown parameters are ignored. A parameter
outside its domain is rejected, not clamped: the page renders nothing
and shows an error element naming the parameter (see 9), so a bad link fails visibly rather than drawing the wrong grid.
| name | type | domain | default | overrides preset | on invalid |
|---|---|---|---|---|---|
d |
enum | a preset name from 4 | cga40 |
n/a | error bad-preset |
w |
integer | 1 to 256 | preset | yes, and fixes it | error bad-w |
h |
integer | 1 to 256 | preset | yes, and fixes it | error bad-h |
px |
integer | 0 to 200, cell height in CSS px, 0 = fit | 0 | yes | error bad-px |
gap |
number | 0 to 1, fraction of a cell | preset | yes | error bad-gap |
aspect |
number | 0.25 to 4, cell width over cell height | preset | yes | error bad-aspect |
src |
URL | ws:// or wss://, host in 10 |
from capabilities | yes | error bad-src |
palette |
enum | a named palette | preset or cga |
yes | error bad-palette |
fps |
integer | 1 to 60 | preset | yes, downward only | error bad-fps |
w * h must not exceed 65,536 (256 by 256). fps can lower a preset's
rate, never raise it: the preset's rate is the hardware's, and the
sink honours the tighter of the two.
Geometry authority: an explicit w or h fixes the grid, and a relay
whose caps disagree is refused (bad-src with the mismatch in the
error element). Without explicit w and h, the relay's caps may
resize the grid, since the source knows the panel it is driving.
px is a rendering hint and never changes the grid.
Resolution order: preset fields first, then each explicit parameter in
the table's order, then caps for w, h, fps, palette when
not fixed. The effective values are written to the root element's
data- attributes so a reader (or Bombadil) can check what the page
decided against what the URL asked.
4. Presets
A preset is a named geometry with a default palette and frame rate.
The footer lists every preset as a link, so
/tools/display?d=green-building works and the others are one click
away.
d |
grid | cell aspect | gap | palette | fps | kind | stands for |
|---|---|---|---|---|---|---|---|
cga40 (default) |
40 x 25 | 1.2 | 0 | cga | 30 | text-mode | IBM PC CGA 40-column text mode: 320 x 200 on a 4:3 tube, pixel aspect 6:5 |
tetris |
10 x 20 | 1 | 0.12 | cga | 30 | field | standard field |
green-building |
9 x 17 | 1.5 | 0.35 | cga | 30 | facade | Building 54: wide windows with masonry between them |
dc32 |
10 x 18 | 1 | 0 | gb | 30 | badge | DEF CON 32 badge, Game Boy field |
gameboy |
10 x 18 | 1 | 0 | gb | 30 | field | same geometry, named for the original |
trs80 |
10 x 12 | 1 | 0.12 | mono | 30 | text-mode | largest field inside a 32 x 16 text screen |
c64 |
10 x 20 | 1 | 0.12 | c64 | 30 | field | 40 x 25 screen; the full field fits |
ws2812 |
16 x 16 | 1 | 0.3 | cga | 30 | panel | ESP32 + 16 x 16 LED matrix |
hub75 |
64 x 32 | 1 | 0.15 | cga | 60 | panel | ESP32 + 64 x 32 HUB75 panel |
blinkenlights |
18 x 8 | 1.6 | 0.3 | mono | 30 | facade | Haus des Lehrers, Berlin, 2001: 8 floors x 18 windows, one lamp and relay each, on or off |
arcade |
20 x 26 | 1.3 | 0.3 | grey8 | 30 | facade | Bibliotheque nationale de France, Paris, 2002: 20 x 26 windows, 8 grey levels |
remote |
9 x 17 | 1.5 | 0.35 | cga | 30 | facade | frame sink; needs &src=ws://host:port |
kind is the display-contract vocabulary (text-mode, field,
facade, badge, panel): what the preset stands in for, so a source
can pick a demo that suits a building rather than a screen. The two
Project Blinkenlights facades are mono displays with 2 and 8 levels;
their gap of 0.3 is a placeholder until the window-to-masonry ratio is
measured from the catalogue photographs. Toronto City Hall (Stereoscope,
2008, 960 windows, 16 levels), the Cira Centre (Philadelphia, 2013,
RGB LED), and the Schonherz dormitory (Budapest) are known but not
configured: their grids are not published in the sources consulted, and
capabilities.json lists them as unconfigured rather than guessing.
aspect is cell width over cell height throughout; the CGA default's
1.2 is the 6:5 pixel of 320 by 200 on a 4:3 display (issue #93's draft
wrote the same cell as 0.8, height over width). It is the largest
default that still reads as a screen rather than a poster on a laptop,
and its palette is the one the sink ships with.
Presets are data (capabilities.json and the same EDN the core reads),
not code. Adding one is a data change plus a footer link.
5. Discovery
GET /tools/display/capabilities.json. Static, co-located with the
page. The live lease holder is only on the WebSocket; the advertisement
never claims to know it. The contract does not live under
.well-known/, which this site reserves for cross-cutting agent
discovery; it is linked from .well-known/api-catalog.json (RFC 9727
linkset) the way agents.json is:
{
"anchor": "https://wal.sh/tools/display/",
"service-desc": [{ "href": "https://wal.sh/tools/display/capabilities.json", "type": "application/json" }],
"service-doc": [{ "href": "https://wal.sh/tools/display/", "type": "text/html" }]
}
The advertisement:
{
"name": "wal.sh display",
"spec": "0.2.0",
"endpoints": { "ws": "wss://wal.sh/tools/display/ws", "page": "https://wal.sh/tools/display/?d=<display>" },
"formats": ["pal16", "hex"],
"max": { "w": 256, "h": 256, "cells": 65536, "fps": 60, "ttl": 900, "frameBytes": 65538 },
"default": "cga40",
"palettes": { "cga": ["#000000", "#0000AA", "..."], "c64": ["..."], "gb": ["..."], "mono": ["..."], "pico8": ["..."] },
"displays": {
"cga40": { "w": 40, "h": 25, "aspect": 1.2, "gap": 0, "palette": "cga", "fps": 30, "note": "IBM PC CGA 40-column text mode" },
"tetris": { "w": 10, "h": 20, "aspect": 1, "gap": 0.12, "palette": "cga", "fps": 30 },
"green-building": { "w": 9, "h": 17, "aspect": 1.5, "gap": 0.35, "palette": "cga", "fps": 30 },
"hub75": { "w": 64, "h": 32, "aspect": 1, "gap": 0.15, "palette": "cga", "fps": 60 }
},
"reservation": { "max_ttl": 900, "renew": "any frame or {\"op\":\"renew\"}", "one_holder_per_display": true }
}
6. Wire protocol
WebSocket at the advertised endpoints.ws (src overrides it). Text
messages are JSON control, except that a hex frame is also a text
message (see 6.4); the two are told apart by the first
character, { for control. Binary messages are pal16 frames. Every
control message carries op; unknown op values are ignored by the
sink and answered with error reason:unknown-op by the relay.
6.1. Viewer
-> {"op":"view", "display":"green-building"} display optional; default cga40
<- {"op":"caps", "display":"green-building", "w":9, "h":17, "fps":30, "format":"pal16", "palette":[16 hex]}
<- {"op":"lease", "display":"green-building", "holder":"emacs@minibos" | null, "expires": <unix s> | null}
<- <frame> ... one per frame the holder sends
A viewer never sends frames. caps.format is the format the relay will
fan out; the page reads it once and does not renegotiate. caps.w and
caps.h resize the grid unless the URL fixed it (see
3).
6.2. Source
-> {"op":"reserve", "name":"emacs@minibos", "display":"green-building", "ttl":300, "format":"pal16"}
<- {"op":"granted", "lease":"<id>", "w":9, "h":17, "fps":30, "format":"pal16", "palette":[16 hex], "expires": <unix s>}
or
<- {"op":"busy", "holder":"...", "expires": <unix s>}
-> <frame> ... a frame renews the lease
-> {"op":"renew"} optional, when idle
-> {"op":"release"} or just close the socket
<- {"op":"error", "reason":"not-holder" | "bad-frame-length" | "rate" | "bad-format" | "unknown-op"}
6.3. Rules
- One holder per display. A
reservewhile held returnsbusy; the caller waits forexpiresor asks the holder to release. ttlis at most 900 s. The lease expiresttlseconds after the last frame orrenew. On expiry the relay sends every viewer{"op":"lease","holder":null}and a black frame (all cells index 0). Expiry is written as an event, never left implicit, so a viewer folding the message stream stays a pure function of it.- Frames from a non-holder are dropped with
error not-holder. - Frames faster than
fpsare dropped, not queued. The relay keeps no backlog; a viewer that joins late sees the next frame, not history. formatdefaults topal16when omitted inreserve.- The reservation half is a timed-reservation state machine,
free -> held(holder, expires) -> free, the same rung asaygp-dr/state-machine-ladderissue #1; this is a production instance of it.
6.4. Frame formats
Row-major, row 0 at the top, column 0 at the left, one palette index per cell. There is no partial or sparse frame; a source that changes one cell sends the whole frame.
| format | carrier | bytes per frame | encoding |
|---|---|---|---|
pal16 |
binary | w * h (+2) |
one byte per cell, value 0 to 15; an optional 2-byte big-endian sequence number may prefix it |
hex |
text | h * (w + 1) (+1) |
h lines of w hex digits 0 to f, LF-terminated; an optional blank line ends the frame |
pal16 is the native form and the one the relay fans out by default:
153 bytes for the Green Building, 1,000 for CGA, 2,048 for HUB75. The
relay strips the sequence prefix before fan-out and uses it only to
drop reordered frames (a lower sequence than the last accepted is
dropped with rate).
hex is the same information as text. It exists because a text frame
is also a render: tail -f the stream and the picture is legible, a
few lines of script turn it into ANSI for a terminal, and every source
can produce it with a printf over a plain pipe, UDP, or serial line
with no binary handling. A hex frame travels as a WebSocket text
message and is told from control by its first character.
A frame of any other length, or a hex digit outside 0 to f, is
dropped with bad-frame-length or bad-format; the byte value 16 to
255 in pal16 is bad-format.
Why not RGB on the wire: the display shows 16 colours, so an RGB cell
carries 24 bits of which 20 are discarded at render; two sources
sending the same picture in slightly different RGB render identically.
v0.1.0 accepted rgb24 at the sink and quantized there; v0.2.0 moves
that to the edge that has the colours (see 7) so the
sink stays a pure index renderer.
6.5. Emacs source sketch
Emacs 30 has no built-in WebSocket client; websocket.el (MELPA)
provides one. The source side is: open the socket, send the reserve
text, then on each gamegrid change build a w*h unibyte string of
palette indices and websocket-send-binary, or build the hex text and
websocket-send-text. That code belongs in the source repo, not here.
7. Source recipes
Not normative; the shapes that sources take, so the wire choices above can be judged against real producers.
Video at CGA geometry.
ffmpegscales and quantizes; a shim does thereserveand sends frames. The quantization is ffmpeg's, to the display's exact palette, so the sink sees indices:# palette.png: the sixteen CGA colours as a 16x1 image (make once). ffmpeg -hide_banner -re -i rick.mp4 -an \ -vf "scale=40:25:flags=area,paletteuse=dither=none" -i palette.png \ -pix_fmt pal8 -f rawvideo - | shim --display cga40 --name ffmpeg@nexuspal8is one byte per cell already; the shim maps the 256-entry palette index to 0 to 15 (identity when the palette is ours) and prefixes nothing. Withoutpaletteuse,-pix_fmt rgb24 -f rawvideogivesw*h*3bytes and the shim quantizes (nearest sRGB, ties to the lower index). On nexus, ffmpeg 8.1 hasrawvideoandrgb24but no libcaca device, so-f cacais not available here; it was never on the wire path.- Gamegrid. Already palette-indexed; one
pal16frame per board change, 153 bytes for the Green Building. - Microcontroller. A HUB75 frame buffer is indexed or RGB;
pal16costs 2,048 bytes at 60 fps, 123 KB/s, well inside an ESP32 over WiFi. - Blinkenlights family. A
pal16frame is an MCUF payload withchannels=1,maxval=15and the header removed; BLP (magicDEADBEEF, one byte per pixel, 0 or 1) is the 2-level case. The relay, when it exists, accepts both over UDP 2323 and as WebSocket binary, picks the display by width x height, scales othermaxvalto 0..15, and treats a UDP sender as a 5-second holder. The sink never sees these formats; it seespal16. Reference: https://wiki.blinkenarea.org/index.php/BlinkenlightsProtocolEnglish - Same-page source.
window.display.send(frame)with aUint8Arrayofw*hindices, orpostMessage({frame})from another window; no socket, no relay, same reduction.
8. Reduction contract (the sink's fold)
core.cljc exposes one pure step over the messages above:
(reduce-event state event) ;; => state, total over every event kind
Canonical state:
{:w 40 :h 25 :fixed? false :format :pal16 :palette :cga :fps 30
:status :connecting ; :connecting | :live | :idle | :closed | :error
:holder nil :expires nil
:seq 0 :cells [...] ; w*h palette indices
:dropped 0 :error nil}
Events are the control messages (as maps) plus :frame (bytes or
text), :open, :close, :tick. Reductions:
| event | effect |
|---|---|
:open |
status :connecting; the adapter then sends view |
caps |
set fps, format, palette; set w and h unless :fixed? (then a mismatch is :error); reallocate :cells to index 0 |
lease |
set holder and expires; holder nil sets status :idle |
:frame |
length and digits must match the format; else :dropped increments and state is unchanged; a valid frame replaces :cells, increments :seq, status :live |
error |
record :error; status unchanged |
:close |
status :closed; cells kept so the last frame stays visible |
:tick |
if expires has passed, status :idle (the relay's black frame will follow) |
| unknown | state unchanged, :dropped increments |
Invariants, asserted on every reduced state and tested by
clojure.test.check properties:
(count cells)equalsw * hwhenever w and h are set.- every cell is an integer in 0 to 15.
:seqis monotone non-decreasing across:frameevents.- a frame of wrong length or with an out-of-range cell never changes
:cells. reduce-eventis total: no event kind throws; unknown input counts as dropped.decodeis format-symmetric: thehexandpal16encodings of the same indices decode to the same:cells.
Projection (core.cljc, pure): (dirty state prev) returns the list
of (x y idx) triples that differ, so the adapter writes only changed
cells. The projection is lossy by declaration: it forgets everything
except what the DOM needs.
9. Semantic DOM
Observable by a reader, by Bombadil, and by the research-audit checks:
- root
#displaywithdata-d data-w data-h data-fixed data-px data-aspect data-gap data-palette data-fps data-format data-src data-status data-holder data-seq data-dropped, the effective values after preset, parameter, andcapsresolution. - one
<i data-x data-y data-c>per cell in row-major order;data-cis the palette index; colour comes from a CSS custom property per index (--c0to--c15) set on the root from the palette. #display-errorpresent only when a parameter was rejected orcapscontradicted a fixed grid, withdata-paramnaming it.- footer
nav.display-presetslisting every preset as a link.
The DOM grid is the v0.2 renderer. A canvas renderer is an experiment
(below), gated, and must produce the same data- attributes on the
root; the per-cell elements are what the canvas path gives up.
10. Limits
Each limit is a number with a reason and a refutation condition (the observation that would justify changing it).
| limit | value | reason | change it when |
|---|---|---|---|
| grid | w, h in 1 to 256; w*h at most 65,536 | 65,536 DOM cells renders in under a frame on a laptop; hub75 is 2,048 | a preset needs more and the DOM renderer measures over 16 ms per frame |
| frame bytes | at most 65,538 (pal16 at 256 x 256 with prefix); hex 65,793 |
one WebSocket message, no fragmentation logic in v0.2 | a source needs a bigger grid |
| fps | at most 60; per preset | the browser cannot paint faster; hardware presets carry their real rate | never upward; a preset may lower it |
| ttl | at most 900 s | a stalled source frees the display within fifteen minutes without operator action | an unattended source needs longer and renews anyway |
| holders | 1 per display | the single-writer rule; see 12 | a second writer is wanted (this is the CRDT trigger) |
src hosts |
loopback, RFC 1918, *.wal.sh |
the page's CSP connect-src admits only these; anything else is refused before connecting |
the relay is hosted somewhere else, and the CSP grows with it |
| palettes | named, or the 16 announced in caps |
a custom list on the URL is 16 more inputs to validate; presets and caps cover the hardware |
two URL-only sources ask for the same custom palette |
| viewers | relay-side cap, 32 per display | fan-out is viewers * fps * frameBytes; hub75 at 60 fps and 32 viewers is 3.9 MB/s |
the relay leaves one process |
The CSP limit is the one that decides where the relay can live. The
site-wide policy is default-src 'self'; crowsnest widens
connect-src for one path to loopback only, via a per-tool
.htaccess. Display does the same for /tools/display/ and adds
wss://wal.sh for the advertised endpoint. Whether the VPS can proxy
WebSockets under Apache is an open question (see 11);
until it can, src points at a relay on the LAN and the advertised
endpoint is a plan, not a fact.
11. Experiments
The tool ships behind the tool.display gate in data/experiments.edn
(Statsig-shaped, plaintext, evaluated client-side by
wal-sh.experiments.core). Gated off, the page renders the preset
footer and the message the other RubyConf-batch tools use.
Dynamic configs, all read once at load:
| config | type | default | what it decides |
|---|---|---|---|
display.relay |
URL | from capabilities | the src used when the URL gives none |
display.max-cells |
integer | 65536 | the grid cap, so a rollback needs no deploy |
display.max-fps |
integer | 60 | the rate cap |
display.renderer |
enum | dom |
dom or canvas |
display.wire-formats |
set | #{pal16 hex} |
which formats the sink negotiates |
Experiments, each a gate with a percentage and a checkpoint:
display.canvas: the canvas renderer against the DOM grid, measured by paint time per frame at hub75 geometry. Refuted for canvas if it does not beat the DOM by 2x at 2,048 cells.display.wire-hex: sources offeredhexby default. Refuted if any SPEC section 2.3 source breaks on a text frame, or if the relay's fan-out cost at hub75 rate exceedspal16by more than the byte ratio.display.relay-host: the advertised endpoint on wal.sh against a LAN relay. Refuted for wal.sh if Apache on the VPS cannot hold a WebSocket for 900 s.
12. Non-requirements
Stated, not omitted, each with the condition under which it becomes a requirement.
- NR-CRDT: no conflict-free replicated state. The lease makes the display single-writer, and a full frame is a last-writer-wins register keyed by sequence; there is nothing to merge. Becomes a requirement the day two holders may paint one grid (then each cell is an LWW register and the frame becomes a per-cell map), or the day sparse updates exist and can interleave.
- NR-HISTORY: the relay keeps no frames. A late viewer sees the next frame. Becomes a requirement if a source wants replay.
- NR-AUTH:
reserveis first-come. A name is a label, not an identity. Becomes a requirement when the relay leaves the LAN or the loopback trust domain (the crowsnest condition). - NR-CONF: frames are visible to every viewer of that display.
- NR-ENGINE: the sink never generates content. A test card, a demo animation, a Tetris engine are sources; if one is wanted for the no-holder case it is a source that reserves the display like any other.
- NR-QUANT: the sink never quantizes. Colour-to-index is a source or relay concern; the sink's input is indices, and a byte outside 0 to 15 is a bad frame, not a colour.
13. Conformance fixtures
Land with the core, under test/wal_sh/tools/display/:
capsfor every preset, and the resulting:cellslength;capsagainst a fixed grid, and the resulting:error.- one valid frame per format per preset; one frame of each wrong
length (off by one either way) per format; a
pal16frame with one byte of 16; ahexframe with oneg. - the same indices as
pal16and ashex, decoding to equal:cells. - a reordered frame (sequence prefix lower than last) and its
ratedrop. - the lease expiry sequence:
lease holder:nullfollowed by the black frame, and the DOM after it.
14. Refutation conditions
This spec is wrong if any of these is observed:
- A parameter outside its domain draws a grid instead of the error element.
- A frame of the wrong length, or with a cell outside 0 to 15, changes any cell.
- Two connections hold the same display at once, or a non-holder's frame is drawn.
- The DOM renderer takes more than 16 ms to paint a full 256 by 256 frame on a 2024 laptop; the grid cap is then too generous.
- The advertised
wss://wal.shendpoint cannot be reached from the page under the site's CSP once the relay is up; thesrclimit and the per-tool.htaccessare then out of step. - A
hexframe and apal16frame of the same indices render differently. - A relay's
capsresize a grid the URL fixed withwandh.
15. Open questions
- Hosting the relay: a long-lived process on the DreamHost VPS behind
Apache, or on a LAN box reached through the
srcparameter. The experiment above decides; until thenremoteis the honest default for anything not on loopback. - Whether
gbandmonoshould reject frames that use indices their palettes alias, rather than aliasing silently. Aliasing keeps every frame renderable; rejecting keeps sources honest. - Whether the sequence prefix should be mandatory. Optional matches SPEC section 2.3 sources today.
- Whether
capabilities.jsonneeds a publish path of its own: org-publish ships org and its attachments; a co-located JSON file needs the static component or an explicit rsync, the way.well-knownhas one.
16. Changelog
16.1. v0.2.1 (2026-09-11)
Display contract merged from the 17x9-Tetris SPEC v1 section 2:
presets blinkenlights (18 x 8, mono) and arcade (20 x 26, grey8);
kind per preset; palettes shorter than 16 entries reduce by the
level rule instead of aliasing to the last entry (gb becomes a ramp);
grey8 palette; levels and mono advertised per preset;
capabilities.json carries interop (BLP, MCUF), unconfigured
facades, and the property vocabulary, and is generated from the core's
tables by wal-sh.tools.display.capabilities. Kept against the
contract: cga40 aspect 1.2 (it wrote 1), hub75 at 60 fps (it caps
30), and RGB on the wire stays a relay concern (NR-QUANT).
16.2. v0.2.0 (2026-09-11)
Reconciled with issue #93. pal16 (one byte per cell) is the native
wire format and hex text the second; rgb24 left the sink for the
source shim and the relay (NR-QUANT). Preset id cga40; px; caps
may resize an unfixed grid; the postMessage and window.display
local-source surface; capabilities.json linked from the RFC 9727
api-catalog; the timed-reservation lease machine named; source recipes
with the ffmpeg pipeline; conformance fixtures and refutation
conditions extended for format symmetry and fixed grids.
16.3. v0.1.0 (2026-09-11)
First draft. Presets with CGA 40 by 25 as the default, formal query
parameters with reject-not-clamp, the lease-based wire protocol with
rgb24 and idx4 frames, numeric limits with refutation conditions,
the tool.display gate and three experiments, and NR-CRDT with its
trigger.