ADR-0059: PROGS, the bare-deck program launcher
roster, unchanged by this ADR), ADR-0005 (launch-app, the primitive
this surface calls), ADR-0016 (numpad input model; the
digits-are-data invariant), ADR-0036 (integer-scale glyph
composition, which the icon tiles use), bare-deck-terminal.md
(the tab contract this adds to), progs-launcher.md
(the per-view spec), ascii-design-system.md
(the components PROGS composes from)
Context
Section titled “Context”Nothing on the deck could launch a program
Section titled “Nothing on the deck could launch a program”The Bare Deck Terminal registers six tabs (runtime/system-image/lib/deck/deck.lsp): STATUS,
CIPHER, LAMBDA, LINK, SYS, MISSIONS. None of them calls launch-app. Verified by grep: there are
zero launch-app call sites anywhere under runtime/system-image/lib/deck/.
So on a shipping deck the only routes to a first-party program are the REPL (TERM, then
(launch-app :knamp)) and the dev recorder (kn86rec --screen <name>). Nine built programs
(CONDUIT, Kommander, AmberCalc, RIPSAW, Keyring, kn9, DOSSIER, bzbx, WavAmpster) are unreachable by
an operator who has not been told to type a Lisp form. ADR-0042 chartered a toolkit and gave it no
door.
The router already holds the list
Section titled “The router already holds the list”runtime/src/screen_router.c carries two tables that are easy to confuse:
| table | entries | what it is |
|---|---|---|
k_programs[] | 14 | The ADR-0042 roster plus WavAmpster. launch-app resolves against this; an off-roster id raises :unknown-app, and a chartered-but-unimplemented one returns false. |
k_program_libs[] | 19 | Which registered screens need their own lazy Fe context. Includes ten lab and demo screens (fireplace, animlab, linelab, whirl, wirelab, trace, echo, bonsai, bootseq, calc) that are not launchable. |
k_programs[] is already closed by ADR, already authoritative, and already excludes the labs. A
launcher needs no second list.
PROGS is a shell surface, so the roster does not change
Section titled “PROGS is a shell surface, so the roster does not change”A launcher could be read as a fifteenth first-party program, which would demand an ADR-0042
amendment. It is not one. PROGS is a tab of the bare-deck shell, in the same category as STATUS and
MISSIONS: it renders inside deck.lsp, holds no state of its own beyond a cursor and two view
modes, and routes. ADR-0042’s roster stays at fourteen.
Decision
Section titled “Decision”The bare deck gains a seventh tab, PROGS, which lists every entry in k_programs[] and launches
the selected one.
-
Placement. One
deck/register-tabcall, appended after MISSIONS. No other tab’s code is touched. It lands at tab index 7 and inherits CDR cycling and digit-jump from the shell. -
Contents. All fourteen
k_programs[]entries, flat. No groups, no folders, no hierarchy. Lab and demo screens stay off the list; making one launchable is ank_programs[]change, which is an ADR-0042 decision, so it happens there. A chartered-but-unimplemented program (today, knSALK) renders dimmed and does not launch. -
Two sorts, cycled by
EQ: A-Z (case-insensitive ascending) and LAST USED (most recent first, never-run entries last). Ascending only; there is no descending mode. -
Two views, cycled by
QUOTE: LIST (one row per program: name, one-line purpose, and a right-hand state column) and ICON (a tile grid, nine across at 1x, each tile a 7x3 cell mark with the program name beneath). -
Icon tiles are drawn from existing glyphs. A tile is 7x3 cells (56x24 px) composed only from codes already in
runtime/src/font.c. This is deliberate: a multi-cell tile needs no new font bytes, so the twelve reserved single-cell icon slots (0x14-0x1F) stay unspent. The tile set lands as anicon/tileslot in the ASCII design system. -
Chrome is an explicit control row: one row above the content rule showing both options of each mode with the active one marked. The operator can see that the other sort and the other view exist without pressing anything.
-
Key map.
CONS/CDRmove,EVALruns,INFOopens the right-docked detail peek,EQcycles sort,QUOTEcycles view,BACKleaves, digits1-9quick-run the Nth entry.APPLYalso launches.bare-deck-terminal.mdrecords APPLY as the one key with no bare-deck function, on the reasoning that APPLY deploys tools and a bare deck has no tools. PROGS is the deck’s tools, so the gap closes here and nowhere else; APPLY remains inert on the other six tabs. -
Return path.
BACKout of a program returns to PROGS with that program selected.Inside the tab,
BACKpeels one layer at a time: it closes the detail peek if open, otherwise it leaves the list. -
LAST USEDpersists in Universal Deck State. A fourteen-entry last-used table keyed by roster index is roughly 56 bytes. If the UDS region needs to grow to hold it, grow it. The 4 KB figure is a number this project chose; enlarging it is aDECK_STRUCT_VERSIONbump and a loader path, the same cost as any other schema change. Donor-field scavenging is one option among several and is not required.
Implementation note (2026-07-27, same-PR): the shell needed a mode gate
Section titled “Implementation note (2026-07-27, same-PR): the shell needed a mode gate”Decision 7 above assumed a tab could see a digit. It could not. The bare-deck shell
consumed every digit as a tab jump before the active tab’s handler ran, and CAR
likewise never reached a tab, so “digits are data inside a mode” had nowhere to live.
Two small, backward-compatible additions to deck.lsp close that:
*deck-digit-claim*: one shell flag a tab raises while it is inside an interactive mode. The shell routes a digit to the tab only while the flag is up, and clears it on every tab change so a claim cannot leak. PROGS raises it onCARand drops it onBACK. Digits stay STATUS-anchored tab jumps otherwise, so theinput-dispatch.md§3D invariant holds exactly as written.- “non-nil means consumed”: a tab’s
on-keymay now return non-nil to tell the shell not to also act on that key.CARis offered to the active tab first and falls through to the shell’s previous-tab step when the tab declines. Every tab written before this convention returnsnil, so none of them change behaviour.
CDR is deliberately not offered to tabs: it always cycles tabs. The key table in
progs-launcher.md §7 reflects that.
Options Considered
Section titled “Options Considered”Option A: A seventh bare-deck tab. (ACCEPTED)
Section titled “Option A: A seventh bare-deck tab. (ACCEPTED)”Chosen because it matches the existing deck/register-tab registry exactly, needs no edits to
any other tab, inherits the shell’s navigation grammar for free, and puts the launcher at the same
level as the other things an operator reaches between missions.
Option B: A section inside the SYS tab.
Section titled “Option B: A section inside the SYS tab.”Rejected because SYS is settings and diagnostics. A launcher is neither, and burying a primary surface one level inside SYS costs two extra keypresses on every launch.
Option C: A command palette only.
Section titled “Option C: A command palette only.”ui/palette already exists in ui/ui/compositing.lsp, so a type-to-filter launcher is nearly free.
Rejected as the sole answer because a palette shows nothing until the operator types, and an operator who does not know CONDUIT exists will never type “con”. Discovery has to come first. The palette remains attractive as a later accelerator layered over PROGS, and is tracked as a follow-on.
Option D: A fifteenth first-party program.
Section titled “Option D: A fifteenth first-party program.”Rejected because the launcher has no state, no save data, and no domain. It is shell chrome, and promoting it would force an ADR-0042 amendment for no gain.
Trade-off Analysis
Section titled “Trade-off Analysis”| Dimension | A: seventh tab (chosen) | B: inside SYS | C: palette only | D: 15th program |
|---|---|---|---|---|
| Discoverable without prior knowledge | ✓ | ◐ buried | ✗ | ✓ |
| Keypresses to launch from boot | ✓ 2 | ✗ 4 | ✓ 2 | ◐ |
| Edits to existing tab code | ✓ none | ✗ SYS rewrite | ✓ none | ✓ none |
| ADR-0042 roster untouched | ✓ | ✓ | ✓ | ✗ amendment |
| Reuses an existing component | ✓ list/panel | ✓ | ✓ ui/palette | ✓ |
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Nine built programs become reachable without typing a Lisp form.
k_programs[]gains an operator-facing consumer, so the roster stops being an internal detail.- APPLY earns a bare-deck meaning on exactly the tab where it is fictionally correct.
- The icon-tile construction rule (multi-cell marks from existing glyphs) is reusable by any future surface that wants pictorial identity without spending font slots.
Negative / Accepted costs
Section titled “Negative / Accepted costs”- The tab strip grows to seven, and the digit map in
bare-deck-terminal.mdneeds a sweep. That doc already describes four tabs against the six that ship, so the drift predates this ADR; this change makes fixing it unavoidable. - The icon tile set is fourteen pieces of drawn art that need design attention and upkeep as the roster grows.
LAST USEDadds a UDS field and therefore a struct-version bump.
Follow-on work this ADR creates
Section titled “Follow-on work this ADR creates”- The
icon/tileslot and the chosen fourteen tiles land inascii-design-system.md, with the implementation inui/ui/. ui/paletteas a type-to-filter accelerator over PROGS (Option C, deferred).- The state-hint sources per program (WavAmpster’s live station, kn9 unread, DOSSIER record count, Keyring key count) are read-only reads that each need a seam.
Documentation Updates (REQUIRED: Spec Hygiene Rule 3)
Section titled “Documentation Updates (REQUIRED: Spec Hygiene Rule 3)”-
docs/adr/ADR-0059-progs-app-launcher.md: this file -
docs/adr/README.md: index entry for ADR-0059 -
docs/software/runtime/progs-launcher.md: the per-view spec -
docs/software/runtime/bare-deck-terminal.md: seventh tab added; the four-tab digit map corrected to the six that ship plus PROGS at 7 -
docs/software/cartridges/authoring/ascii-design-system.md: theicon/tileslot and the locked 14-tile PROGS set (§4b)