Skip to content

nEmacs + REPL + Input Model — Design Brief

After CIPHER-LINE landed (ADR-0015, PR #67), the earlier ADR-0002 / ADR-0008 / ADR-0009 designs needed re-examining against the new auxiliary display and against fresh Emacs-lineage research Josh surfaced. This brief captures the brainstorm reasoning; the formal decision record is in ADR-0016.


Josh’s research, summarized:

  • Emacs’s real innovation is not buffers or keybindings — it’s the editor as a live programming environment whose extension language is the same language the editor is written in, all the way down.
  • “Every command invocation in Emacs is a read-eval-print cycle” — keystroke → keymap lookup → eval. The keymap is a Lisp alist.
  • A minimal Emacs-shaped editor has five moving parts: buffer (two lists), point (boundary), C bridge (~6 primitives), keymap (alist), command loop (three lines).

This maps onto KN-86 with uncanny cleanness: 31 keys are already a keymap shape; Fe VM runs cart code; NoshAPI is the C bridge; the capability model has carts contributing behavior the way Emacs modes contribute.


  • ADR-0002 (2026-04-14): REPL launch-day with tutorial + history. nEmacs slips post-launch. Scripted missions always optional on critical path.
  • ADR-0008: Structural editor, CAR/CDR/BACK navigation, CONS predictive palette on main-grid rows 22–25, literal entry mode, 8-token T9 palette.
  • ADR-0009: Static, grammar-aware ranking — legal-form filter + vocabulary boost + local-id + recency + popularity.

Tension 1: keymap as Lisp data vs. C dispatch

Section titled “Tension 1: keymap as Lisp data vs. C dispatch”

Decision: Keymap in Lisp, mode-scoped, not runtime-rebindable.

Full Emacs doctrine (keymap is data, dispatch through eval) for mode-level composability. Not runtime-rebindable because the device is a game appliance, not a text editor — users don’t need to customize keys and we don’t want to manage that surface. Different modes (nEmacs-nav, nEmacs-literal, REPL-prompt) swap keymaps.

Decision: Unified surface, ship together launch-day. Supersedes ADR-0002’s “nEmacs post-launch” slip.

Emacs insight: the editor is the REPL. nEmacs is the editor; REPL is just a specific buffer type where EVAL submits the current input. One keymap machinery, one buffer model, one FFI. Shipping them apart was a seam we don’t need.

Tension 3: ADR-0008 palette location vs. ADR-0015 Rule 5

Section titled “Tension 3: ADR-0008 palette location vs. ADR-0015 Rule 5”

Decision: Palette moves to CIPHER-LINE in nEmacs mode.

ADR-0008 put the palette on main-grid rows 22–25, overlapping with ADR-0015 Rule 5’s firmware-owned Row 24 action bar. Moving to CIPHER-LINE:

  • Recovers the full 23 content rows for the editor buffer
  • Gives the auxiliary display its canonical “editor modeline” role
  • Pauses Cipher scrollback during editing (thematically correct — the deck’s voice mutes while you’re focused on code)

Decision: Context-polymorphic.

Cursor position determines what primitive keys mean. No Vi-style mode toggle. Cursor on a form → CAR descends (literally applies car at cursor); cursor in a literal buffer → numpad multi-tap; cursor at REPL prompt → keys build input, EVAL submits.

The objection “modal state adds cognitive load during gameplay” was partially moot once Josh clarified that nEmacs is invoked pre-run, not mid-mission. Editing happens calmly at the deck; the script is deployed and executes autonomously. But we still chose context-polymorphic because:

  • Semantic purity: keys mean their Lisp primitives in context.
  • No hidden state: cursor is always visible; the modeline on CIPHER-LINE Row 1 + the Row 24 action bar render current bindings.
  • Unified with REPL: EVAL at REPL prompt submits; EVAL on a form in nEmacs evaluates the form. Same key, same meaning in context.

Decision: T9-style multi-tap on numpad 2–9.

Prerequisite: phone-style keypad layout (1-2-3 top, 0 bottom-center) replaces calculator layout. Letters attach to phone-position muscle memory (2=ABC, 3=DEF, …). Timeout-based commit (~600ms), CIPHER-LINE Row 4 echo with cycle indicator.

Scope. Text entry on KN-86 is rare and short. Firmware-level: operator handle setup, nEmacs/REPL literal entry, snippet naming. Cart-level: ~3–5 of 14 carts realistically need it (Relay, Null, Black Ledger, The Vault, Cipher Garden). The rest orchestrate Lisp primitives; the 30 function keys do the work.

One FFI primitive (prompt-text) for cart-facing text input. The nOSh runtime owns the UI.

New: T9 prediction + cart grammar contributions

Section titled “New: T9 prediction + cart grammar contributions”

Decision: Extend ADR-0009 ranking model with cart FFI.

Cartridges can declare:

  • Grammar fragments (legal forms specific to their scripted missions — e.g., a cart that defines (scan-result) as a legal form)
  • Vocabulary terms (domain language that receives the +5 ranking boost)

Merges at cart-load into the per-cart grammar arena. Parallels the CIPHER-LINE grammar framework pattern.

Decision: Extend the raw key-event stream with KEY_TAP, KEY_DOUBLE_TAP, KEY_LONG_PRESS.

Every keymap entry gains three optional slots (:tap, :double-tap, :long-press). Most keys only bind :tap; double-tap and long-press are opt-in affordances for verb drill-down. Cartridges can bind all three levels, giving them more expressive power without requiring more physical keys.

Defaults: 300 ms double-tap window, 400 ms long-press threshold. Both firmware-tunable.

Nokia disambiguation: in :nemacs-literal / :prompt-text scopes, double-tap is suppressed on numpad (multi-tap wins); long-press remains usable. Context disambiguates; the two models never collide.

Row 24 rendering: when a key has multiple bindings, action bar shows superscript markers — CAR:SCAN CAR²:DEEP CAR…:SCOPE. Only bound slots rendered, no visual noise for plain-tap keys.

Separate Notion task owns the event-model implementation; parallel-compatible with the nEmacs/REPL foundation task.

Clarification (not a new decision): nEmacs/REPL are invoked before a mission starts, at the deck. Author your script, test primitives in the REPL, save, deploy. Mid-mission execution is autonomous. This is the “loadout” frame — write your logic, deploy, watch it execute.

Relaxes timing constraints on Nokia and makes modal overhead trivial.


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

Cipher scrollback pauses during nEmacs editing. Resumes on exit.


The answer to “what do these keys do right now” is uniformly Row 24 (firmware action bar), across every context:

ContextRow 24 source
Deck homeStatic verbs (Mission Board / REPL / Deck State …)
Cart gameplayCart’s Lisp handler renders its current verbs
nEmacsThe nOSh runtime renders verbs based on cursor context
REPLThe nOSh runtime renders prompt-mode verbs (EVAL:submit, CDR:history)

CIPHER-LINE Row 1 is the deck-level status modeline, not the key-level bindings surface. Two different modelines serving two different roles.


Open tuning knobs (land during implementation)

Section titled “Open tuning knobs (land during implementation)”
  • Multi-tap timeout default (600ms recommended)
  • Case toggle key (proposal: /)
  • Delete key (proposal: *)
  • Exact numpad key roles (1 = punctuation cycle? 0 = space?)
  • Cart grammar arena scope — shared 8 KB with Cipher grammar, or separate?
  • Mode-state rendering on Row 1 modeline vs. Row 24 action bar — division of labor

  • ADR-0002: nEmacs post-launch → ships with REPL. Otherwise intact.
  • ADR-0008: navigation model and row allocation superseded by ADR-0016. Kept as design exploration history.
  • ADR-0009: extended with cart-contributed grammar + vocabulary FFI.

Tracked in the Notion Tasks database under the KN86 tag. Five follow-on tasks queued:

  1. Phone-style keypad layout (spec + emulator)
  2. nEmacs + REPL firmware foundation (context-polymorphic keymap)
  3. Nokia multi-tap alpha entry
  4. T9 prediction engine + cart grammar FFI
  5. Gameplay specs revision across 15 carts (blocked on this PR merge)