Skip to content

ADR-0016: nEmacs + REPL + Input Model (Post-CIPHER-LINE)

Three prior ADRs specified the player-facing Lisp subsystem:

  • ADR-0002 committed to the REPL + nEmacs + scripted-missions trio, with nEmacs slipping post-launch.
  • ADR-0008 specified the structural editor UX — CAR/CDR/BACK navigation, predictive palette on main-grid rows 22–25, literal entry mode.
  • ADR-0009 specified the static token-prediction ranking model.

Two forcing functions made these specs need revision:

  1. CIPHER-LINE (ADR-0015) reserved main-grid Row 24 as the nOSh runtime action bar. ADR-0008’s palette on rows 22–25 overlapped.
  2. Emacs-lineage research surfaced the “keymap is a Lisp alist” insight and the editor/REPL unification — prior ADRs treated them as separate subsystems sharing a VM.

Josh convened a brainstorm on 2026-04-24 that resolved the tensions. Full reasoning in docs/plans/2026-04-24-nemacs-repl-input-design.md. This ADR ratifies.


1. REPL + nEmacs ship together, launch-day

Section titled “1. REPL + nEmacs ship together, launch-day”

The ADR-0002 split (“REPL launch-day, nEmacs post-launch”) is canceled. Both ship in firmware v1.0. They are not two subsystems sharing a VM — they are one subsystem with two buffer types: an editor buffer and a REPL buffer. Same keymap machinery, same cursor model, same FFI.

2. Keymap is a Lisp alist, mode-scoped, not user-rebindable

Section titled “2. Keymap is a Lisp alist, mode-scoped, not user-rebindable”

Per Emacs doctrine, the keymap is a Lisp data structure (alist). The command loop dispatches through eval: scancode → keymap lookup → eval the bound handler.

Modes are distinct keymap scopes:

  • :nemacs-nav — cursor on a form node; primitive keys apply Lisp semantics to the form at cursor.
  • :nemacs-literal — cursor inside a literal-entry buffer; numpad becomes multi-tap; primitive keys commit-and-return or are suppressed.
  • :repl-prompt — cursor at REPL input line; keys build the input sexp; EVAL submits.
  • :repl-history — cursor browsing prior REPL forms; CDR advances; EVAL re-executes.
  • :grabQUOTE-entered mode for cut/copy of a subtree.

Each keymap entry supports three optional handler slots: :tap, :double-tap, and :long-press. See §9 for the event model. Most keys only bind :tap; double-tap and long-press are opt-in affordances for verb drill-down.

Not runtime user-rebindable. Mode-level composability is available; per-key user customization is not. Cartridges don’t rebind nOSh-runtime-owned keys, and users don’t have a key-rebinding UI. This is deliberate — the device is a game appliance, not a text editor.

3. Context-polymorphic key dispatch inside nEmacs / REPL

Section titled “3. Context-polymorphic key dispatch inside nEmacs / REPL”

Inside nEmacs/REPL, the cursor position determines what primitive keys mean. There is no Vi-style mode toggle.

  • Cursor on a form node → primitive keys apply their Lisp semantics at cursor. CAR = descend (applies car to the form); CDR = next-sibling (applies cdr); EVAL = evaluate current form; CONS = open palette for insertion.
  • Cursor inside a literal-entry buffer → numpad is Nokia multi-tap; primitive keys commit-and-return or are suppressed.
  • Cursor at REPL prompt → keys build the input sexp; EVAL submits.

Non-nEmacs cart gameplay is unaffected. Cartridges define their own key verbs via their Lisp handlers. Row 24 (firmware action bar, per ADR-0015) renders current bindings uniformly across every context — that’s how the player always knows what the keys do.

4. CIPHER-LINE hosts the editor modeline / palette / echo in nEmacs mode

Section titled “4. CIPHER-LINE hosts the editor modeline / palette / echo in nEmacs mode”

CIPHER-LINE is the editor’s auxiliary surface while nEmacs is active:

RowPurpose
1Modeline: [NEMACS] buffer-name L12/45 * [TERM:SAVE]
2Palette Row A: 8 T9 candidates numbered [1]..[8]
3Palette Row B: continuation / selection indicator
4Echo area — doubles as Nokia multi-tap buffer during literal entry

Cipher voice scrollback pauses during nEmacs editing. The deck’s voice mutes while the operator is focused on code. Scrollback resumes on nEmacs exit.

This recovers the full 23 content rows on the main grid for the editor buffer — a direct gain from ADR-0008’s earlier rows-22–25 palette allocation.

The 16-key numpad is rearranged from calculator layout to phone layout:

  • Calculator (superseded): 7-8-9 top, 4-5-6 middle, 1-2-3 third, 0 bottom-left.
  • Phone (canonical): 1-2-3 top, 4-5-6 middle, 7-8-9 third, 0 bottom-center. Arithmetic ops (/, *, -, +) stay in the 4th column; . and ENT retain positions flanking 0.

Phone layout is a prerequisite for Nokia multi-tap (§6) — letters attach to phone-position muscle memory. Calculator layout would misalign the mapping.

Literal text input uses T9-style multi-tap on numpad keys 2–9:

KeyLetters
2A B C
3D E F
4G H I
5J K L
6M N O
7P Q R S
8T U V
9W X Y Z
0space
1punctuation cycle (. , ' " : ; !)

Case toggle: dedicated key (proposal: / numpad; final during implementation). Delete: dedicated key (proposal: * numpad; final during implementation).

Timeout-based commit (~600ms default after last keypress auto-commits current letter) with ENT for immediate commit. Pressing a different letter-key commits the current letter and advances position.

CIPHER-LINE Row 4 is the echo surface. During multi-tap, shows the string being typed + a cycle indicator for the current letter ([A_][B_][C_] mid-cycle, then commits to [AB_] with the next press).

Character set is ASCII letters + digits + a handful of punctuation. CP437-compatible.

Nokia vs. double-tap disambiguation. In :nemacs-literal and :prompt-text keymap scopes, KEY_DOUBLE_TAP events are suppressed on numpad keys — every press is multi-tap letter cycling. KEY_LONG_PRESS remains available (e.g., hold 2 for rapid A-B-C cycle). Mode context disambiguates; the two models never step on each other. See §9.

7. T9 prediction at point + cartridge grammar contributions

Section titled “7. T9 prediction at point + cartridge grammar contributions”

The predictive palette runs the ADR-0009 static ranking model (legal-form filter + vocabulary boost + local-identifier boost + recency + popularity). Extended in this ADR with two FFI primitives for cart contributions:

  • emacs-extend-grammar <sexp> — cart contributes grammar productions. E.g., a cart that defines (scan-result) as a legal form in its mission-script grammar.
  • emacs-extend-vocabulary <list-of-strings> — cart contributes domain terms that receive the +5 vocabulary boost in ranking.

Cart grammar + vocabulary merge at cart-load into the per-cart grammar arena (sizing TBD during §4 implementation task — shared 8 KB with Cipher grammar, or separate). Cleared on cart unload.

8. prompt-text FFI for cart-facing text entry

Section titled “8. prompt-text FFI for cart-facing text entry”

One FFI primitive exposed for cart-authored text input:

  • prompt-text <prompt-string> <max-len> — nOSh-runtime-owned modal dialog. Uses Nokia multi-tap on CIPHER-LINE Row 4. Returns the entered string, or nil if canceled. TERM or BACK cancels.

Scope: 3–5 of 14 carts realistically need it (Relay, Null, Black Ledger, The Vault, Cipher Garden per initial analysis). The rest orchestrate Lisp primitives and don’t need raw text.

9. Double-tap and long-press key event model

Section titled “9. Double-tap and long-press key event model”

The keymap supports three event types per key, not just one. This extends §2 (keymap as Lisp alist) with richer per-key binding and §3 (context-polymorphic dispatch) with richer per-event resolution. Platform-wide — every cart, mode, and firmware context can use it.

  • KEY_TAP — single short press. Fires on key-up, after the double-tap window closes with no follow-up press.
  • KEY_DOUBLE_TAP — second key-down within the double-tap window of the previous key-up.
  • KEY_LONG_PRESS — key held past the long-press threshold. Fires during the hold (not on release), so the UI can reflect the alt binding activating before it commits.

Raw KEY_DOWN and KEY_UP events remain for runtime-level consumers (debug overlays, power-management wake detection, etc.). The three derived events sit on top.

Each keymap entry gains three optional handler slots:

(define-key map 'CAR
:tap 'descend
:double-tap 'descend-to-leaf
:long-press 'show-context-path)

Missing slots fall back to :tap (or no-op if :tap is also unbound). Most keys in most contexts will only bind :tap; double-tap and long-press are opt-in affordances for verb drill-down.

  • Double-tap window: 300 ms (iOS/Android convention).
  • Long-press threshold: 400 ms (Android convention; snappier than iOS 500 ms).

Both are firmware-tunable constants. Accessibility-conscious defaults — don’t go aggressive-fast.

In :nemacs-literal and :prompt-text scopes, KEY_DOUBLE_TAP is suppressed on numpad keys so multi-tap letter cycling (§6) works cleanly. KEY_LONG_PRESS remains available on those keys. Context disambiguates; the two models never collide.

When a key has multiple bindings at the current context, Row 24 renders them with superscript markers:

CAR:SCAN CAR²:DEEP CAR…:SCOPE

Superscript ² = double-tap; = long-press. Plain tap has no marker. Only bound slots render (no visual noise for single-tap keys).

The keyboard overlay (GWP-210) adopts the same convention when context provides multi-level bindings.

Cartridges define keymap entries with any of the three slots via define-key. This gives carts more expressive power without requiring more keys — a Relay cart might bind INFO:preview INFO²:stats INFO…:verbose-log for varying drill-down depth on a single key.

Event detection is firmware, not Lisp-interpreted

Section titled “Event detection is firmware, not Lisp-interpreted”

Event timing + classification happens in C (input.c). Lisp handlers only see the three derived events, never raw timing. Keeps the event model fast and deterministic with a clean FFI boundary.


  • Authoring consistency. The same keymap model drives nEmacs, REPL, and cart gameplay.
  • Test surface. Keymap-in-Lisp means dispatch is testable without SDL — it’s lookup + eval over structured data.
  • Cart extensibility. Cartridges contribute grammar/vocabulary the same way they contribute Cipher fragments.
  • Launch scope. nEmacs + REPL shipping together avoids a post-launch seam that would have required a second adoption moment for players.
  • Firmware implementation. Dispatching through Lisp adds indirection vs. a direct C switch on scancode. Arena discipline must cover the keymap data alongside cart code.
  • Documentation discipline. ADR-0002 / ADR-0008 / ADR-0009 need visible amendments pointing at this ADR as current.
  • Tuning knobs multiply. Multi-tap timeout, case toggle, delete key, arena budgets — all need defaults and testing.
  • Relay messaging scope. If Relay ends up with central messaging, the “rare short modal” text assumption breaks. Nokia may need autocomplete / prediction.
  • Cart grammar arena sizing. Shared 8 KB with Cipher grammar, or separate? Lands during the nEmacs/REPL foundation task.
  • Runtime keymap customization. Not supporting user-level rebinding at v1. If community feedback later demands it, the mechanism exists (add a config surface).

Documentation Updates (per Spec Hygiene Rule 3)

Section titled “Documentation Updates (per Spec Hygiene Rule 3)”

This ADR lands changes in these files in the same PR:

Delegated (tracked in Notion KN86 queue, blocked on this PR merge):

  • 15 gameplay specs under docs/gameplay-specs/ — “nEmacs Contributions” section added per cart (grammar fragments, vocabulary), parallel to the existing GWP-201 CIPHER-LINE Contributions section.
  • docs/ui-design/KN-86-Input-System-Architecture.md — keypad layout flipped to phone-style; TERM context-sensitivity table extended.
  • kn86-emulator/src/keyboard_overlay.c — digit key positions flipped to phone-style.

Tracked in Notion Tasks DB under the KN86 tag:

TaskOwnerPriorityDepends on
Phone-style keypad layout — spec + emulatorDocs + C EngineerHighThis PR merge
nEmacs + REPL firmware foundationC EngineerHighThis PR merge + phone layout
Nokia multi-tap alpha entryC EngineerMediumFoundation
T9 prediction engine + cart grammar FFIC EngineerMediumFoundation
Input event model: double-tap + long-press semantics (§9)C EngineerMediumThis PR merge (parallel-compatible with foundation)
Gameplay specs revision (15 carts)Gameplay DesignMediumThis PR merge (parallel with code)

  1. Multi-tap timeout tuning. 600 ms is the starting default. Real-world feel during playtest may argue for 500 or 750 ms. Firmware-tunable knob.
  2. Case toggle key. Proposal /. May change during implementation based on ergonomics of the phone-layout numpad.
  3. Delete key. Proposal *. Same note.
  4. 1-key punctuation cycle. The 7 punctuation glyphs on the 1-key are a first cut; if a cart author or scripted-mission writer needs a symbol not in the cycle, we’ll extend.
  5. Cart grammar arena scope. Share the 8 KB Cipher grammar arena, or allocate a separate arena? Probably separate for isolation; confirm during foundation task.
  6. REPL ↔ nEmacs buffer handoff. If a player edits a snippet in nEmacs and wants to test it in the REPL, what’s the invocation? Probably SYS → Test or a dedicated key. Design during implementation.
  7. Double-tap / long-press timing tune. 300 ms / 400 ms are accessibility-conscious defaults. If playtest reveals the window is too forgiving or too strict, firmware constants are tunable. The conservative defaults ship in v1.
  8. Cart opt-in patterns for §9. How many carts should offer multi-level bindings, and where is the cost-of-discoverability line? Let cart authors decide per spec; revisit once a few carts have been authored against the event model.

The Emacs research didn’t change the architecture — it validated it. The KN-86’s 30-key Lisp-primitive keyboard was always an Emacs-shaped idea, just not stated that way. “Every keystroke is already a read-eval-print cycle” is a literal description of what the device does whether or not we write the keymap in Lisp or C.

The real productive tension in the brainstorm was the split between “keys mean what their label says” (Lisp purity) and “keys are repurposed per-mode” (editor ergonomics). Context-polymorphism resolved that without Vi-style modal overhead — the cursor position becomes the “mode,” and because cursor position is always visible on the screen, on CIPHER-LINE Row 1’s modeline, and in the Row 24 action bar, there’s no hidden state to track.

The pre-run framing of nEmacs/REPL also de-risked the design. If the operator authors their script at the deck before deploying it, the typing speed and modal overhead concerns relax — there’s no time pressure, the operator has room to think. That’s why we could commit to multi-tap Nokia input (slower than a real keyboard) and a rich structural editor (learning curve) without those becoming gameplay frictions. The gameplay frame is loadout-style: write your logic, deploy, watch it execute.

What’s left to build is substantial but well-scoped: four firmware tasks (layout, foundation, Nokia, T9) + one gameplay-spec update pass. Launch-day readiness for the full player-facing Lisp subsystem is now a plausible target.