Skip to content

Input Dispatch

Specification for the KN-86 input system — the event model, hold/multi-tap detection, nOSh runtime services, and the cartridge SDK surface. The canonical physical layout is the 34-key Ferris Sweep split + 2 trackpoints (see CLAUDE.md Canonical Hardware Specification and ADR-0031 / ADR-0032); key meaning follows the anchors + layers model (ADR-0040). The 31-key matrix in the physical-layout sections below is historical — see the banner.

See CLAUDE.md Canonical Hardware Specification for the canonical keyboard count and TERM row entry.

⚠ Stale layout — superseded; pending a keyboard-sweep rewrite. This doc still describes the retired custom 31-key matrix. The deck now uses the Ferris Sweep 34-key split + 2 trackpoints (ADR-0031 / ADR-0032), and key meaning is governed by the anchors + layers model (ADR-0040): a fixed anchor set (BACK / EVAL / SYS) plus DeckRunner-swapped layers, not a fixed key map. The event-model semantics below (hold detection, multi-tap, context-polymorphic TERM) remain broadly valid; the physical-layout sections are historical until the keyboard sweep lands. The current KEC-complete SHIFT manifest (4-5-6 → < = >, etc.), the unprinted L2 symbol layer (LSHIFT+TERM tri-layer), and the EQ→is authoring binding are in ADR-0044; the §1/§3D/§8D shift-secondary tables below (" : + ( ), * # bottom-row) predate both ADR-0031 and ADR-0044 and are superseded by it.


The KN-86 Deckline has 31 keys total: 30 cart-facing primary keys arranged in two distinct grids separated by the amber LCD, plus 1 TERM key whose binding is determined by the nOSh runtime per context (§3C and §6D). The operator’s left hand works the function grid (14 keys) and the right hand works the data grid (16 keys). Two positions in the 8×4 primary matrix are empty; the TERM key sits on a dedicated switch to the right of the function grid’s fourth row, wired into the matrix at one of the two previously-empty positions (see §1A below).

╔═══════════════════════════════════════╗ ╔═══════════════════════════════════════╗
║ LEFT HAND — FUNCTION GRID (14 keys) ║ ║ RIGHT HAND — DATA GRID (16 keys) ║
╠═══════════╤═══════════╤═══════╤══════╣ ╠════════╤════════╤════════╤════════════╣
║ QUOTE │ CONS │ NIL │ FN ║ ║ 1 │ 2 │ 3 │ / ║
║ (defer) │ (build) │ (nil) │(func)║ ║ │ (ABC) │ (DEF) │ (slash) ║
╠═══════════╪═══════════╪═══════╪══════╣ ╠════════╪════════╪════════╪════════════╣
║ INFO │ CAR │ APPLY │ SYS ║ ║ 4 │ 5 │ 6 │ ; ║
║ (scan) │ (first) │ (use) │(menu)║ ║ (GHI) │ (JKL) │ (MNO) │ (semi) ║
╠═══════════╪═══════════╪═══════╪══════╣ ╠════════╪════════╪════════╪════════════╣
║ LINK │ BACK │ CDR │ ATOM ║ ║ 7 │ 8 │ 9 │ - ║
║ (link) │ (up) │(rest) │(leaf)║ ║ (PQRS) │ (TUV) │ (WXYZ) │ (minus) ║
╠═══════════╧═══════════╪═══════╪══════╣ ╠════════╪════════╪════════╪════════════╣
║ ══ EVAL (1.75U) ══ │ EQ │ TERM ║ ║ * │ 0 │ # │ ENT ║
║ (execute / commit) │(equal)│░empty║ ║ (star) │ │(hash) │ (enter) ║
╚═══════════════════════╧═══════╧══════╝ ╚════════╧════════╧════════╧════════════╝

Data grid layout is phone-style (1-2-3 top, 0 bottom-center), not calculator-style. See §3D and ADR-0016 §5 for the rationale — letters on the Nokia multi-tap alpha entry (§6 below, and ADR-0016 §6) attach to phone-position muscle memory (2=ABC, 3=DEF, …), so the phone layout is a prerequisite for that input model. The right-column primaries (/, ;, -) and bottom-row outer keys (*, #) are the finalized keycap legends per ADR-0022; this column carries Lisp punctuation (with shift secondaries ", :, +, (, )) instead of the earlier ÷ × − + calculator arrangement. The FN keycap is the printed legend for the canonical LAMBDA key (cosmetic alias only; the Lisp slot, key code, and runtime semantics remain LAMBDA). See ADR-0022 for the full legend manifest, shift-secondary table, and the closure of the ADR-0016 §6 case-toggle / delete placeholders on / and *.

PropertyFunction Grid (Left)Data Grid (Right)
Keys1416
Rows × Cols4 × 4 (minus 2 empties)4 × 4
Physical groupingSemantic clusters by colorPhone-layout numpad (ADR-0016 §5)
Keycap legendsLisp primitives + verbsNumerals + arithmetic
EVAL key1.75U wide (centered over col 0, row 4)

The 8×4 matrix scans all 32 intersections. Two are unpopulated:

RowCol 0Col 1Col 2Col 3Col 4Col 5Col 6Col 7
0QUOTECONSNILLAMBDAPAD_1PAD_2PAD_3PAD_DIV
1INFOCARAPPLYSYSPAD_4PAD_5PAD_6PAD_MUL
2LINKBACKCDRATOMPAD_7PAD_8PAD_9PAD_SUB
3EVAL(empty)EQ(empty)PAD_DOTPAD_0PAD_ENTERPAD_ADD

Note: EVAL occupies a 1.75U keycap centered over Col 0, but electrically connects to the single matrix intersection at (3,0). Of the two previously-empty positions (3,1) and (3,3), (3,3) is populated as the TERM key (context-sensitive, see §3C and §6D), wearing a 1.5U keycap; (3,1) remains unpopulated.

TERM is a 1.5U key seated to the immediate right of the EVAL bar (with EQ between them), on the same row as the EVAL footprint. The keycap legend reads TERM in gray (same treatment as SYS and INFO). Electrically it occupies matrix position (3,3). Its row-4 placement, right of EVAL, makes it the only non-data-grid key beneath the function cluster; this is deliberate — the operator’s left thumb can reach it without leaving home position, and it sits opposite SYS (on the top-right of the function grid), making the function-grid’s system-layer affordances symmetric.


PropertyValue
TypeKailh Choc v1 (PG1350)
VariantWhite (Clicky, 50gf) or Jade (Heavy Clicky, 60gf)
Travel3mm total, 1.5mm pre-travel to actuation
Rated lifespan50 million actuations
MountingPCB-mount, hot-swap sockets (Mill-Max 0305 or Kailh hot-swap)
Quantity31 populated + 4 spares = 35 switches

The audible click is non-negotiable. The design document calls it “Deckline Chatter” — the sound of the device being operated is part of the fiction. Silent switches would undermine the experience.

PropertyValue
ProfileMBK (low-profile, uniform, Choc v1 compatible)
MaterialPBT
Legend methodDye-sublimation or UV printing
EVAL key1.75U stabilized keycap
TERM key1.5U stabilized keycap

Color coding by legend category:

Row / CategoryLegend ColorKeys
Lisp PrimitivesAmberCAR, CDR, CONS, NIL, ATOM, EQ
Action VerbsWhiteEVAL, QUOTE, LAMBDA, APPLY
System / NavigationRedLINK, BACK
nOSh runtimeGrayINFO, SYS
NumpadWhiteAll 16 data grid keys

Scanning (Device: custom mech keeb → USB HID)

Section titled “Scanning (Device: custom mech keeb → USB HID)”

The 31-key input is built as a custom mechanical keyboard per ADR-0018. A QMK-compatible keyboard controller (Pro Micro ATmega32U4, RP2040-class such as Sea-Picro / KB2040, nice!nano, or equivalent) scans the 8×4 matrix in hardware, applies debounce, and enumerates to the Pi Zero 2 W as a standard USB HID keyboard. The controller connects to the Pi through an internal USB hub IC on the interior plate — no external USB cables. nOSh reads keypresses via Linux evdev (/dev/input/event*).

PropertyValue
Matrix topology8 column + 4 row = 32 intersections, 31 populated
ControllerRP2040-class QMK target — Adafruit KB2040 (v0.1) per ADR-0024; Sea-Picro fallback
FirmwareStock QMK (or Vial on QMK). Firmware-layer features (tap-dance, layers, combos) are not used — LAMBDA / QUOTE / SYS-hold / TERM semantics all live in nOSh (§6)
Scan pathKeyboard controller (QMK) → USB HID → internal USB hub → Pi OTG → Linux evdev → nOSh
Effective scan rate≥ 1 kHz at the controller; polled by nOSh at input-dispatch time
Debounce10 ms software window, controller-side
Key state storage32-bit bitmask (30 bits used)

Two acceptable physical realizations of the 31-key matrix:

  1. Custom-fab unified 31-key PCB (preferred). A purpose-designed board carrying hot-swap Choc v1 sockets, 1N4148 SMD diodes, and the controller (socketed or onboard). Fabbed at JLCPCB / OshPark / PCBWay; ~$30–$60 for 5 boards; ~2-week design + fab cycle. Matches the device’s cohesive identity — single plate, single controller, clean internal routing.
  2. Modified split layout, reconnected internally (fallback). A pair of split-ergo PCBs (Corne, Ferris Sweep) wired to a single controller under a unified keyplate. Populate only the 31 logical keys; leave unused footprints empty under the plate. Zero-fab path when custom-PCB lead time is unacceptable. Aesthetically compromised (two PCBs under one plate, visible from the inside) but functionally identical.

Either path delivers the same logical 31-key scancode set to the controller, and the controller delivers the same USB HID event stream to the Pi. See ADR-0018 for the full trade-off analysis and options considered.

The emulator translates SDL keyboard scancodes to KN86 key codes via a 512-element lookup array. No physical matrix exists — the mapping is software-only.


These six keys implement genuine Lisp list operations. Every data structure in every cartridge module is a nested recursive list, and these keys are how the operator traverses and manipulates it.

PropertyDetail
Lisp origin(car '(a b c))a
KN-86 meaningDrill into / examine the first child
Default behaviorstdlib_drill_into() — push first_child onto nav stack
If leaf (no children)Error beep (stdlib_sfx_error())
Cognitive roleOODA → Orient (investigate what’s in front of you)
Mnemonics”Contents of Address Register” — look inside

Per-module examples:

  • ICE Breaker: Enter selected network node; inspect ICE details
  • Depthcharge: Descend one fathom bracket; examine sonar contact
  • Black Ledger: Drill into transaction; open sub-ledger
  • NeonGrid: Move forward into corridor/room
PropertyDetail
Lisp origin(cdr '(a b c))(b c)
KN-86 meaningTraverse to next sibling
Default behaviorstdlib_next_sibling() — move to next_sibling pointer
If no next siblingError beep
Cognitive roleOODA → Orient (scan options laterally)
Mnemonics”Contents of Decrement Register” — advance through list

Per-module examples:

  • Mission board: Scroll to next available contract
  • ICE Breaker: Move to adjacent network node
  • Black Ledger: Next transaction in ledger
  • Depthcharge: Pan sonar sweep to next bearing
PropertyDetail
Lisp origin(cons 'a '(b c))(a b c)
KN-86 meaningAttach / combine / construct
Default behaviorNone (module-specific)
Cognitive roleOODA → Act (build something from parts)
Mnemonics”Construct” — join things together

Per-module examples:

  • ICE Breaker: Attach tool/exploit to target node
  • Black Ledger: Link two transactions as related entries
  • SynthFence: Combine buy/sell orders into a paired position
  • Depthcharge: Attach depth charge to bearing
PropertyDetail
Lisp originnil / '() — the empty list
KN-86 meaningDiscard / clear / cancel / reset
Default behaviorNone (module-specific)
Cognitive roleAbort/undo current partial action
Mnemonics”Nothing” — make it empty

Per-module examples:

  • ICE Breaker: Discard selected tool, cancel deployment
  • NeonGrid: Clear current path markers
  • Black Ledger: Void flagged transaction
  • General: Cancel any in-progress CONS operation
PropertyDetail
Lisp origin(atom 'a)T; (atom '(a b))NIL
KN-86 meaningTest if current element is a leaf (no children)
Default behaviorstdlib_is_leaf() — returns true/false, often with audio cue
Cognitive roleOODA → Observe (is there more depth here, or is this terminal?)
Mnemonics”Atomic?” — can I drill further?

Per-module examples:

  • ICE Breaker: Is this node a terminal? (no sub-processes)
  • Depthcharge: Is this contact resolved? (no further analysis possible)
  • Black Ledger: Is this a single entry? (no sub-transactions)
  • General: Useful before CAR — tells you whether drilling will work
PropertyDetail
Lisp origin(eq 'a 'a)T
KN-86 meaningCompare two quoted/bookmarked elements
Default behaviorThe nOSh runtime compares current element against last QUOTE’d reference
Cognitive roleOODA → Orient (spot differences, find matches)
Mnemonics”Equal?” — are these the same?

Per-module examples:

  • Black Ledger: Compare two transactions for discrepancies
  • ICE Breaker: Compare network node signatures
  • Cipher Garden: Test if decrypted output matches expected plaintext
  • General: Bookmark with QUOTE, navigate elsewhere, press EQ to compare
PropertyDetail
Lisp origin(eval '(+ 1 2))3
KN-86 meaningExecute / confirm / commit action
Physical note1.75U wide key — the largest and most prominent key on the deck
Cognitive roleOODA → Act (commit your decision, trigger resolution)
Tempo roleThe “action clock tick” — every EVAL press advances the game state

Per-module examples:

  • Mission board: Accept selected contract
  • ICE Breaker: Execute deployed tool against target
  • Depthcharge: Release depth charge at current bearing/depth
  • Bare deck: Confirm handle entry, select menu item

QUOTE — “Defer / Bookmark” (Value 0)

Section titled “QUOTE — “Defer / Bookmark” (Value 0)”
PropertyDetail
Lisp origin(quote x) / 'x — return x without evaluating
KN-86 meaningBookmark current element for later reference
nOSh runtime servicePress QUOTE → QUO? prompt → numpad 1-8 → Cell ID stored in quote slot
Storage8 slots in SRAM (volatile across power-off, persistent within session)
SemanticsStores a reference, not a snapshot (like Lisp quote)

Per-module examples:

  • Black Ledger: Bookmark suspicious transaction for comparison
  • ICE Breaker: Mark a node for later EQ comparison
  • General: Navigate away, then recall via QUOTE + digit
PropertyDetail
Lisp origin(lambda (x) (* x x)) — anonymous function
KN-86 meaningRecord a sequence of keystrokes for replay
nOSh runtime serviceHold LAMBDA 2s → λREC indicator → press keys → press LAMBDA to stop → numpad 1-8 to assign
PlaybackTap LAMBDA → λ? → numpad 1-8 → the nOSh runtime replays sequence
Quick invokeLAMBDA + digit simultaneously → instant playback
Storage8 slots × 32 key events max = 256 bytes in wear-leveled flash
TransparencyReplayed events enter the input queue indistinguishably from live input

Use cases:

  • Black Ledger: Record a 5-key audit sequence, replay across hundreds of entries
  • ICE Breaker: Record a reconnaissance pattern (INFO→CDR→CDR→CAR), replay at each node
  • General: Any repetitive multi-key operation worth automating
PropertyDetail
Lisp origin(apply fn args) — apply function to arguments
KN-86 meaningDeploy / use selected tool against current target
Cognitive roleOODA → Decide → Act bridge (choose tool, then EVAL confirms)

Per-module examples:

  • ICE Breaker: Deploy exploit tool against ICE node
  • Depthcharge: Apply sonar processing filter
  • Shellfire: Deploy countermeasure against incoming signal
  • General: Often paired with EVAL — APPLY selects, EVAL commits

3C. System / Navigation Keys (Red + Gray Legends)

Section titled “3C. System / Navigation Keys (Red + Gray Legends)”
PropertyDetail
KN-86 meaningPop navigation stack — return to parent context
Default behaviorstdlib_navigate_back() — pop nav stack, call on_exit/on_enter
Legend colorRed
nOSh runtime noteIf nav stack is empty at runtime level (depth 0), BACK is ignored
PropertyDetail
KN-86 meaningShow detailed information about current element
Legend colorGray
Cognitive roleOODA → Observe (gather intelligence before acting)

Per-module examples:

  • ICE Breaker: Display adjacency list, threat level, ICE type
  • Mission board: Show full contract details (difficulty, payout, reputation)
  • Black Ledger: Show transaction metadata, timestamp, parties
PropertyDetail
KN-86 meaningInitiate a link operation between elements
Legend colorRed
nOSh runtime noteLINK protocol is module-defined — the nOSh runtime passes through

Per-module examples:

  • ICE Breaker: Establish connection between network nodes
  • Relay: Initiate system image update handshake
  • Depthcharge: Link sonar contact to classification
PropertyDetail
KN-86 meaningTap: open system menu. Hold 2s: hard abort
Legend colorGray
Tap behaviorContext menu (brightness, volume, deck status, key test)
Hold behavior (2s)Force-quit to boot screen (replaces ESC — there is no ESC key)
nOSh runtime noteSYS hold is the emergency exit. Always available, never overridable by cartridges

TERM — “Terminal / Context-Sensitive” (Value 10)

Section titled “TERM — “Terminal / Context-Sensitive” (Value 10)”
PropertyDetail
KN-86 meaningContext-sensitive — the meaning is determined by current deck state
Legend colorGray
Default behavior (no special mode)Open the terminal view — bare deck REPL if in :bare-deck beat, otherwise a cart-contextual terminal if the cart registers one; otherwise a no-op with a short audio bounce
Mode-sensitive behaviorsSee §3C.1 below
nOSh runtime noteTERM is nOSh-runtime-owned at the mode layer — the nOSh runtime decides which binding fires based on current CIPHER-LINE state, cart beat, and hold duration. Cartridges cannot silently steal TERM; they may request a cart-scoped binding via (term-register-binding :tag :binding-id :label "...") and the nOSh runtime honors it only when no higher-priority mode is active.

TERM is the only key on the deck whose binding is allowed to change across contexts. This is intentional: CIPHER-LINE introduced a surface that needs a dedicated capture affordance, and it made sense to bind it to the one uncommitted key rather than shadow a Lisp primitive. The table below is canonical; other behaviors may be added by future cartridge registrations, but every addition must fit this model.

PriorityContextTERM bindingLabel shown on CIPHER-LINE Row 4
1 (highest)CIPHER-LINE seed-capture mode active (set by (aux-show-seed ...))Capture seed. Persist the displayed seed to Universal Deck State; exit seed-capture mode.TERM: CAPTURE
2Hot-swap prompt active; nOSh runtime awaiting operator confirmationSkip swap. Defer swap and put the mission on hold (same as QUOTE-hold; provides a second, ambidextrous path).TERM: DEFER
3Null module, Cipher-Analysis bounty activeFreeze Cipher state. Snapshot current coherence stack and memory-store for analysis; the nOSh runtime renders the snapshot on the main grid.TERM: FREEZE
4Cart has registered a cart-scoped binding and the cart is the active cartCart-defined behavior. Cart provides the label; the nOSh runtime renders it on Row 4 unless a higher-priority context overrides.TERM: <cart-label>
5Bare deck, no cart insertedOpen bare-deck terminal REPL. Toggle the main-grid into the nOSh runtime REPL (nEmacs-minor-mode-style).TERM: REPL
6 (lowest)None of the aboveNo-op. Short audio bounce (400 Hz, 50ms). CIPHER-LINE Row 4 is unchanged.(no override)

Priority is evaluated top-down at key-press time. The nOSh runtime determines the binding; the cart cannot see a TERM press when a higher-priority binding is active. This is a deliberate asymmetry from the other system keys (SYS, BACK) — TERM is the operator’s handshake with nOSh runtime modes.

TERM can also be held (≥500 ms). Hold behavior is always the same regardless of context: toggle CIPHER-LINE mute. The OLED backlight stays lit and Row 1 still renders, but Rows 2–3 (Cipher scrollback) blank and the engine suspends emission until TERM is held again. This is the operator’s physical kill switch for the Cipher voice — useful when concentration is wanted or when the operator is recording a session for the community.

Mute state is stored in Universal Deck State (cipher_muted flag, 1 bit) and survives power cycles. Row 4 shows CIPHER: MUTED while muted.

The 16-key numpad on the right side is a data grid, not a directional pad. It serves four purposes depending on the module context. The critical design principle: digits are data first, direction second.

Layout is finalized in ADR-0022. The right column primaries are / ; - (with shift secondaries " : +); the bottom-row outer keys are * # (with shift secondaries ( )); key 1 carries backtick (`) as its shift secondary; key 9’s multi-tap letter group is WXYZ. The earlier ÷ × − + calculator-style right column has been retired in favor of Lisp punctuation density. Key codes (KN86_KEY_PAD_DIV, _MUL, _SUB, _ADD, _DOT) are preserved as position labels and continue to map through the QMK firmware unchanged.

When a module expects numeric input (handle entry, Cipher puzzle answers, LFSR predictions, bearing coordinates), the numpad provides direct digit input 0-9. The arithmetic keys (now legended /, *, -, +-on-shift) serve as additional data keys or operators in some contexts; their KN86 codes remain KN86_KEY_PAD_DIV / _MUL / _SUB / _ADD per the position-naming convention in ADR-0022 §2.

Directional / Spatial Interpretation (Module-Optional)

Section titled “Directional / Spatial Interpretation (Module-Optional)”

Modules that need steering, heading, or spatial input MAY interpret the numpad’s physical layout as direction. With the phone-style layout (§1, ADR-0016 §5), 1-2-3 sit on the top row and 7-8-9 sit on the third row, so the cardinal and diagonal assignments track the physical topology:

1 = NW 3 = NE (top-row diagonals)
2
4 ← 5 → 6 5 = center / confirm / hold position
8
7 = SW 9 = SE (third-row diagonals)
  • 2 = north (top-center of the numpad grid)
  • 8 = south (bottom-of-digits row)
  • 4 = west, 6 = east (middle row)
  • 1 / 3 = top-row (NW / NE) diagonals
  • 7 / 9 = third-row (SW / SE) diagonals
  • 5 = center / confirm / hold position

This is not a nOSh runtime feature — the nOSh runtime always delivers raw digit values via on_numpad(self, digit). Directional interpretation is a module-level convention. A module’s on_numpad handler decides whether 2 means “the number two” or “steer north.”

Modules that use directional numpad:

  • Depthcharge: 2/4/6/8 sets sonar bearing; digit = heading in 45° increments. Pressing 2 means “bearing north,” not “two.”
  • Drift: 2/4/6/8 steers antenna heading for RF triangulation. Speed/sensitivity on 1-9 scale.
  • NeonGrid: 2/4/6/8 moves through grid corridors. This is the most d-pad-like usage.
  • Shellfire: 2/4/6/8 rotates countermeasure array. Digit magnitude sets intensity.

Modules that use pure numeric numpad:

  • Black Ledger: Digits enter account numbers, transaction amounts. No directional meaning.
  • Cipher Garden: Digits enter decryption key guesses. Pure numeric.
  • The Vault: Digits index knowledge entries. Pure numeric.
  • Bare Deck: Digits enter operator handle characters, LFSR predictions.

Hybrid usage is valid. A module can use 2/4/6/8 for navigation within one cell type and 0-9 for data entry within another. The interpretation is per-cell, not per-module. The on_numpad handler for a “heading_selector” cell reads 2 as north; the on_numpad handler for a “coordinate_entry” cell reads 2 as the digit two.

Design rule: When a module uses directional numpad, the digit’s numeric value should still make spatial sense in the current phone layout. 2 = north (top-center), 8 = south, 4 = west, 6 = east, 5 = center. Don’t remap digits to arbitrary directions. The physical topology of the numpad IS the map.

Migration note for module authors: The phone layout inverts the vertical axis compared to calculator-layout conventions that appear in earlier drafts. Modules authored against the old convention (8 = north, 2 = south) must swap their north/south digit assignments when re-targeting the phone layout. Corresponding gameplay-spec revisions are tracked under the GWP-217 wave (Launch-titles gameplay-spec revision for phone-style numpad).

After LAMBDA or QUOTE prompt, numpad 1-8 selects the slot. This is a runtime-level function — cartridges never see these keypresses during slot selection.

In SYS menu and module configuration, PAD_SUB increments/decrements values via its - primary and + shift secondary. Numpad digits can set values directly (e.g., pressing 7 sets volume to 70%). The right-column keys (/, ;, -) can serve as modifier/mode keys in module-specific contexts; gameplay specs that historically referred to “the ÷ key” or “the × key” mean the same physical positions (R1C4 / R2C4 / R4C1) — the printed legend is now /, ;, * per ADR-0022.

3D.1 Digit Reservation — Bare-Deck Tab Navigation vs. Cart Dispatch

Section titled “3D.1 Digit Reservation — Bare-Deck Tab Navigation vs. Cart Dispatch”

The Bare Deck Terminal reserves digits 1–5 for numbered-tab navigation (NetWatch pattern — see bare-deck-terminal.md “Numbered-Tab Navigation”): at tab level, digit 1 jumps to STATUS, 2 to LAMBDA, 3 to LINK, 4 to SYS, and 5 is reserved for a future MISSIONS tab (no-op bounce until that tab ships). This is a runtime-owned, Bare-Deck-only interception — it sits in the same class as LAMBDA/QUOTE slot selection: when active, those digit presses are consumed by the runtime and the (absent) cart never sees them.

The reservation is conditional, narrow, and reclaimable, governed by three rules:

  1. Bare Deck only. The reservation is in force only when no cartridge is inserted (the :bare-deck beat) and the operator is at tab level, not inside a tab’s interactive mode. Inside an interactive mode (after CAR — LFSR prediction entry, contrast/volume adjustment, handle entry, etc.) digits are ordinary data per §3D; the “digits are data first” invariant (§9 invariant 8) is preserved. Tab-jump is a tab-level affordance; data entry is a mode-level affordance; they never collide.

  2. A loaded cart’s digits default to the cart. The moment a cartridge is inserted, the Bare Deck tab strip is no longer the active surface — cart tabs occupy positions 5+ and the runtime tabs sit at 1–4 (per bare-deck-terminal.md “Cartridge Detection & Hot Swap”). By default, the runtime does not intercept digits for tab jumps while a cart is active: tab cycling reverts to CDR, and digit presses flow to the active cell’s on_numpad(self, digit) handler exactly as documented in §3D. There is no implicit digit-tab-jump shadowing cart numeric input. Default = not overridden, and the cart receives all digits.

  3. A cart may reclaim explicit ownership via the :override-digit-dispatch capability (see below). This is a no-op for the common case — carts already receive digits by default per rule 2 — but it is the cart’s positive, machine-checkable declaration that it requires unimpeded digit dispatch, which (a) future-proofs the cart against any runtime that might layer a digit-tab accelerator onto the cart surface, and (b) makes the requirement legible to the loader, the emulator, and tooling.

:override-digit-dispatch is a cartridge capability flag, declared in the cart’s capability manifest — the same (defcapabilities …) form in the cart’s top-level Lisp source that declares :supersedes, :cipher-main-grid-escape (Null only, ADR-0015 §3a), and the other capability declarations the loader reads from the .kn86 header at load time (ADR-0006 cart format; capability model per CLAUDE.md “Software Architecture: Capability Model”).

;; In the cartridge's top-level capability manifest:
(defcapabilities
:id :ice-breaker
:supersedes :terminal
:override-digit-dispatch t) ;; cart claims full digit dispatch; runtime adds no digit accelerator
PropertyValue
Declared inCart capability manifest ((defcapabilities …)), read from the .kn86 header at cart-load
TypeBoolean flag
Default (absent / nil)Not overridden. Cart still receives every digit via on_numpad while a cart is loaded (rule 2). The flag’s absence does not mean “runtime steals digits” — it means “no explicit claim.” The runtime ships no digit-tab accelerator on the cart surface in v0.x, so absent-and-present behave identically today; the flag exists so a cart’s requirement survives any future runtime that does add one.
Present (t)Cart asserts exclusive digit dispatch on its own surfaces. The runtime guarantees no digit key (1–9, 0) is intercepted for tab navigation or any other runtime digit accelerator while this cart is the active cart and the operator is on a cart surface. Runtime tabs 1–4 remain reachable via CDR; the cart owns all digits.
ScopeCart surfaces only. Never affects the Bare Deck tab strip (rules 1–2): when the operator is on a runtime tab with no cart numeric context, the runtime’s own digit handling applies regardless of any cart flag — but in practice a loaded-cart operator on a runtime tab is using CDR/CAR, not digit-jump, since digit-jump is a Bare-Deck-only affordance.
Runtime guarantees never overriddenThe flag governs digit (numpad) dispatch only. It cannot reclaim SYS-hold (§6C), TERM mode dispatch (§6D), LAMBDA/QUOTE services (§6A/§6B), or any other runtime-owned key service. A cart cannot use :override-digit-dispatch to suppress the emergency abort or the Cipher mute toggle.

Precedence rule (canonical). Digit dispatch resolves top-down at key-press time, first match wins:

  1. Runtime modal digit consumer active (LAMBDA λ? / QUOTE QUO? slot-select prompt, seed-capture, REPL toast numeric entry) → runtime consumes the digit. Highest priority; not overridable by any cart flag. (Same intercept-before-dispatch pattern as TERM §3C.1 and PAD_ENTER §3E.)
  2. Bare Deck, at tab level, no cart inserted → digit 1–5 is a tab jump (:override-digit-dispatch is irrelevant here — there is no cart to declare it).
  3. Cart inserted, operator on a cart surface → digit goes to the active cell’s on_numpad. This is the default whether or not :override-digit-dispatch is set; the flag makes the guarantee explicit and binding against future runtime digit accelerators.
  4. Bare Deck, inside an interactive mode (after CAR) → digit is data for that mode (§3D), never a tab jump.

The two-line summary: on the bare deck, digits 1–5 jump tabs at tab level; once a cart is loaded, digits belong to the cart, and :override-digit-dispatch t is the cart’s explicit, loader-visible assertion of that claim. Default is not-overridden, and a loaded cart receives its digits either way.

The “Primary legend” column shows the printed character on the keycap per ADR-0022. The “Shift secondary” column shows the second printed glyph (when present); ADR-0022 §3–§4 covers the shift-pairing rationale, and the active shift gesture (long-press vs. chord) is finalized in the input-dispatch foundation task per ADR-0016 Implementation Queue.

Key codeValuePositionPrimary legendShift secondaryMulti-tap lettersPrimary Role
PAD_122R1C11` (backtick)Digit / slot select
PAD_223R1C22ABCDigit / slot select
PAD_324R1C33DEFDigit / slot select
PAD_DIV17R1C4/"Slash / case-toggle in :nemacs-literal + :prompt-text (ADR-0022 §7)
PAD_418R2C14GHIDigit / slot select
PAD_519R2C25JKLDigit / slot select
PAD_620R2C36MNODigit / slot select
PAD_MUL21R2C4;:Statement separator / Lisp keyword qualifier on shift
PAD_714R3C17PQRSDigit / slot select
PAD_815R3C28TUVDigit / slot select
PAD_916R3C39WXYZDigit
PAD_SUB25R3C4-+Decrement / minus; arithmetic pair with shift
PAD_DOT27R4C1*(Star / delete in :nemacs-literal + :prompt-text (ADR-0022 §7); shift produces (
PAD_026R4C20.Digit; shift produces decimal point
PAD_ENTER28R4C3#)Hash; shift produces ). Note: the KN86_KEY_PAD_ENTER code name predates the legend swap; the dedicated ENT keycap is at R4C4. Position-to-code alignment is queued as a firmware follow-up (ADR-0022 Known Unknowns #4).
PAD_ADD29R4C4ENTConfirm numpad entry — see §3E for dual-binding

The numpad ENTER key carries two distinct semantics across deck contexts. Both share the same physical switch and KN86 key code (KN86_KEY_PAD_ENTER = 28); the binding is selected by the active surface, not by a modifier:

ContextBindingBehavior
Arithmetic / numeric entry (Black Ledger transaction line, Cipher Garden key guess, LFSR prediction, handle entry)= (commit value)Commit the entered numeric value to the cell or prompt. Equivalent to a calculator’s = key. The cart’s on_numpad handler — or in OOBE flows the bare-deck handler — interprets the press as “the operator is done typing a number”.
Player-facing Lisp REPL toast (GWP-230 / ADR-0002)Evaluate formEvaluate the current REPL composer line through the Fe VM. Equivalent to RET in a Lisp REPL. The toast intercepts the press before any cart-level dispatch; carts never see ENT while the toast is visible (cart-clock pause guard, §6G).

The choice between bindings is contextual: if the REPL toast is visible, the REPL binding wins; otherwise PAD_ENTER falls through to the active cell’s on_numpad handler with the value 28. This is the same priority pattern as the TERM mode dispatcher (§3C.1) — runtime-owned modes intercept before cart dispatch, with no operator-visible modifier required.

Implementation note. GWP-230 wires the cart-clock pause guard (repl_toast_should_pause_cart) into the main event loop so the REPL toast freezes runtime_tick / bare_deck_tick while open. The REPL evaluator wiring proper is a downstream task (the toast’s repl_toast_commit is currently a placeholder that stamps “OK”); when the real evaluator lands, the dual-binding contract above is what carts may rely on.

The TERM key (KN86_KEY_TERM = 30, matrix position (3,3) per §1A) is the runtime-owned mode dispatch key documented in §3C / §6D. GWP-230 implements its default behavior — priority 5 in the §3C.1 table:

Default (no special mode): Toggle the player-facing Lisp REPL toast on the main 80×25 grid. Slide-down animation (~80 ms envelope), pauses runtime_tick / bare_deck_tick while visible, restores cart framebuffer byte-exact on close.

Higher-priority context overrides (CIPHER-LINE seed capture, hot-swap defer, Cipher freeze, cart-scoped binding) are out of scope for GWP-230 and land in Wave 2 (GWP-231 / GWP-232). They layer on top of the default by gating before the toast-toggle dispatch fires; the §3C.1 priority table is the canonical reference for the eventual layered semantics.

SDL scancode. Default emulator binding is SDL_SCANCODE_GRAVE (the backtick / tilde key, immediately above Tab on a US keyboard). Documented in assets/default.keymap and assets/laptop.keymap as TERM = ``; the in-process fallback (sdl_to_kn86[SDL_SCANCODE_GRAVE] = KN86_KEY_TERM`) covers builds that load no keymap file. Choice rationale: the backtick row is unbound by every Lisp primitive (1/2 are EVAL/EQ on the host keyboard) and reaches the operator’s left thumb on both desktop and laptop layouts.

Out-of-band of the 30-key arrays. TERM does not occupy a slot in state->key_states[30] or state->key_hold_time[30]. The input_inject_kn86_key path enqueues raw KEY_DOWN / KEY_UP events for TERM but skips the derived-event engine (TAP / DOUBLE_TAP / LONG_PRESS) and the state-array bookkeeping the 30 cart-facing keys use. This is by intent — TERM is not a cart-dispatched key, and the runtime handles it before cart dispatch sees the queue. The 30-bit matrix bitmask in §2 covers the cart-facing keys; TERM rides outside that mask.


Physical Key Press
┌─────────────────────┐
│ Keyboard Controller │ Device: USB HID from QMK controller → evdev
│ or SDL Event │ Emulator: SDL_PollEvent()
└─────────┬───────────┘
┌─────────────────────┐
│ Debounce (10ms) │ Ignore state changes within 10ms window
└─────────┬───────────┘
┌─────────────────────┐
│ Translate to │ sdl_to_kn86[512] lookup (emulator)
│ KN86KeyCode │ Matrix position lookup (hardware)
└─────────┬───────────┘
┌─────────────────────┐
│ Enqueue InputEvent │ 128-slot ring buffer
│ (type, key, time) │ KEY_DOWN, KEY_UP, KEY_REPEAT, KEY_HOLD
└─────────┬───────────┘
┌─────────────────────────────────────────────┐
│ Hold Detection (per-frame) │
│ LAMBDA/SYS: 2000ms → KEY_HOLD event │
│ All others: 500ms → KEY_REPEAT at 10Hz │
└─────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ nOSh Runtime Intercept │
│ LAMBDA hold → enter recording mode │
│ LAMBDA tap (in λ? mode) → replay prompt │
│ SYS hold → hard abort to boot screen │
│ QUOTE tap → bookmark prompt (QUO?) │
│ Lambda/Quote + numpad → slot operation │
└─────────┬───────────────────────────────────┘
│ (if not intercepted)
┌─────────────────────────────────────────────┐
│ Dispatch to Current Cell's Handler │
│ CellHandlers lookup: key → handler slot │
│ If handler exists → call it │
│ If NULL → ignore (silent) │
│ Numpad 0-9 → on_numpad(self, digit) │
└─────────────────────────────────────────────┘
typedef enum {
INPUT_EVENT_KEY_DOWN = 0, /* Key pressed (initial) */
INPUT_EVENT_KEY_UP = 1, /* Key released */
INPUT_EVENT_KEY_REPEAT = 2, /* Auto-repeat (500ms delay, 10Hz rate) */
INPUT_EVENT_KEY_HOLD = 3 /* Long hold detected (2000ms, LAMBDA/SYS only) */
} InputEventType;
typedef struct {
InputEvent events[128]; /* Ring buffer */
int head; /* Next write position */
int tail; /* Next read position */
} InputQueue;

The queue is drained every frame by either bare_deck_tick() or runtime_tick(). At 60fps with a 128-slot buffer, overflow is practically impossible under normal operation.

uint8_t key_states[30]; /* 1 = currently pressed, 0 = released */
uint32_t key_hold_time[30]; /* Timestamp of last KEY_DOWN (0 if released) */

Cartridge authors define input behavior per-cell-type using handler macros. Each macro generates a static function with the correct signature:

/* Lisp primitives (amber legend) — list operations */
CELL_ON_CAR(name) /* void fn(void *self) — drill in / examine first child */
CELL_ON_CDR(name) /* void fn(void *self) — traverse to next sibling */
CELL_ON_CONS(name) /* void fn(void *self) — attach / construct */
CELL_ON_NIL(name) /* void fn(void *self) — discard / cancel */
CELL_ON_ATOM(name) /* void fn(void *self) — test if leaf */
CELL_ON_EQ(name) /* void fn(void *self) — compare with quote slot */
/* Action keys (white legend) — execution verbs */
CELL_ON_EVAL(name) /* void fn(void *self) — execute / commit */
CELL_ON_QUOTE(name) /* void fn(void *self) — bookmark (usually nOSh runtime) */
CELL_ON_LAMBDA(name) /* void fn(void *self) — macro (usually nOSh runtime) */
CELL_ON_APPLY(name) /* void fn(void *self) — deploy tool */
/* System keys (gray/red legend) */
CELL_ON_BACK(name) /* void fn(void *self) — navigate up */
CELL_ON_INFO(name) /* void fn(void *self) — inspect / show details */
CELL_ON_LINK(name) /* void fn(void *self) — initiate link */
CELL_ON_SYS(name) /* void fn(void *self) — system menu (tap only) */
/* Data grid */
CELL_ON_NUMPAD(name) /* void fn(void *self, uint8_t digit) — numpad 0-9 */
/* Lifecycle (not input-triggered) */
CELL_ON_DISPLAY(name) /* void fn(void *self) — render to framebuffer */
CELL_ON_ENTER(name) /* void fn(void *self) — cursor arrived at this cell */
CELL_ON_EXIT(name) /* void fn(void *self) — cursor leaving this cell */
/* Define cell type with custom fields */
CELL_TYPE(network_node,
uint8_t ice_level;
uint16_t data_value;
bool compromised;
);
/* Define handlers for each key */
CELL_ON_CAR(network_node) {
cell_network_node *self = (cell_network_node *)_self;
if (self->base.first_child) {
stdlib_drill_into(g_state); /* Navigate to first child */
} else {
stdlib_sfx_error(); /* Leaf node — can't drill */
}
}
CELL_ON_CDR(network_node) {
stdlib_next_sibling(g_state); /* Move to next adjacent node */
}
CELL_ON_INFO(network_node) {
cell_network_node *self = (cell_network_node *)_self;
display_clear_text(g_state);
nosh_print(g_state, 0, 0, "NODE INTEL");
nosh_printf(g_state, 0, 2, "ICE Level: %d", self->ice_level);
nosh_printf(g_state, 0, 3, "Data: %04X", self->data_value);
nosh_printf(g_state, 0, 4, "Status: %s", self->compromised ? "OPEN" : "LOCKED");
}
CELL_ON_EVAL(network_node) {
cell_network_node *self = (cell_network_node *)_self;
if (self->compromised) {
/* Extract data — mission progress */
mission_extract_data(self->data_value);
stdlib_sfx_confirm();
} else {
stdlib_sfx_error(); /* Can't extract from locked node */
}
}
/* Wire handlers into table */
CELL_HANDLERS(network_node,
.on_car = _cell_network_node_on_car,
.on_cdr = _cell_network_node_on_cdr,
.on_info = _cell_network_node_on_info,
.on_eval = _cell_network_node_on_eval,
/* Unset handlers (NULL) are silently ignored by the dispatcher */
);

The runtime’s dispatch_key_handler() maps each KN86KeyCode to the corresponding function pointer in the current cell’s CellHandlers table:

static CellHandler dispatch_key_handler(const CellHandlers *handlers, KN86KeyCode key) {
if (handlers == NULL) return NULL;
switch (key) {
case KN86_KEY_CAR: return handlers->on_car;
case KN86_KEY_CDR: return handlers->on_cdr;
case KN86_KEY_CONS: return handlers->on_cons;
case KN86_KEY_NIL: return handlers->on_nil;
case KN86_KEY_ATOM: return handlers->on_atom;
case KN86_KEY_EQ: return handlers->on_eq;
case KN86_KEY_EVAL: return handlers->on_eval;
case KN86_KEY_QUOTE: return handlers->on_quote;
case KN86_KEY_LAMBDA: return handlers->on_lambda;
case KN86_KEY_APPLY: return handlers->on_apply;
case KN86_KEY_BACK: return handlers->on_back;
case KN86_KEY_INFO: return handlers->on_info;
case KN86_KEY_LINK: return handlers->on_link;
case KN86_KEY_SYS: return handlers->on_sys;
default: return NULL;
}
}

Numpad keys are handled separately — the dispatcher extracts the digit (0-9) and calls on_numpad(self, digit).

The nOSh runtime maintains a 32-deep navigation stack. Standard library helpers manage it:

FunctionEffect
stdlib_drill_into(state)Push current->first_child onto stack. Call on_exit on old, on_enter on new.
stdlib_next_sibling(state)Replace stack top with current->next_sibling. Call on_exit/on_enter.
stdlib_prev_sibling(state)Replace stack top with current->prev_sibling. Call on_exit/on_enter.
stdlib_navigate_back(state)Pop stack. Call on_exit on current, on_enter on new top.

All four functions beep (stdlib_sfx_error()) if the target pointer is NULL.


These key functions are owned by the nOSh runtime and cannot be overridden by cartridges. The nOSh runtime intercepts them before dispatch reaches the cell handler table.

PropertyValue
TriggerHold LAMBDA for 2000ms
IndicatorλREC in status bar
Capacity8 slots × 32 key events each
Storage256 bytes in wear-leveled flash (production) / file (emulator)
PlaybackTap LAMBDA → λ? → numpad 1-8
Quick invokeLAMBDA + digit simultaneously

Recording flow:

  1. Hold LAMBDA 2s → λREC appears, recording starts
  2. Press any sequence of keys (up to 32 events)
  3. Press LAMBDA to stop → λ? prompt
  4. Press numpad 1-8 to assign slot (overwrites existing)

Playback flow:

  1. Tap LAMBDA → λ? prompt
  2. Press numpad 1-8 → the nOSh runtime replays at original timing
  3. Events enter input queue — cartridge code cannot distinguish live from macro
PropertyValue
TriggerTap QUOTE
IndicatorQUO? prompt
Capacity8 slots, each holds a Cell ID reference
StorageSRAM (volatile across power-off, persistent across cartridge swaps)
SemanticsReference (not snapshot) — like Lisp quote
PropertyValue
TriggerHold SYS for 2000ms
EffectUnconditional abort to boot screen
OverrideNot possible — the nOSh runtime handles before cartridge dispatch
PurposeReplaces ESC key. The KN-86 has no ESC.
PropertyValue
TriggerTap TERM
EffectEvaluates priority table (§3C.1) at key-press time, dispatches the matching binding
OverrideCartridges may register a priority-4 cart-scoped binding via (term-register-binding ...); nOSh runtime priorities 1–3 and 5 always win
PurposeSingle context-sensitive affordance for nOSh runtime modes (seed capture, hot-swap defer, Cipher freeze, REPL)
PropertyValue
TriggerHold TERM for 500ms
EffectToggle cipher_muted flag; blank CIPHER-LINE Rows 2–3 and suspend Cipher engine emission (when muted); restore emission (when unmuted)
OverrideNot possible — the nOSh runtime handles before cartridge dispatch
PurposeOperator kill-switch for the Cipher voice; stored in Universal Deck State and persistent across power cycles

The seed-capture interaction is a first-class nOSh runtime service used whenever a cartridge or nOSh runtime subsystem needs to surface a value for the operator to commit to Universal Deck State. Typical callers: Cipher Garden’s decryption key slots, Null’s Cipher-seed freeze/replay, Shellfire’s intercept session IDs, Drift’s triangulation solution hash.

Trigger. Cartridge or nOSh runtime calls (aux-show-seed seed-bytes :label "<short-label>"). See docs/software/runtime/cipher-voice.md §11 for the primitive signature.

State machine.

state: CIPHER_LINE_DEFAULT
└─ Row 1: nOSh runtime status strip
Row 4: nOSh runtime chord hints
transition on aux-show-seed(seed, label):
→ state: CIPHER_LINE_SEED_MODE
state: CIPHER_LINE_SEED_MODE
└─ Row 1: FROZEN — "SEED <label>: <seed-hex>" (e.g., "SEED RELAY: A7F391...")
Row 4: PINNED — "TERM: CAPTURE BACK: DISMISS"
Rows 2–3: Cipher engine continues normal operation
Input routing:
TERM (tap) → capture + transition
BACK (tap) → dismiss + transition
TERM (hold) → still toggles Cipher mute (6E has higher priority than mode)
Any other key → forwarded to cart as normal
transition on TERM-tap:
nOSh runtime writes seed to Universal Deck State:
- If cart supplied a :slot hint, write to (aux-seed-slots[:slot])
- Otherwise, write to the next free aux-seed-slot
- If no free slot, display "SLOTS FULL" on Row 4 for 2s and
remain in SEED_MODE — no key is captured until operator dismisses
nOSh runtime plays confirmation tone (1.5 kHz, 100ms)
nOSh runtime exits SEED_MODE → CIPHER_LINE_DEFAULT
nOSh runtime pushes (:event :type :seed-captured :tag :firmware
:target <slot> :affect (:significant))
transition on BACK-tap:
nOSh runtime plays cancel tone (800 Hz, 100ms)
nOSh runtime exits SEED_MODE → CIPHER_LINE_DEFAULT
no deck-state write
transition on aux-show-seed(nil, nil):
cart cancels the seed capture
nOSh runtime exits SEED_MODE → CIPHER_LINE_DEFAULT

Aux seed slots. Universal Deck State reserves 8 slots for captured seeds (aux-seed-slots[0..7], each 16 bytes). Slots are visible to cartridges via the (deck-seed-slot N) accessor and persisted to flash on the normal deck-state cadence (mission-phase boundary, cart swap, power-off). The slots are cross-cartridge — a seed captured in Cipher Garden is readable by any subsequent cart — which is intentional, per the capability model’s “cartridge history builds deck texture” principle.

Voice integration. While in CIPHER_LINE_SEED_MODE, the Cipher engine continues to emit on Rows 2–3 per the Grammar Framework. Beat typically drops to :mission-brief or :idle during seed capture (cart’s call). Common pattern: Cipher drifts about prior seed captures while the operator decides to commit. Example expected fragment stream:

Row 2 another handshake.
Row 3 last one cost us.

Grammars may be tuned to bias drift during seed capture; this is a cart-level choice, not a nOSh runtime behavior.

Accessibility. TERM is on the function grid’s right edge — reachable by either hand. The default emulator mapping for TERM (to be confirmed during hardware bring-up) should accommodate single-handed operation; the current planning target is a Tab-key-adjacent binding so laptop users can capture without reaching.


Every module on the KN-86 maps its interaction to the Observe-Orient-Decide-Act loop. The keys are the physical expression of this cycle:

OBSERVE ──────────────────── INFO (scan), ATOM (test depth)
ORIENT ───────────────────── CAR (drill in), CDR (scan laterally), BACK (retreat)
DECIDE ──────────────────── CONS (attach tool), APPLY (deploy), QUOTE (bookmark)
ACT ─────────────────────── EVAL (commit), EQ (compare result)
└─── cycle back to OBSERVE

Tempo varies by module:

  • ICE Breaker: 2-5 second cycles (fast, reactive)
  • Shellfire: 6-8 second cycles (medium, strategic)
  • Depthcharge: 10-30 second cycles (slow, deliberate)
  • Black Ledger: Self-paced (no time pressure, deep analysis)

Expert operators develop rhythmic “Deckline Chatter” — the sound of their OODA cycle is audible in the room.


8A. Emulator Keymapping (Current Implementation)

Section titled “8A. Emulator Keymapping (Current Implementation)”
/* input.c — hardcoded, static array */
static KN86KeyCode sdl_to_kn86[512] = {0};
static void init_keymap(void) {
/* Function grid: QWER / ASDF / ZXCV / 12 rows */
sdl_to_kn86[SDL_SCANCODE_Q] = KN86_KEY_QUOTE;
sdl_to_kn86[SDL_SCANCODE_W] = KN86_KEY_CONS;
/* ... 14 function keys ... */
/* Data grid: physical numpad */
sdl_to_kn86[SDL_SCANCODE_KP_7] = KN86_KEY_PAD_7;
/* ... 16 numpad keys ... */
}

Per ADR-001 (docs/adr/001-keymap-config-format.md), the remapping system adds:

  1. Config file format: INI-style KN86_KEY = SDL_SCANCODE pairs
  2. CLI flag: --keymap <path> loads custom mapping
  3. Default keymap: assets/default.keymap — matches current hardcoded mapping
  4. Laptop keymap: assets/laptop.keymap — UIOP/JKL;/M,./ replaces numpad
  5. Runtime rebind: SYS menu → key test mode → press key to remap → EVAL to confirm
  6. API: keymap_load(), keymap_save(), keymap_init_defaults()
KN86 KeySDL ScancodeQWERTY KeyHandGrid Position
QUOTESDL_SCANCODE_QQLeftFunc R1C1
CONSSDL_SCANCODE_WWLeftFunc R1C2
NILSDL_SCANCODE_EELeftFunc R1C3
LAMBDASDL_SCANCODE_RRLeftFunc R1C4
INFOSDL_SCANCODE_AALeftFunc R2C1
CARSDL_SCANCODE_SSLeftFunc R2C2
APPLYSDL_SCANCODE_DDLeftFunc R2C3
SYSSDL_SCANCODE_FFLeftFunc R2C4
LINKSDL_SCANCODE_ZZLeftFunc R3C1
BACKSDL_SCANCODE_XXLeftFunc R3C2
CDRSDL_SCANCODE_CCLeftFunc R3C3
ATOMSDL_SCANCODE_VVLeftFunc R3C4
EVALSDL_SCANCODE_11LeftFunc R4C1 (1.75U)
EQSDL_SCANCODE_22LeftFunc R4C3
PAD_1SDL_SCANCODE_KP_1Numpad 1RightData R1C1
PAD_2SDL_SCANCODE_KP_2Numpad 2RightData R1C2
PAD_3SDL_SCANCODE_KP_3Numpad 3RightData R1C3
PAD_DIVSDL_SCANCODE_KP_DIVIDENumpad /RightData R1C4
PAD_4SDL_SCANCODE_KP_4Numpad 4RightData R2C1
PAD_5SDL_SCANCODE_KP_5Numpad 5RightData R2C2
PAD_6SDL_SCANCODE_KP_6Numpad 6RightData R2C3
PAD_MULSDL_SCANCODE_KP_MULTIPLYNumpad *RightData R2C4
PAD_7SDL_SCANCODE_KP_7Numpad 7RightData R3C1
PAD_8SDL_SCANCODE_KP_8Numpad 8RightData R3C2
PAD_9SDL_SCANCODE_KP_9Numpad 9RightData R3C3
PAD_SUBSDL_SCANCODE_KP_MINUSNumpad -RightData R3C4
PAD_DOTSDL_SCANCODE_KP_PERIODNumpad .RightData R4C1
PAD_0SDL_SCANCODE_KP_0Numpad 0RightData R4C2
PAD_ENTERSDL_SCANCODE_KP_ENTERNumpad EnterRightData R4C3
PAD_ADDSDL_SCANCODE_KP_PLUSNumpad +RightData R4C4
TERMSDL_SCANCODE_GRAVE` (backtick)EitherFunc R4C4 (matrix 3,3)

Note: The SDL_SCANCODE_KP_N bindings are keyboard-level scancodes — they refer to the USB keyboard’s physical keycap, not the KN-86 position. A USB numpad’s 7 keycap stays 7 (still the top-left of the USB numpad because laptops/desktops use calculator-layout numpads); it’s the KN-86 position of KN86_KEY_PAD_7 that moves to Row 3 Col 1 under phone layout. Scancode-to-key mapping is purely semantic.

TERM (GWP-230): The grave/backtick scancode is unbound by every Lisp primitive (1/2 are EVAL/EQ; the function-grid letters are QWER/ASDF/ZXCV) and sits immediately above Tab on US keyboards, reaching the left thumb on desktop and laptop layouts. See §3F for the full rationale.

Phone-style KN-86 positions mapped onto home-row-adjacent QWERTY keys. The KN86 key codes underneath are the same phone-layout bindings as §8C; only the scancodes change. Right-column legend annotations reflect the finalized ADR-0022 manifest (/, ;, -, */#/ENT on the bottom row).

Left hand (unchanged): Right hand (letter keys replace numpad):
Q W E R U I O P (1 2 3 /)
A S D F J K L ; (4 5 6 ;)
Z X C V M , . / (7 8 9 -)
1 2 N SPC RET ] (* 0 # ENT)

The exact laptop-keymap bindings ship in assets/laptop.keymap and are subject to ergonomic tuning. The invariant is that the logical grid (row × col) follows the phone layout per §1 and §3D regardless of which scancodes the laptop keymap chooses. Key codes (KN86_KEY_PAD_DIV, _MUL, _SUB, _DOT, _ENTER, _ADD) are preserved as position labels; printed legends moved per ADR-0022.


These rules cannot be violated by any module, remapping, or future extension:

  1. 31 keys, no more. The physical device has exactly 31 switches (14 function + 16 numpad + 1 TERM). The emulator must expose exactly 31 KN86 key codes — 30 cart-facing (KN86_KEY_* indices 0–29) plus KN86_KEY_TERM at index 30. The 30-key arrays (key_states[30], etc.) cover only the cart-facing subset; TERM is runtime-owned and rides outside the matrix bitmask (see §6D).
  2. Two grids, two hands. Left = function (semantic operations). Right = data (numeric entry). This split is architectural, not cosmetic.
  3. SYS hold is sacred. 2-second SYS hold always aborts to boot screen. No cartridge can override this. It’s the operator’s emergency exit.
  4. LAMBDA/QUOTE belong to the nOSh runtime. Macro recording and bookmarking are nOSh runtime services. Cartridges can define on_lambda and on_quote handlers for conditional behavior during recording, but the services themselves belong to the nOSh runtime.
  5. CAR drills, CDR traverses. These are genuine list operations. If a module’s data isn’t structured as a nested list navigable by CAR/CDR, the module’s data model needs to be redesigned (see Lisp Paradigm Audit).
  6. EVAL is irreversible. Pressing EVAL commits an action. There is no undo. The operator must OBSERVE and ORIENT before they ACT. This is by design — the tension of commitment is the game.
  7. NULL handlers are silent. If a cell doesn’t define a handler for a key, pressing that key does nothing. No error, no beep. Only ATOM (leaf test) and the stdlib navigation functions produce error beeps.
  8. Numpad is data-first, direction-optional. The nOSh runtime delivers numpad events as raw digits (0-9) via on_numpad(self, digit). The module decides whether a digit means a number or a direction. Modules MAY interpret the physical layout of 2/4/6/8 as cardinal steering and 1/3/7/9 as diagonals — but this is a module convention, not a nOSh runtime feature. The nOSh runtime never sends “north” — it sends “8.” The right-column and bottom-row outer keys (KN86_KEY_PAD_DIV, _MUL, _SUB, _DOT, _ENTER, _ADD — printed /, ;, -, *, #, ENT per ADR-0022) are also numpad keys and follow the same rule: raw values, module-interpreted meaning. Cartridge gameplay specs that historically referred to “the ÷ key” or “the × key” continue to denote the same physical positions; only the printed legend changes. The one runtime-owned digit interception is Bare-Deck numbered-tab navigation (digits 1–5 at tab level, no cart inserted — §3D.1); a loaded cart receives all digits by default and may make that claim explicit with :override-digit-dispatch t. This does not weaken data-first: tab-jump is a tab-level affordance and never fires inside a cell’s data-entry mode.