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→isauthoring 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.
1. Physical Layout
Section titled “1. Physical Layout”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 *.
Grid Topology
Section titled “Grid Topology”| Property | Function Grid (Left) | Data Grid (Right) |
|---|---|---|
| Keys | 14 | 16 |
| Rows × Cols | 4 × 4 (minus 2 empties) | 4 × 4 |
| Physical grouping | Semantic clusters by color | Phone-layout numpad (ADR-0016 §5) |
| Keycap legends | Lisp primitives + verbs | Numerals + arithmetic |
| EVAL key | 1.75U wide (centered over col 0, row 4) | — |
Matrix Positions
Section titled “Matrix Positions”The 8×4 matrix scans all 32 intersections. Two are unpopulated:
| Row | Col 0 | Col 1 | Col 2 | Col 3 | Col 4 | Col 5 | Col 6 | Col 7 |
|---|---|---|---|---|---|---|---|---|
| 0 | QUOTE | CONS | NIL | LAMBDA | PAD_1 | PAD_2 | PAD_3 | PAD_DIV |
| 1 | INFO | CAR | APPLY | SYS | PAD_4 | PAD_5 | PAD_6 | PAD_MUL |
| 2 | LINK | BACK | CDR | ATOM | PAD_7 | PAD_8 | PAD_9 | PAD_SUB |
| 3 | EVAL | (empty) | EQ | (empty) | PAD_DOT | PAD_0 | PAD_ENTER | PAD_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.
1A. TERM Key Placement
Section titled “1A. TERM Key Placement”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.
2. Hardware
Section titled “2. Hardware”Switches
Section titled “Switches”| Property | Value |
|---|---|
| Type | Kailh Choc v1 (PG1350) |
| Variant | White (Clicky, 50gf) or Jade (Heavy Clicky, 60gf) |
| Travel | 3mm total, 1.5mm pre-travel to actuation |
| Rated lifespan | 50 million actuations |
| Mounting | PCB-mount, hot-swap sockets (Mill-Max 0305 or Kailh hot-swap) |
| Quantity | 31 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.
Keycaps
Section titled “Keycaps”| Property | Value |
|---|---|
| Profile | MBK (low-profile, uniform, Choc v1 compatible) |
| Material | PBT |
| Legend method | Dye-sublimation or UV printing |
| EVAL key | 1.75U stabilized keycap |
| TERM key | 1.5U stabilized keycap |
Color coding by legend category:
| Row / Category | Legend Color | Keys |
|---|---|---|
| Lisp Primitives | Amber | CAR, CDR, CONS, NIL, ATOM, EQ |
| Action Verbs | White | EVAL, QUOTE, LAMBDA, APPLY |
| System / Navigation | Red | LINK, BACK |
| nOSh runtime | Gray | INFO, SYS |
| Numpad | White | All 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*).
| Property | Value |
|---|---|
| Matrix topology | 8 column + 4 row = 32 intersections, 31 populated |
| Controller | RP2040-class QMK target — Adafruit KB2040 (v0.1) per ADR-0024; Sea-Picro fallback |
| Firmware | Stock 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 path | Keyboard 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 |
| Debounce | 10 ms software window, controller-side |
| Key state storage | 32-bit bitmask (30 bits used) |
PCB path (ADR-0018)
Section titled “PCB path (ADR-0018)”Two acceptable physical realizations of the 31-key matrix:
- 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.
- 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.
Scanning (Emulator: SDL3)
Section titled “Scanning (Emulator: SDL3)”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.
3. The 31 Keys — Complete Reference
Section titled “3. The 31 Keys — Complete Reference”3A. Lisp Primitive Keys (Amber Legends)
Section titled “3A. Lisp Primitive Keys (Amber Legends)”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.
CAR — “First Element” (Value 5)
Section titled “CAR — “First Element” (Value 5)”| Property | Detail |
|---|---|
| Lisp origin | (car '(a b c)) → a |
| KN-86 meaning | Drill into / examine the first child |
| Default behavior | stdlib_drill_into() — push first_child onto nav stack |
| If leaf (no children) | Error beep (stdlib_sfx_error()) |
| Cognitive role | OODA → 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
CDR — “Rest of List” (Value 10)
Section titled “CDR — “Rest of List” (Value 10)”| Property | Detail |
|---|---|
| Lisp origin | (cdr '(a b c)) → (b c) |
| KN-86 meaning | Traverse to next sibling |
| Default behavior | stdlib_next_sibling() — move to next_sibling pointer |
| If no next sibling | Error beep |
| Cognitive role | OODA → 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
CONS — “Construct Pair” (Value 1)
Section titled “CONS — “Construct Pair” (Value 1)”| Property | Detail |
|---|---|
| Lisp origin | (cons 'a '(b c)) → (a b c) |
| KN-86 meaning | Attach / combine / construct |
| Default behavior | None (module-specific) |
| Cognitive role | OODA → 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
NIL — “Empty List” (Value 2)
Section titled “NIL — “Empty List” (Value 2)”| Property | Detail |
|---|---|
| Lisp origin | nil / '() — the empty list |
| KN-86 meaning | Discard / clear / cancel / reset |
| Default behavior | None (module-specific) |
| Cognitive role | Abort/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
ATOM — “Is It a Leaf?” (Value 11)
Section titled “ATOM — “Is It a Leaf?” (Value 11)”| Property | Detail |
|---|---|
| Lisp origin | (atom 'a) → T; (atom '(a b)) → NIL |
| KN-86 meaning | Test if current element is a leaf (no children) |
| Default behavior | stdlib_is_leaf() — returns true/false, often with audio cue |
| Cognitive role | OODA → 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
EQ — “Are They Equal?” (Value 13)
Section titled “EQ — “Are They Equal?” (Value 13)”| Property | Detail |
|---|---|
| Lisp origin | (eq 'a 'a) → T |
| KN-86 meaning | Compare two quoted/bookmarked elements |
| Default behavior | The nOSh runtime compares current element against last QUOTE’d reference |
| Cognitive role | OODA → 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
3B. Action Keys (White Legends)
Section titled “3B. Action Keys (White Legends)”EVAL — “Execute” (Value 12)
Section titled “EVAL — “Execute” (Value 12)”| Property | Detail |
|---|---|
| Lisp origin | (eval '(+ 1 2)) → 3 |
| KN-86 meaning | Execute / confirm / commit action |
| Physical note | 1.75U wide key — the largest and most prominent key on the deck |
| Cognitive role | OODA → Act (commit your decision, trigger resolution) |
| Tempo role | The “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)”| Property | Detail |
|---|---|
| Lisp origin | (quote x) / 'x — return x without evaluating |
| KN-86 meaning | Bookmark current element for later reference |
| nOSh runtime service | Press QUOTE → QUO? prompt → numpad 1-8 → Cell ID stored in quote slot |
| Storage | 8 slots in SRAM (volatile across power-off, persistent within session) |
| Semantics | Stores 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
LAMBDA — “Define Macro” (Value 3)
Section titled “LAMBDA — “Define Macro” (Value 3)”| Property | Detail |
|---|---|
| Lisp origin | (lambda (x) (* x x)) — anonymous function |
| KN-86 meaning | Record a sequence of keystrokes for replay |
| nOSh runtime service | Hold LAMBDA 2s → λREC indicator → press keys → press LAMBDA to stop → numpad 1-8 to assign |
| Playback | Tap LAMBDA → λ? → numpad 1-8 → the nOSh runtime replays sequence |
| Quick invoke | LAMBDA + digit simultaneously → instant playback |
| Storage | 8 slots × 32 key events max = 256 bytes in wear-leveled flash |
| Transparency | Replayed 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
APPLY — “Deploy Tool” (Value 6)
Section titled “APPLY — “Deploy Tool” (Value 6)”| Property | Detail |
|---|---|
| Lisp origin | (apply fn args) — apply function to arguments |
| KN-86 meaning | Deploy / use selected tool against current target |
| Cognitive role | OODA → 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)”BACK — “Navigate Up” (Value 9)
Section titled “BACK — “Navigate Up” (Value 9)”| Property | Detail |
|---|---|
| KN-86 meaning | Pop navigation stack — return to parent context |
| Default behavior | stdlib_navigate_back() — pop nav stack, call on_exit/on_enter |
| Legend color | Red |
| nOSh runtime note | If nav stack is empty at runtime level (depth 0), BACK is ignored |
INFO — “Inspect” (Value 4)
Section titled “INFO — “Inspect” (Value 4)”| Property | Detail |
|---|---|
| KN-86 meaning | Show detailed information about current element |
| Legend color | Gray |
| Cognitive role | OODA → 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
LINK — “Initiate Link” (Value 8)
Section titled “LINK — “Initiate Link” (Value 8)”| Property | Detail |
|---|---|
| KN-86 meaning | Initiate a link operation between elements |
| Legend color | Red |
| nOSh runtime note | LINK 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
SYS — “System Menu” (Value 7)
Section titled “SYS — “System Menu” (Value 7)”| Property | Detail |
|---|---|
| KN-86 meaning | Tap: open system menu. Hold 2s: hard abort |
| Legend color | Gray |
| Tap behavior | Context menu (brightness, volume, deck status, key test) |
| Hold behavior (2s) | Force-quit to boot screen (replaces ESC — there is no ESC key) |
| nOSh runtime note | SYS 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)”| Property | Detail |
|---|---|
| KN-86 meaning | Context-sensitive — the meaning is determined by current deck state |
| Legend color | Gray |
| 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 behaviors | See §3C.1 below |
| nOSh runtime note | TERM 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. |
3C.1 TERM Context-Sensitivity Table
Section titled “3C.1 TERM Context-Sensitivity Table”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.
| Priority | Context | TERM binding | Label 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 |
| 2 | Hot-swap prompt active; nOSh runtime awaiting operator confirmation | Skip swap. Defer swap and put the mission on hold (same as QUOTE-hold; provides a second, ambidextrous path). | TERM: DEFER |
| 3 | Null module, Cipher-Analysis bounty active | Freeze Cipher state. Snapshot current coherence stack and memory-store for analysis; the nOSh runtime renders the snapshot on the main grid. | TERM: FREEZE |
| 4 | Cart has registered a cart-scoped binding and the cart is the active cart | Cart-defined behavior. Cart provides the label; the nOSh runtime renders it on Row 4 unless a higher-priority context overrides. | TERM: <cart-label> |
| 5 | Bare deck, no cart inserted | Open bare-deck terminal REPL. Toggle the main-grid into the nOSh runtime REPL (nEmacs-minor-mode-style). | TERM: REPL |
| 6 (lowest) | None of the above | No-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.
3C.2 TERM Hold Behavior
Section titled “3C.2 TERM Hold Behavior”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.
3D. Data Grid (White Numeral Legends)
Section titled “3D. Data Grid (White Numeral Legends)”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 isWXYZ. 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.
Numeric Data Entry (Primary)
Section titled “Numeric Data Entry (Primary)”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) diagonals7/9= third-row (SW / SE) diagonals5= 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).
Lambda/Quote Slot Selection
Section titled “Lambda/Quote Slot Selection”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.
Value Adjustment
Section titled “Value Adjustment”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:
-
Bare Deck only. The reservation is in force only when no cartridge is inserted (the
:bare-deckbeat) 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. -
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’son_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. -
A cart may reclaim explicit ownership via the
:override-digit-dispatchcapability (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.
The :override-digit-dispatch capability
Section titled “The :override-digit-dispatch capability”: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| Property | Value |
|---|---|
| Declared in | Cart capability manifest ((defcapabilities …)), read from the .kn86 header at cart-load |
| Type | Boolean 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. |
| Scope | Cart 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 overridden | The 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:
- Runtime modal digit consumer active (LAMBDA
λ?/ QUOTEQUO?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.) - Bare Deck, at tab level, no cart inserted → digit 1–5 is a tab jump (
:override-digit-dispatchis irrelevant here — there is no cart to declare it). - 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-dispatchis set; the flag makes the guarantee explicit and binding against future runtime digit accelerators. - 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.
Numpad Key Reference
Section titled “Numpad Key Reference”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 code | Value | Position | Primary legend | Shift secondary | Multi-tap letters | Primary Role |
|---|---|---|---|---|---|---|
| PAD_1 | 22 | R1C1 | 1 | ` (backtick) | — | Digit / slot select |
| PAD_2 | 23 | R1C2 | 2 | — | ABC | Digit / slot select |
| PAD_3 | 24 | R1C3 | 3 | — | DEF | Digit / slot select |
| PAD_DIV | 17 | R1C4 | / | " | — | Slash / case-toggle in :nemacs-literal + :prompt-text (ADR-0022 §7) |
| PAD_4 | 18 | R2C1 | 4 | — | GHI | Digit / slot select |
| PAD_5 | 19 | R2C2 | 5 | — | JKL | Digit / slot select |
| PAD_6 | 20 | R2C3 | 6 | — | MNO | Digit / slot select |
| PAD_MUL | 21 | R2C4 | ; | : | — | Statement separator / Lisp keyword qualifier on shift |
| PAD_7 | 14 | R3C1 | 7 | — | PQRS | Digit / slot select |
| PAD_8 | 15 | R3C2 | 8 | — | TUV | Digit / slot select |
| PAD_9 | 16 | R3C3 | 9 | — | WXYZ | Digit |
| PAD_SUB | 25 | R3C4 | - | + | — | Decrement / minus; arithmetic pair with shift |
| PAD_DOT | 27 | R4C1 | * | ( | — | Star / delete in :nemacs-literal + :prompt-text (ADR-0022 §7); shift produces ( |
| PAD_0 | 26 | R4C2 | 0 | . | — | Digit; shift produces decimal point |
| PAD_ENTER | 28 | R4C3 | # | ) | — | 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_ADD | 29 | R4C4 | ENT | — | — | Confirm numpad entry — see §3E for dual-binding |
3E. PAD_ENTER (ENT) Dual-Binding
Section titled “3E. PAD_ENTER (ENT) 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:
| Context | Binding | Behavior |
|---|---|---|
| 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 form | Evaluate 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.
3F. TERM Key Default Binding (GWP-230)
Section titled “3F. TERM Key Default Binding (GWP-230)”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.
4. Input Processing Pipeline
Section titled “4. Input Processing Pipeline”4A. Event Flow
Section titled “4A. Event Flow”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) │└─────────────────────────────────────────────┘4B. Event Types
Section titled “4B. Event Types”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;4C. Input Queue
Section titled “4C. Input Queue”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.
4D. Key State Tracking
Section titled “4D. Key State Tracking”uint8_t key_states[30]; /* 1 = currently pressed, 0 = released */uint32_t key_hold_time[30]; /* Timestamp of last KEY_DOWN (0 if released) */5. Cartridge SDK — Input API
Section titled “5. Cartridge SDK — Input API”5A. Handler Macros (nosh_cart.h)
Section titled “5A. Handler Macros (nosh_cart.h)”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 */5B. Handler Table Registration
Section titled “5B. Handler Table Registration”/* 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 */);5C. Runtime Dispatch (nosh_runtime.c)
Section titled “5C. Runtime Dispatch (nosh_runtime.c)”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).
5D. Navigation Stack
Section titled “5D. Navigation Stack”The nOSh runtime maintains a 32-deep navigation stack. Standard library helpers manage it:
| Function | Effect |
|---|---|
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.
6. nOSh-Runtime-Level Key Services
Section titled “6. nOSh-Runtime-Level Key Services”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.
6A. Lambda Macro System
Section titled “6A. Lambda Macro System”| Property | Value |
|---|---|
| Trigger | Hold LAMBDA for 2000ms |
| Indicator | λREC in status bar |
| Capacity | 8 slots × 32 key events each |
| Storage | 256 bytes in wear-leveled flash (production) / file (emulator) |
| Playback | Tap LAMBDA → λ? → numpad 1-8 |
| Quick invoke | LAMBDA + digit simultaneously |
Recording flow:
- Hold LAMBDA 2s →
λRECappears, recording starts - Press any sequence of keys (up to 32 events)
- Press LAMBDA to stop →
λ?prompt - Press numpad 1-8 to assign slot (overwrites existing)
Playback flow:
- Tap LAMBDA →
λ?prompt - Press numpad 1-8 → the nOSh runtime replays at original timing
- Events enter input queue — cartridge code cannot distinguish live from macro
6B. Quote Bookmark System
Section titled “6B. Quote Bookmark System”| Property | Value |
|---|---|
| Trigger | Tap QUOTE |
| Indicator | QUO? prompt |
| Capacity | 8 slots, each holds a Cell ID reference |
| Storage | SRAM (volatile across power-off, persistent across cartridge swaps) |
| Semantics | Reference (not snapshot) — like Lisp quote |
6C. SYS Hold — Emergency Abort
Section titled “6C. SYS Hold — Emergency Abort”| Property | Value |
|---|---|
| Trigger | Hold SYS for 2000ms |
| Effect | Unconditional abort to boot screen |
| Override | Not possible — the nOSh runtime handles before cartridge dispatch |
| Purpose | Replaces ESC key. The KN-86 has no ESC. |
6D. TERM Mode Selector
Section titled “6D. TERM Mode Selector”| Property | Value |
|---|---|
| Trigger | Tap TERM |
| Effect | Evaluates priority table (§3C.1) at key-press time, dispatches the matching binding |
| Override | Cartridges may register a priority-4 cart-scoped binding via (term-register-binding ...); nOSh runtime priorities 1–3 and 5 always win |
| Purpose | Single context-sensitive affordance for nOSh runtime modes (seed capture, hot-swap defer, Cipher freeze, REPL) |
6E. TERM Hold — CIPHER Mute Toggle
Section titled “6E. TERM Hold — CIPHER Mute Toggle”| Property | Value |
|---|---|
| Trigger | Hold TERM for 500ms |
| Effect | Toggle cipher_muted flag; blank CIPHER-LINE Rows 2–3 and suspend Cipher engine emission (when muted); restore emission (when unmuted) |
| Override | Not possible — the nOSh runtime handles before cartridge dispatch |
| Purpose | Operator kill-switch for the Cipher voice; stored in Universal Deck State and persistent across power cycles |
6F. CIPHER-LINE Seed Capture Flow
Section titled “6F. CIPHER-LINE Seed Capture Flow”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_DEFAULTAux 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.
7. OODA Cognitive Framework
Section titled “7. OODA Cognitive Framework”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 OBSERVETempo 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.
8. Key Remapping Architecture
Section titled “8. Key Remapping Architecture”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 ... */}8B. Planned: Configurable Keymapping
Section titled “8B. Planned: Configurable Keymapping”Per ADR-001 (docs/adr/001-keymap-config-format.md), the remapping system adds:
- Config file format: INI-style
KN86_KEY = SDL_SCANCODEpairs - CLI flag:
--keymap <path>loads custom mapping - Default keymap:
assets/default.keymap— matches current hardcoded mapping - Laptop keymap:
assets/laptop.keymap— UIOP/JKL;/M,./ replaces numpad - Runtime rebind: SYS menu → key test mode → press key to remap → EVAL to confirm
- API:
keymap_load(),keymap_save(),keymap_init_defaults()
8C. Default Emulator Mapping
Section titled “8C. Default Emulator Mapping”| KN86 Key | SDL Scancode | QWERTY Key | Hand | Grid Position |
|---|---|---|---|---|
| QUOTE | SDL_SCANCODE_Q | Q | Left | Func R1C1 |
| CONS | SDL_SCANCODE_W | W | Left | Func R1C2 |
| NIL | SDL_SCANCODE_E | E | Left | Func R1C3 |
| LAMBDA | SDL_SCANCODE_R | R | Left | Func R1C4 |
| INFO | SDL_SCANCODE_A | A | Left | Func R2C1 |
| CAR | SDL_SCANCODE_S | S | Left | Func R2C2 |
| APPLY | SDL_SCANCODE_D | D | Left | Func R2C3 |
| SYS | SDL_SCANCODE_F | F | Left | Func R2C4 |
| LINK | SDL_SCANCODE_Z | Z | Left | Func R3C1 |
| BACK | SDL_SCANCODE_X | X | Left | Func R3C2 |
| CDR | SDL_SCANCODE_C | C | Left | Func R3C3 |
| ATOM | SDL_SCANCODE_V | V | Left | Func R3C4 |
| EVAL | SDL_SCANCODE_1 | 1 | Left | Func R4C1 (1.75U) |
| EQ | SDL_SCANCODE_2 | 2 | Left | Func R4C3 |
| PAD_1 | SDL_SCANCODE_KP_1 | Numpad 1 | Right | Data R1C1 |
| PAD_2 | SDL_SCANCODE_KP_2 | Numpad 2 | Right | Data R1C2 |
| PAD_3 | SDL_SCANCODE_KP_3 | Numpad 3 | Right | Data R1C3 |
| PAD_DIV | SDL_SCANCODE_KP_DIVIDE | Numpad / | Right | Data R1C4 |
| PAD_4 | SDL_SCANCODE_KP_4 | Numpad 4 | Right | Data R2C1 |
| PAD_5 | SDL_SCANCODE_KP_5 | Numpad 5 | Right | Data R2C2 |
| PAD_6 | SDL_SCANCODE_KP_6 | Numpad 6 | Right | Data R2C3 |
| PAD_MUL | SDL_SCANCODE_KP_MULTIPLY | Numpad * | Right | Data R2C4 |
| PAD_7 | SDL_SCANCODE_KP_7 | Numpad 7 | Right | Data R3C1 |
| PAD_8 | SDL_SCANCODE_KP_8 | Numpad 8 | Right | Data R3C2 |
| PAD_9 | SDL_SCANCODE_KP_9 | Numpad 9 | Right | Data R3C3 |
| PAD_SUB | SDL_SCANCODE_KP_MINUS | Numpad - | Right | Data R3C4 |
| PAD_DOT | SDL_SCANCODE_KP_PERIOD | Numpad . | Right | Data R4C1 |
| PAD_0 | SDL_SCANCODE_KP_0 | Numpad 0 | Right | Data R4C2 |
| PAD_ENTER | SDL_SCANCODE_KP_ENTER | Numpad Enter | Right | Data R4C3 |
| PAD_ADD | SDL_SCANCODE_KP_PLUS | Numpad + | Right | Data R4C4 |
| TERM | SDL_SCANCODE_GRAVE | ` (backtick) | Either | Func R4C4 (matrix 3,3) |
Note: The
SDL_SCANCODE_KP_Nbindings are keyboard-level scancodes — they refer to the USB keyboard’s physical keycap, not the KN-86 position. A USB numpad’s7keycap stays7(still the top-left of the USB numpad because laptops/desktops use calculator-layout numpads); it’s the KN-86 position ofKN86_KEY_PAD_7that 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.
8D. Laptop Keymap
Section titled “8D. Laptop Keymap”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.keymapand 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.
9. Design Invariants
Section titled “9. Design Invariants”These rules cannot be violated by any module, remapping, or future extension:
- 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) plusKN86_KEY_TERMat 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). - Two grids, two hands. Left = function (semantic operations). Right = data (numeric entry). This split is architectural, not cosmetic.
- 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.
- LAMBDA/QUOTE belong to the nOSh runtime. Macro recording and bookmarking are nOSh runtime services. Cartridges can define
on_lambdaandon_quotehandlers for conditional behavior during recording, but the services themselves belong to the nOSh runtime. - 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).
- 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.
- 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.
- 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/,;,-,*,#,ENTper 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.