Skip to content

ADR-0046: knEmacs is the application engine — first-party programs as modes

ADR-0042 chartered thirteen first-party programs and said they are “authored in KEC Lisp on DeckRunner’s Content tier” and live in runtime/programs/. It fixed what they are; it left open how they are built. That is the gap this ADR closes.

The observation that forces the choice: most of the thirteen are the same kind of thing — a structured-data tool the operator navigates and edits — and each already has an Emacs module as its conceptual ancestor. AmberCalc ≈ Calc, DOSSIER ≈ Org, kn9 ≈ a Gnus/Mutt message reader, Kinoshita Kommander ≈ Dired / a two-pane commander, RIPSAW ≈ a Speedbar tree, and nEmacs + the REPL are literally Emacs surfaces. The kec-lisp field notes establish the thesis (Glickstein): Emacs is a general-purpose application toolkit whose apps are thin because the substrate — keys→named commands→Lisp functions, buffers, modes, the minibuffer/completion surface, the rings — is rich. That substrate is app-agnostic, is shared by every one of these programs, and already exists as kec-lisp’s editor tier.

So the construction question is: build the thirteen as thirteen bespoke surfaces on the ui/ kit (the current per-screen pattern), or as modes on one shared engine?

  • DOSSIER, AmberCalc, and kn9 are next up for design/build. The construction model should be fixed before we hand-roll three different interaction models.
  • 34 keys + no mouse make discoverability the hardest UX problem. Solving navigation / command-by-name / completion once on a shared engine is the difference between one interaction grammar and thirteen drifting ones.
  • Arena-bounded Pi Zero 2 W; the engine and modes are KEC Lisp over a thin firmware/FFI seam (the field notes’ N-A column).
  • The engine partly exists already (kec-lisp editor tier: zipper / buffer / view / keymap / lifecycle / ranker / repl).
  • The buffer/structured-data model does not fit real-time or graphical experiences — that boundary must be explicit so nothing gets forced into it.

  1. knEmacs is the application engine for the first-party (data-tier) programs — not merely the on-device editor. The text editor is the engine’s first mode, not the engine itself.

  2. The engine is the app-agnostic substrate, built framework-first: keymaps-as-data → named KEC commands; buffers + buffer-local state; major/minor modes + hooks; the minibuffer / completion / command-by-name surface; the kill/mark rings + linear undo. It is built first because every program shares it and it is not speculative — and it already exists as kec-lisp’s editor tier, which this decision designates as that engine.

  3. Each first-party program is a mode on the engine: consume the basics, then extend per-domain following Emacs’s module patterns (a spreadsheet mode, an outline mode, a message-reader mode, …).

  4. Build the concrete program, then extract the reusable domain library when a second consumer needs it — the rule of three, with design-for-extraction (keep a program’s generic half behind a seam from day one). We do not build a generic per-domain framework before its first app.

  5. Scope line: the engine hosts the data / structured-navigation tier. Real-time and graphical experiences — carts, and any program surface that is fundamentally an animated/visual experience rather than a navigable data view — stay on the ui/ cell-API. Which side of that line a given program sits on is a per-program design-doc call, not this ADR’s.


Option A: knEmacs as the application engine; programs as modes; build-then-extract. (ACCEPTED)

Section titled “Option A: knEmacs as the application engine; programs as modes; build-then-extract. (ACCEPTED)”

Chosen because it steals the actual genius of the Emacs lineage these programs already descend from: one fabric, so navigation / command-by-name / completion / discoverability are solved once and inherited by every program — decisive on a 34-key device — and a new program is a thin mode, not a from-scratch surface. The engine isn’t speculative (every program needs it) and already exists as the kec-lisp editor tier.

Option B: Toolkit model — each program an independent surface on the ui/ kit (the status quo).

Section titled “Option B: Toolkit model — each program an independent surface on the ui/ kit (the status quo).”

Rejected for the data tier. It re-solves navigation, command-by-name, and discoverability per program, by convention instead of inheritance, and on 34 keys those conventions drift into thirteen inconsistent interaction models. It remains the correct model for the experience tier (carts and graphical surfaces — see the scope line); the ui/ kit is the shared render layer underneath the engine regardless.

Option C: Build a generic per-domain framework first (e.g. a generic outline library before DOSSIER).

Section titled “Option C: Build a generic per-domain framework first (e.g. a generic outline library before DOSSIER).”

Rejected as speculative generality — you guess the abstraction wrong against zero real consumers. The field notes’ own method (Glickstein builds concrete modes; the reusable patterns are distilled from them) and the build-then-extract rule (Decision 4) are the discipline instead.


Option A’s win is build-once + one interaction grammar where it matters most (the 34-key discoverability problem) plus composability and reprogrammability. Its honest costs: (1) the engine-promotion work — generalizing the kec-lisp editor tier from “a structural code editor” into a buffer + major-mode platform (the minibuffer/command/completion surface, mode dispatch, per-mode render hooks) — is real, and gated on the field notes’ keystone gap, vectors; and (2) the buffer worldview doesn’t fit everything, which is exactly why the scope line (Decision 5) fences real-time/graphical surfaces out to the cell-API rather than forcing them in. Both costs are accepted: the engine is shared by all data-tier programs so the promotion pays for itself, and the scope line keeps the model honest.


  • One interaction grammar (navigation, command-by-name, completion, discoverability) inherited by every data-tier program — the 34-key lifeline.
  • A new program is a thin mode, not a bespoke surface; thirteen builds collapse toward one engine + N modes.
  • Composability and reprogrammability (the “Lisp over shared state, in the language the operator types” thesis) become structural, not per-program.
  • ADR-0042’s roster is unchanged; this only fixes how the programs are built.
  • Engine-promotion (editor-tier → application platform) is real work, and gated on the vectors gap (keymaps / grids / rings need O(1) access; tracked in the kec-lisp field notes / a future kec-lisp ADR).
  • The buffer model excludes real-time/graphical surfaces; those stay on the cell-API (the scope line is the mitigation).
  • Promote the kec-lisp editor tier into the application engine (minibuffer/command/completion surface, mode dispatch, per-mode render) — owned in kec-lisp, gated on vectors.
  • Build DOSSIER as the first non-editor mode, with design-for-extraction (its generic outline/agenda half kept behind a seam) — the proving program for the build-then-extract rule.
  • Per-program design docs (ADR-0042’s outstanding ten) note their construction as engine-modes, and which side of the scope line they sit on.

Explicitly Not Decided (open — recorded so nothing here reads as committed)

Section titled “Explicitly Not Decided (open — recorded so nothing here reads as committed)”

This ADR locks posture, not particulars. The following are deliberately left open:

  • The program roster and which Emacs module seeds which program. Owned by ADR-0042. The AmberCalc≈Calc / DOSSIER≈Org / kn9≈mail / Kommander≈Dired mappings in this ADR are illustrations of the pattern, not commitments.
  • Per-program mechanics, layout, keybindings, glyphs. Per-program design docs.
  • The moldable-development / Glamorous Toolkit material (an object carrying multiple context-activated views; Spotter-style search) — captured here as inspiration to evaluate against the engine, NOT adopted. Specifically: ido-style incremental-narrowing completion is the committed minibuffer grammar; a Spotter-style extensible, object-result, drill-down search is a noted upgrade path, not a decision. Whether the engine’s unit of design grows from “mode” toward “object + views” is an open question for a later ADR if a second consumer justifies it.
  • Engine internals and the KEC stdlib gaps (vectors, regex subset, syntax tables, …) — the kec-lisp field notes and a future kec-lisp ADR.
  • Which specific programs sit on the engine vs. the cell-API. Decided per program against the scope line, not here.

Documentation Updates (REQUIRED — Spec Hygiene Rule 3)

Section titled “Documentation Updates (REQUIRED — Spec Hygiene Rule 3)”
  • docs/adr/ADR-0046-knemacs-application-engine.md — this file (incl. the 2026-06-28 / GWP-646 Amendment Log).
  • docs/adr/README.md — index row added; marked as amending ADR-0042; amended-row note added 2026-06-28 (GWP-646).
  • Per-program design docs under docs/software/programs/ (ADR-0042 follow-ons) — when written, state the program’s construction as an engine-mode and its side of the scope line. (Non-blocking; tracked with the ADR-0042 doc debt.) The 2026-06-28 amendment lands with keyring.md (one clean mode — the worked example) and bzbx.md (waterfall now an in-mode soft-glyph option) already on the sharpened criteria.
  • docs/software/cartridges/authoring/animation.md — Keyring added as a 2nd worked in-mode consumer (2026-06-28 / GWP-646).
  • kec-lisp: the engine-promotion work + the vectors gap are tracked in the kec-lisp field notes / a future kec-lisp ADR (cross-repo; the engine is a kec-lisp tier).
  • ADR-0042 is immutable and is not edited; this ADR is the recorded amendment to its construction model.

ADR-0042 named the deck’s thirteen first-party programs but left a quieter question hanging: are they thirteen hand-built screens, or thirteen faces of one thing? The field notes answer it — almost every one of them is a structured-data tool with an Emacs module for an ancestor, and Emacs’s real lesson is that those apps are thin because the substrate is rich: keys become named commands become Lisp functions, buffers and modes and a completion surface carry the weight, and the apps just lean on it. The deck already has that substrate, sitting in kec-lisp’s editor tier. So the decision is to stop treating knEmacs as “the editor” and start treating it as the engine every data-tier program is a mode on — solve navigation and discoverability once, on a keyboard that can’t afford to solve them thirteen times, and build each program by writing a thin mode and harvesting the reusable half only when a second program asks for it. What this ADR deliberately does not do is decide the roster, the mechanics, or whether to chase the richer “object with many views” idea the moldable-development world points at — those stay open on purpose. It fixes the shape of the thing and leaves the contents to be filled in by doing.


Amendment 2026-06-28 — the scope line sharpened: a MODE animates IN-MODE on the idle-timer; the cell-API is the render layer, not a separate surface (per GWP-646)

Section titled “Amendment 2026-06-28 — the scope line sharpened: a MODE animates IN-MODE on the idle-timer; the cell-API is the render layer, not a separate surface (per GWP-646)”

Status effect: Accepted (unchanged). This amendment sharpens Decision 5’s scope line and revisits the SPLIT verdicts it had implied; it changes no other Decision, the roster, the Options analysis, or any engine/FFI surface. No new primitive.

Decision 5 drew the scope line — “the engine hosts the data / structured-navigation tier; real-time and graphical experiences … stay on the ui/ cell-API” — at a moment when the engine had no clock or loop. A mode could render its cells once, in response to an event; it could not make them move over time. So any program whose surface had a timed, moving beat — Keyring’s crack, bzbx’s capture waterfall — read as “fundamentally an animated/visual experience” and got flagged toward the cell-API, producing SPLIT construction verdicts (a knEmacs mode for the data half + a separate thin cell-API passenger for the animated half).

Two pieces have since landed that dissolve that read:

  • The animation substrate (GWP-644) — an idle-timer registry (run-with-timer), a monotonic now, and read-key/poll-key, pumped from the host loop (screen_router_pump_timers). A mode can now arm a repeating timer that advances a state counter and re-render its own cells each tick. The engine gained the clock+loop it was missing; the cell-API was always the render layer underneath it (Decision 5 / Option B: “the ui/ kit is the shared render layer underneath the engine regardless”).
  • The soft-glyph animation plane + two-style model (ADR-0052)render/soft-define + render/soft-fill give O(1)-per-frame uniform field motion (the soft-glyph style), beside the already-shipped O(cells) per-cell render/glyph (the glyph-cycle style). Together they let a mode paint loaders, reveals, dither, and ambient texture inside its own cells, cheaply, without leaving the grammar surface.

Keyring is the proving case: its crack — a timed, moving operation — collapsed the earlier SPLIT verdict into one clean keyring-mode (software/programs/keyring.md §“Scope-line”; design record plans/2026-06-27-keyring-crack-design.md (deleted 2026-07-29; recover from git history)). Input stays grammar (completing-read + EVAL); the RUN loader animates via soft-glyph and the RESOLVE reveal via glyph-cycle, both idle-timer-driven and guarded on (provided? 'editor/timer). There is no cell-API passenger and no second surface.

N1 — A knEmacs MODE may animate IN-MODE by re-rendering its own cells on the idle-timer. A mode is not restricted to event-driven single renders. It MAY arm a repeating idle-timer (run-with-timer, GWP-644) that advances a state counter and re-render its cells each tick, via the ADR-0052 two-style model: soft-glyph (render/soft-define + render/soft-fill) for O(1)-per-frame uniform fields (loaders, dither, scanlines, ambient texture), and glyph-cycle (render/glyph per cell) for O(cells) spatially-varying fields (reveals, dissolves, heatmaps). Animation rides within Rows 1–73; chrome (Rows 0/74) is firmware-owned. Timers are armed lazily and guarded on (provided? 'editor/timer) so a substrate-less host degrades to a static frame and never raises.

N2 — The cell-API is the shared render layer + the heavy real-time/pixel tier, NOT a separate surface a data/nav program must “drop to.” Decision 5’s phrasing (“real-time and graphical experiences … stay on the ui/ cell-API”) is not a rule that any moving output exiles a program from the engine. The cell-API is the render layer the engine paints through (Option B), and additionally the home of the genuinely heavy tier — active-loop/control or pixel-addressed experiences (ADR-0047: tactical/experience surfaces, render_bitmap, the 20 fps action ceiling). Timed, moving output inside a mode is the former (in-mode), not the latter.

N3 — The scope line is re-read against the ADR-0047 axes, not against “does it move.” What crosses to the cell-API/experience tier is interaction model + address, not motion per se: a surface crosses when it is fundamentally active-loop/control (a twitch/real-time control loop) and/or pixel-addressed (sub-cell free pixels — cursors, reticles, smooth sprites). A surface stays in-mode when its input is grammar (event/command — keys → named commands → completing-read) even if its output animates. “It animates” no longer implies “it leaves the engine”; “you twitch-control it in a render loop” or “it needs raw pixels” does.

N4 — SPLIT verdicts are revisited; default to a single clean mode where input is grammar. A program previously flagged SPLIT (engine mode + cell-API animation passenger) is reconsidered: if its input is grammar and its motion is in-mode field animation (N1), it is one clean major-mode, no passenger. Keyring is the worked example of the collapse (SPLIT → one keyring-mode). bzbx is the remaining candidate: its capture-waterfall is now an in-mode soft-glyph option, not a forced cell-API surface — its primary capture viewer is the engine tabulated-list, and the waterfall is optional texture a cart may add (software/programs/bzbx.md §“Scope-line”). The per-program design doc still makes the call (Decision 5’s “per-program design-doc call” is unchanged); what changes is the default and the criteria (N3), not the locus of the decision.

  • Attract / ambient / CIPHER-ticker. The same in-mode idle-timer pattern is what FIREPLACE/animlab attract screens already use (system-tier render, not a mode — but the same substrate). The OLED CIPHER-LINE ticker is unaffected: CIPHER stays OLED-exclusive (ADR-0015); a main-grid mode animating in-mode never renders CIPHER glyphs on the main grid (the Null cartridge remains the sole exception). A mode firing events (e.g. Keyring’s crack-progress) to drive the OLED voice is the sanctioned cross-display path — unchanged.
  • Decisions 1–4 (engine = application substrate; framework-first; program = mode; build-then-extract) — verbatim.
  • The roster — owned by ADR-0042, untouched.
  • The Options analysis and the open questions — unchanged; this only sharpens Decision 5’s reading.
  • The “per-program design-doc call” of which side of the line a program sits on (Decision 5, last sentence) — preserved; the amendment fixes the criteria + default, not who decides.
  • The vectors-gap gating on engine-promotion — unchanged.
  1. Front-matter Amended: line — appended the 2026-06-28 / GWP-646 entry.
  2. Related: line — added ADR-0052 (the two-style model) and ADR-0047 (the surfaces that do cross the line).
  3. This Amendment Log section — N1–N4 + implications.

Authority trail. The animation substrate (GWP-644 — idle-timer / now / run-with-timer; the FIREPLACE proof); the soft-glyph plane + two-style model (ADR-0052, authoring guide software/cartridges/authoring/animation.md); the experience-tier axes (ADR-0047); the worked example (software/programs/keyring.md + plans/2026-06-27-keyring-crack-design.md (deleted 2026-07-29; recover from git history)); the remaining candidate (software/programs/bzbx.md).