Skip to content

Text-entry surface survey (2026-07-29)

Phase 1 of the hardware text-entry interaction design. This document enumerates every KN-86 surface that accepts operator-authored characters, records what alphabet each one needs, and states the constraints a single interaction model has to satisfy. It selects nothing. Candidate interaction models are judged in the kn86-textentry workbench (Phase 2); the selected model and its seam land as an ADR (Phase 3).

Governing documents: ADR-0016 §6 to §9 (the multi-tap model, the prompt-text FFI, the tap / double-tap / long-press event model), ADR-0044 (the current three-layer keyboard authority), ADR-0046 (programs as major modes), ADR-0042 (the program roster), and the Canonical Hardware Specification (key count and layer model).

Two paths reach a composer today, and they share nothing.

Path A, the emulator character path. A screen registers a one-argument handler in a registry separate from the render / on-key registry:

SDL_EVENT_TEXT_INPUT hosts/emulator/src/main.c:613
-> nosh_host_handle_char(ch) runtime/src/nosh_host.c:1032
-> screen_router_handle_key(rt, KN86_KEY_NONE, ch)
-> (deck/dispatch-char name ch) runtime/src/sys_screen.c:139
-> the screen's registered handler runtime/system-image/lib/deck.lsp:131

Three screens register a handler: the REPL (repl/input-putc, runtime/system-image/lib/repl/repl.lsp:406), AmberCalc (runtime/programs/ambercalc/ambercalc.lsp:1283), and CALC (runtime/programs/calc/calc.lsp:502). The registry is gated on screen_router_is_active, so the path exists for system screens and programs and never for carts.

Path B, the multi-tap machine. runtime/src/nokia.c is the ADR-0016 §6 state machine: digits 2 to 9 cycle letters, 0 commits a space, 1 cycles . , ' " : ; !, / shifts case, * deletes, ENT commits, and a 600 ms dwell (NOKIA_COMMIT_TIMEOUT_MS) auto-commits the pending letter. The module is pure: the caller owns a NokiaState and supplies every timestamp.

Two callers own an instance:

OwnerInstanceReached from Lisp as
runtime/src/nemacs.c:125g_nemacs.nokianothing program-agnostic; drives :nemacs-literal scope and the prompt-text modal
runtime/src/deck_aux.c:67g_aux.nokiadeck/oobe-press!, deck/oobe-tick!, deck/oobe-handle, deck/oobe-handle-len, deck/oobe-pending

Two further files touch the machine without owning one: runtime/src/dev_overlay.c:313 reads nemacs’s instance through nemacs_nokia_state_const() for a debug readout, and runtime/src/input.c:297 gates double-tap synthesis on numpad keys while multi-tap scope is active (input_set_suppress_double_tap).

The cart-facing seam that already exists. (prompt-text "label" max-len) is bound in the cart FFI bridge (runtime/src/nosh_lisp_bridge.c:3920) and runs a blocking modal: the host pumps its own event loop, feeds keys through nemacs_literal_keypress, echoes on CIPHER-LINE Row 4, and returns the string or nil on cancel (hosts/emulator/src/main.c:523). No cart calls it. Grepping carts/ for prompt-text returns nothing.

Path A carries letters and is fed only by SDL_EVENT_TEXT_INPUT. Path B carries letters from the physical keyboard and is reachable from Lisp only under deck/oobe-* names. The two never meet, so no program can accept letters on the deck.

The keyboard makes the gap concrete. Under ADR-0044 the printed L0 and L1 layers carry digits, the five punctuation primaries, the function block, and the KEC punctuation set. Neither printed layer carries a letter. The unprinted L2 (§4) carries the rare ASCII tail and no letters either. Letters exist on the deck in exactly one place: the ADR-0016 §6 digit-to-letter binding that nokia.c implements.

A second defect follows from the same split. The QMK keymap emits real letter scancodes for the function block (KC_Q for QUOTE, KC_W for CONS, and so on; hosts/device/firmware/keyboard/keymap.c:161), and runtime/src/input.c remaps those scancodes to KN86_KEY_QUOTE / KN86_KEY_CONS. Text input is started whenever a composer screen is active (runtime/src/nosh_host.c:598). On the device, one press of QUOTE therefore delivers both a KN86_KEY_QUOTE dispatch and a TEXT_INPUT character q into the composer. On hardware Path A carries the wrong letters. Any selected model has to state which path owns characters on the device, and close the other.

That keymap is also pre-ADR-0031: it encodes the retired 31-key matrix with a single _BASE layer, no SHIFT layer, and no L2. The Sweep retarget is a separate open piece of work and is a dependency of any candidate that puts letters in firmware.

Columns: alphabet is the smallest character set the surface has to accept; length is the typical committed field; carried by records whether ranked completion, history, or a fixed grammar can do most of the work instead of per-character entry; typing is whether entry is the primary interaction of the surface or an occasional one.

3.1 First-party programs (runtime/programs/, ADR-0042 roster)

Section titled “3.1 First-party programs (runtime/programs/, ADR-0042 roster)”
ProgramWhat gets typedAlphabetLengthCarried byTyping
ambercalccell literals, @row$col refs, formulas (ADR-0049)digits, @ $ . + - * / ( ), uppercase letters for function names3 to 24grammar: refs and operators are structural, function names are a closed setprimary
calcRPN number entrydigits, ., sign1 to 12nothing; numeric onlyprimary
conduitendpoints, credentials, then arbitrary remote shell commandsfull ASCII4 to 60history repeats the session; the dialer is a closed endpoint set; credentials come from Keyringprimary, and the hardest program case
bzbxa filter grammar over hosts and captures, hostnameslowercase letters, digits, . : / >4 to 24completion over the scanned host list; the filter keys are a closed setoccasional
kommanderpath fragments for find and filterlowercase letters, digits, . / _ -3 to 32completion over the mount index (kommander/find-index) does nearly all of itoccasional
ripsawentity-name queriesuppercase letters, digits, space3 to 24completion over ripsaw/entity-names; ido-style narrowingoccasional
kn9search over from and subjectletters, digits, @ . -3 to 24completion over kn9/search-indexoccasional
keyringkey labels, method and lock selectionuppercase letters, digits, -4 to 16selection is completing-read; only a new label is free textoccasional
dossiernothing today; browse onlynone0index navigationnone
tracenothing; pursuit gridnone0key verbsnone
echooperator utterances addressed to CIPHERfull ASCII, sentence-shaped10 to 80nothing; free prose is the pointprimary, and the longest fields on the deck
knampnothing today; station browse and tunenone0list selectionnone

Six of the twelve need no character entry at all or need it only through completion. The four narrowing programs (bzbx, kommander, ripsaw, kn9) all reach text through completing-read over an index the program already holds, which is the same shape in every one of them. CONDUIT and ECHO are the two surfaces where free characters are the interaction rather than a way to filter a list.

3.2 System-image libraries (runtime/system-image/lib/)

Section titled “3.2 System-image libraries (runtime/system-image/lib/)”
SurfaceWhat gets typedAlphabetLengthCarried byTyping
REPLarbitrary KEC Lisp formsfull ASCII including every KEC punctuation character5 to 70history ring, token prediction (ADR-0009), the TERM command paletteprimary; the hardest case on the deck
nEmacssymbols and literals inside a structure editorfull ASCII, Lisp-symbol shaped3 to 40structural editing supplies the parens; prediction supplies the symbolprimary
deck (OOBE)the operator handleuppercase letters, digits1 to 12 (DECK_AUX_HANDLE_MAX)nothing; a name is a nameonce per device
mission boardnothing; offers are selectednone0list selectionnone

The REPL sets the ceiling. Anything that can type an idiomatic KEC form can type everything else on the deck, which is why the REPL is the acceptance surface for the selected model rather than a special case bolted on afterward.

None of the five shipped carts (snake, icebreaker, neongrid, depthcharge, blackledger) accepts characters. Three constraints make a cart’s entry surface narrower than a program’s:

  • The cart context binds one library. cartridge.c embeds ui/ui/draw.lsp only (runtime/src/cartridge.c:37). A cart cannot compose ui/field, ui/palette, or any other System-tier component, because render/* is unbound in a cart context by design. A cart-tier field has to be drawn through the cell-* tier or drawn for the cart by the runtime.
  • A cart has no clock. Nothing in the cart tier reads monotonic time, so a cart cannot drive a dwell timeout itself. nokia_tick needs a timestamp from a caller that has one.
  • Row 0 and Row 74 are runtime-owned. A cart draws Rows 1 to 73, so an entry field that wants chrome rows has to be composed by the runtime on the cart’s behalf.

The consequence: the cart tier wants a runtime-owned modal rather than a component a cart draws itself. prompt-text is already that shape. Whatever model is selected, the cart seam should stay one call that returns a string or nil, with the runtime owning the field, the timing, and the echo.

  1. Three alphabet classes. Numeric-only (calc, ambercalc literals), a closed identifier set reached by narrowing (bzbx, kommander, ripsaw, kn9, keyring), and free ASCII (REPL, nEmacs, CONDUIT, ECHO, the OOBE handle). A model that serves only the third makes the first two slower than they are today; a model that serves only the first two leaves the REPL unusable on hardware.
  2. Completion carries the majority of surfaces. Four of the five programs that accept text already narrow a list the program holds. For those, the cost that matters is the cost of the first two or three characters; the twentieth character is never reached.
  3. The REPL is the ceiling and the acceptance test. Full ASCII, every KEC punctuation character, and lines up to about 70 characters.
  4. The dwell timeout is the weakest part of the shipped machine. The on-glass clock is coarse, and a 600 ms dwell prices every letter that follows a letter on the same key. A model that commits on an explicit key instead of a dwell removes the timing dependency from the interaction and from the tests.
  5. Two tiers, one machine. The program tier wants a registered handler and a composed field (Path A’s shape, with a real character source behind it). The cart tier wants a blocking modal (prompt-text’s shape). Both should sit on one instance of the machine behind program-agnostic names, with deck/oobe-* preserved so first boot does not regress.
  6. The split across runtime and firmware is a real fork in the design. A model that assigns letters through QMK layers or combos lands in hosts/device/firmware/keyboard/, needs the Sweep retarget first, and changes nothing in the runtime except deleting the multi-tap path. A model that keeps letters in the runtime lands in runtime/src/ and leaves the keymap alone. Candidates state which side they fall on.

Fourteen candidates are staged in the kn86-textentry workbench (port 7731) with a keypad simulator over the ADR-0031 §3.1 L0 manifest, so each model can be typed rather than read about. Twelve are simulatable; the two firmware-layer models are shown as layer maps. Costs are counted uniformly: one press is one key actuation, a shift-layer glyph counts as two, and a dwell wait counts as zero presses and is recorded in the clock column instead.

CodeModelLands inClockFLINT(nil? x)kx-04.hab
MT-DWELLmulti-tap, dwell commit (as shipped)runtimeyes131921
MT-COMMITmulti-tap, commit key, no timeoutruntimeno131921
MT-STRICTmulti-tap, uniform commit per letterruntimeno182427
PICK-GRIDon-screen glyph grid, cursor-navigatedruntimeno324146
PICK-2KEYtwo-key glyph paging, group then memberruntimeno111619
PREFIX-ESCone-glyph punctuation escape (a modifier)runtimeno131719
TRACK-WHEELtrackpoint glyph ring (ADR-0035)runtimeno111921
T9-WORDpredictive over a deck dictionaryruntimeno61110
T9-KECpredictive over the live KEC symbol tableruntimeno131321
PALETTE-1STcompletion-first; nothing is ever spelledruntimeno1333
QMK-ALPHAa latched L3 alpha layer, one press per letterfirmwareno71011
QMK-CHORDletters from two-key combos on the digit blockfirmwareyes61011
HIST-1SThistory and prediction first (a modifier)runtimeno1343
HYBRIDper-tier profiles over one machinecompositionno--3

Three of these are modifiers rather than whole models (PREFIX-ESC, HIST-1ST, and HYBRID as the composition), and the workbench tags them so their value is not double-counted inside another card.

Two findings came out of building the simulator and belong in the record:

  • Committed and pending must never render alike. A dwell commit changes no glyph on screen unless the pending letter is visually distinct, so an operator cannot tell whether the dwell has fired. nokia_pending already exposes the pending character separately from nokia_buffer; the field component has to render the two differently, and the ADR-0016 §6 echo format ([<committed><pending>_]) does not by itself.
  • Punctuation-carrying symbols need a digit signature that skips the punctuation. A T9 signature for nil? runs to three digits, because ? sits on no multi-tap table. Any predictive model over the KEC symbol table has to strip unmappable characters when it builds the signature and restore them on acceptance.
  • Which interaction model the deck ships. Judged in the workbench (Phase 2).
  • Whether the function block’s letter scancodes (KC_Q and the rest) are remapped in firmware to non-letter codes, or whether text input is gated per surface in the runtime. Both close the spurious-character defect in §2; the selected model decides which.
  • The Sweep keymap retarget, a dependency of any firmware-side candidate.