Layout switcher — phase plan
Roadmap for letting users pick a keyboard layout (QWERTY, Colemak, Miryoku, etc.) in the Cosmos editor and have it flow through both visual legends and firmware (ZMK/QMK) output. One PR per phase.
Background
Before this work, Cosmos was QWERTY-only:
letterForKeycap()insrc/lib/worker/config.tsbaked QWERTY letters into every alpha key as it was generated.FLIPPED_KEYinsrc/lib/geometry/keycaps.tsmirrored those letters for the left half.keycap.letterflowed straight to the firmware exporters; ZMK emitted&kp <LETTER>and QMK emittedKC_<LETTER>from whatever was stored on the key.
Three discoveries shaped the phases:
- No layer system exists. No layer UI, no layer field on the data model. ZMK exports a single
default_layer; QMK exports[0] = LAYOUT(...). Mod-tap, layer-tap, and hold-tap are all unimplemented. - Key counts are flexible (~14 to 42+). But there’s no semantic notion of “home-row index finger” —
home: 'index'exists only as a cosmetic hint for MT3 keycap row selection, not something firmware can target. - Letter-swap layouts and Miryoku are different problems. QWERTY/Colemak/Dvorak/Workman are alpha remaps that fit the existing single-layer pipeline. Miryoku is a 36-key, 6-layer system built on home-row mods and layer-taps — it requires layers, mod-tap encoding, and assumptions about which physical keys play which Miryoku roles.
Phase 1 — Letter-swap layouts (PR #3, in flight)
In scope: QWERTY (default), Colemak, Colemak-DH, Dvorak, Workman.
Design decisions:
- Layout lives at the top level, not per-key. Stored on
CosmosKeyboardaslayout: LayoutIdand persisted in the URL via proto fieldlayout = 33(uint32, with QWERTY trimmed to keep old URLs unchanged). - Letters resolve through layout at generation time, not at render time.
cosmosFingers()andkeycapInfo()accept an optional layout (default QWERTY) and write the correct letter intokey.profile.letter. Firmware exporters need no changes — they already readkeycap.letter. - Layout switching rewrites the alpha block. A new
applyLayoutToKeys()walks finger clusters, identifies alpha columns via the existingalphaColumns()heuristic, and updates only rows 2/3/4. Number row, F-row, and outer-punctuation keys stay untouched (they’re layout-independent). - Mirror is layout-aware.
flippedKey()andmirrorCluster()accept an optional layout. Editor callers threadkbd.layoutthrough;transformation-ext.ts-level mirror callers default to QWERTY (legacy code path, rarely hit by non-QWERTY users). - Per-key letter overrides are supported by the data model (the stored
keycap.letterwins) but there’s no UI for them yet. That’s deferred — when an editor exists in Phase 3, it’ll co-exist with the layout setting.
Files touched:
| Area | File |
|---|---|
| New registry | src/lib/layouts/index.ts (LAYOUT.* const, DEFAULT_LAYOUT, LAYOUT_IDS, getLayout, rightSideLetter, flipLetter) |
| Proto + types | src/proto/cosmos.proto, src/lib/worker/config.cosmos.ts, src/lib/worker/config.serialize.ts |
| Letter generation | src/lib/worker/config.ts (letterForKeycap, cosmosFingers, keycapInfo) |
| Mirror | src/lib/geometry/keycaps.ts (flippedKey), src/lib/worker/config.cosmos.ts (mirrorCluster) |
| Editor | src/routes/beta/lib/editor/visualEditorHelpers.ts (applyLayoutToKeys), src/routes/beta/lib/editor/VisualEditor2.svelte (UI) |
| Firmware exports | (no changes — keycap.letter is now layout-aware end-to-end) |
Tests: src/lib/layouts/layouts.test.ts (5 layouts × letter lookup, flip behavior); src/lib/layouts/layoutEndToEnd.test.ts (proto round-trip per layout, legacy URL back-compat, applyLayoutToKeys, ZMK/QMK keycode contract).
Out of scope: layers, mod-tap/layer-tap, Miryoku, per-key letter override UI.
Phase 2 — Miryoku bundle
In scope: ship Miryoku as a fixed preset bundle, not as customizable layers. Apply Miryoku → 6 layers materialize in the firmware export.
Why this needs a separate PR: introduces three things Cosmos doesn’t have:
- Layer data model. Probably an array of layers on
CosmosKeyboard, each layer beingMap<keyId, KeyAction>whereKeyActioncovers basic kp, mod-tap, layer-tap, transparent, and “no-op.” Phase 1’s flat letter model survives as the implicit default-layer view. - Mod-tap / layer-tap encoding in
src/routes/beta/lib/firmware/zmk.tsandqmk.ts. ZMK gets&mt LSHFT A,< 1 SPACE, etc. QMK getsMT(MOD_LSFT, KC_A),LT(1, KC_SPC). CHARS/SPECIALS tables stay; the keycode generator branches onKeyAction.kind. - Miryoku slot assignment. Miryoku has 36 named slots (LH4/LH3/LH2/LT1, etc.). Cosmos doesn’t tag keys with those roles. The user picks “apply Miryoku” → UI shows the 36 slots with smart suggestions based on physical position (index/middle/ring/pinky finger and thumb cluster), and the user can override each slot. Slot assignments persist in the config.
Open questions to settle when starting Phase 2:
- How does the slot-picker UI live in the editor — modal? dedicated panel? inline overlay on the 3D view?
- For boards with extra keys beyond Miryoku’s 36, do unassigned keys get
&trans(passthrough) or a configurable default? - For boards with fewer than 36 keys (e.g., a 3×4 micro), do we refuse Miryoku or auto-disable specific layers? Probably refuse with a clear message.
- Phase 2 ships Miryoku only; do we generalize the layer data model to be Miryoku-shaped (6 specific layers), or keep it open-ended for Phase 3? Lean open-ended so Phase 3 inherits cleanly.
Out of scope: general layer editor (Phase 3), tap-dance, combos.
Phase 3 — General layer editor
In scope: users build their own layered keymaps on top of the Phase 2 layer data model. The Miryoku bundle becomes “import Miryoku as a starting point, then customize.”
Anticipated work:
- Per-key, per-layer action editor (a UI surface that lists layers and lets the user click a key to change its action on a given layer).
- Layer add/remove/rename.
- Tap-dance and combo support (extends
KeyAction). - Per-key letter override UI — finally lands here, since the same editor that picks “this key is layer-tap to layer 2” also picks “this key prints
'.” - Validation: warn on unreachable layers, missing return-to-base, duplicate combos, etc.
Open questions for Phase 3:
- Do we render the per-layer view as a 2D matrix (KLE-style) or layered overlays on the 3D model? 2D is more standard, 3D is more on-brand.
- Storage cost: a 60-key board × 6 layers × non-trivial action = ~1KB+ added to the URL. Do we need a more compact encoding, or is the URL size fine?
- Do we add a firmware-side “studio mode” hook (ZMK Studio) so users can edit layers on-device, with Cosmos acting only as the initial keymap generator? Probably yes for ZMK; QMK is harder.
Branching note
Phase 1 (PR #3) is rebased onto current main (commit 660e20c). The svelte-check fixes from PR #2 went to claude-init, not main, so this PR uses the CI script (bun src/scripts/check.ts) which filters the pre-existing accepted errors. If/when the fixes land on main, Phase 1 will fast-forward cleanly.