Skip to content

KN-86 Deckline — Input System Architecture

Definitive specification for the 30-key input system plus the TERM context-sensitive key: physical layout, hardware, functional semantics, nOSh runtime services, and cartridge SDK.

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


The KN-86 Deckline has 30 primary keys arranged in two distinct grids separated by the amber LCD, plus a 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 │ λ ║ ║ 1 │ 2 │ 3 │ ÷ ║
║ (defer) │ (build) │ (nil) │(func)║ ║ │ │ │ (divide) ║
╠═══════════╪═══════════╪═══════╪══════╣ ╠════════╪════════╪════════╪════════════╣
║ INFO │ CAR │ APPLY │ SYS ║ ║ 4 │ 5 │ 6 │ × ║
║ (scan) │ (first) │ (use) │(menu)║ ║ │ │ │ (multiply) ║
╠═══════════╪═══════════╪═══════╪══════╣ ╠════════╪════════╪════════╪════════════╣
║ LINK │ BACK │ CDR │ ATOM ║ ║ 7 │ 8 │ 9 │ − ║
║ (link) │ (up) │(rest) │(leaf)║ ║ │ │ │ (subtract) ║
╠═══════════╧═══════════╪═══════╪══════╣ ╠════════╪════════╪════════╪════════════╣
║ ════ EVAL (3U) ════ │ EQ │░░░░░░║ ║ . │ 0 │ ENT │ + ║
║ (execute / commit) │(equal)│░empty║ ║ (point)│ │(enter) │ (add) ║
╚═══════════════════════╧═══════╧══════╝ ╚════════╧════════╧════════╧════════════╝

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. Arithmetic column (÷, ×, −, +) and ENT position are unchanged from earlier drafts.

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 key3U wide (spans cols 1-3, 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 3U 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); (3,1) remains unpopulated.

TERM is a 1U key seated to the immediate right of the EVAL bar, 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)
Quantity30 populated + 5 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 key3U 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 30-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, 30 populated
ControllerQMK-compatible (ATmega32U4, RP2040-class, or equivalent); exact part chosen at hardware bring-up
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 30-key matrix:

  1. Custom-fab unified 30-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 30 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 30-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 note3U 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.

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 (÷, ×, −, +) serve as additional data keys or operators in some contexts.

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_ADD and PAD_SUB increment/decrement values. Numpad digits can set values directly (e.g., pressing 7 sets volume to 70%). The arithmetic operator keys (÷, ×, −, +) can serve as modifier/mode keys in module-specific contexts.

KeyValueLabelPrimary Role
PAD_7147Digit / slot select
PAD_8158Digit / slot select
PAD_9169Digit
PAD_DIV17÷Context-specific
PAD_4184Digit / slot select
PAD_5195Digit / slot select
PAD_6206Digit / slot select
PAD_MUL21×Context-specific
PAD_1221Digit / slot select
PAD_2232Digit / slot select
PAD_3243Digit / slot select
PAD_SUB25Decrement / minus
PAD_0260Digit
PAD_DOT27.Decimal / separator
PAD_ENTER28ENTConfirm numpad entry
PAD_ADD29+Increment / plus

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/architecture/KN-86-CIPHER-LINE-Grammar-Framework.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 (3U)
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

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.

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.

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.


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

  1. 30 keys, no more. The physical device has exactly 30 switches. The emulator must expose exactly 30 KN86 key codes.
  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.” ÷, ×, −, + are also numpad keys and follow the same rule: raw values, module-interpreted meaning.