Explore: dongle display (Prospector/OLED), Zephyr 4 line, DE/EN/programming keymap, training plan #2

Open
opened 2026-08-27 14:16:15 +02:00 by ma3s7r0 · 13 comments
Owner

Context

Current state: totem_dongle shield on xiao_ble, no display. config/west.yml tracks zmkfirmware/zmk@main, which moved to Zephyr 4.1 in Dec 2025. #1 already fixed the resulting xiao_ble//zmk board-variant break. The old totem-prospector branch is stale (only touched dtsi/kscan, predates the current dongle setup, safe to ignore/delete later).

1. Dongle display options

Module Display Zephyr 4.1 / our main line Hardware effort
carrefinho/prospector-zmk-module ST7789 240x280 color LCD main branch = ZMK v0.3/Zephyr 3.5 only — need the feat/new-status-screens branch for our line Highest — dedicated Prospector PCB (light sensor, rotary encoder) or hand-wiring a prospector_adapter shield to match its pinout
englmaxi/zmk-dongle-display I2C OLED, SSD1306 128x64 or SH1106 128x64/32 main already updated for Zephyr 4.1 Lowest — one OLED wired straight onto the xiao_ble
janpfischer/zmk-dongle-screen (YADS) ST7789V + LVGL, ambient-light auto-dim latest tagged release is Zephyr-3.5-only; need the upgrade-4.1 branch Medium — needs the LCD + light sensor, no dedicated PCB required

Decide hardware appetite first: englmaxi's is the cheap way to get a display on the current dongle as-is; Prospector is the nicer end state but means sourcing/soldering its board.

2. Zephyr 4 line

Already on it via unpinned zmk@main (Zephyr 4.1 since zmk.dev/blog/2025/12/09/zephyr-4-1, see #1). Two open questions:

  • ZMK itself recommends pinning to a release instead of riding main — no v0.4 release exists yet, so staying on main means staying in "tester" territory a while longer.
  • Whichever display module gets picked has to target the same line (branches noted above).

3. CI

.github/workflows/build-user-config.yml is a vendored/patched copy of upstream's workflow (artifact-upload v3 pin for Forgejo, see ma3s7r0/nix-config#82) — adding a shield doesn't touch it, build.yaml's matrix already drives it. What will need changes once a display module is picked:

  • config/west.yml: add the module's remote + project entry.
  • build.yaml: wire the display shield into the totem_dongle build entry (check each module's docs for exact mechanism — Prospector uses a separate prospector_adapter shield, the OLED/LVGL ones may instead be Kconfig/snippet-driven).
  • Re-check the Forgejo artifact-v3 workaround still holds once the matrix grows.

4. DE/EN/programming keymap

Current config/totem.keymap has 5 layers: BASE (QWERTY + home-row mods), NAV, SYM (already covers most programming symbols), ADJ, GER. GER is a thin AltGr passthrough (&kp RA(Q) = Ä, etc.) that only works if the host OS keyboard layout maps those AltGr combos — not documented anywhere which OS layout that assumes.

Open questions to explore:

  • Keep QWERTY base vs. switch base layout entirely (e.g. Colemak-DH/Graphite) — a much bigger relearn, treat as a separate decision, ask before committing.
  • GER as AltGr-passthrough (simple, host-layout-dependent) vs. combos/tap-dances that don't depend on OS config.
  • Audit SYM for programming gaps (pipe, tilde, backtick placement, common digraphs) now that it'd be used daily for both languages.

5. Training schedule

Deferred until the keymap decisions in #4 land — a schedule for a keymap that's still being designed gets thrown out. Once base layout is decided:

  • If base layout changes: staged keybr.com/monkettype-custom-layout drills, few short sessions/day, ramping up over ~2-3 weeks.
  • If only new layers land (GER rework, no base change): shorter drills targeting just the new layer's muscle memory, ZMK Studio for live iteration without reflashing.
## Context Current state: `totem_dongle` shield on `xiao_ble`, no display. `config/west.yml` tracks `zmkfirmware/zmk@main`, which moved to Zephyr 4.1 in Dec 2025. #1 already fixed the resulting `xiao_ble//zmk` board-variant break. The old `totem-prospector` branch is stale (only touched dtsi/kscan, predates the current dongle setup, safe to ignore/delete later). ## 1. Dongle display options | Module | Display | Zephyr 4.1 / our `main` line | Hardware effort | |---|---|---|---| | [carrefinho/prospector-zmk-module](https://github.com/carrefinho/prospector-zmk-module) | ST7789 240x280 color LCD | `main` branch = ZMK v0.3/Zephyr 3.5 only — need the `feat/new-status-screens` branch for our line | Highest — dedicated [Prospector PCB](https://github.com/carrefinho/prospector) (light sensor, rotary encoder) or hand-wiring a `prospector_adapter` shield to match its pinout | | [englmaxi/zmk-dongle-display](https://github.com/englmaxi/zmk-dongle-display) | I2C OLED, SSD1306 128x64 or SH1106 128x64/32 | `main` already updated for Zephyr 4.1 | Lowest — one OLED wired straight onto the xiao_ble | | [janpfischer/zmk-dongle-screen (YADS)](https://github.com/janpfischer/zmk-dongle-screen) | ST7789V + LVGL, ambient-light auto-dim | latest tagged release is Zephyr-3.5-only; need the `upgrade-4.1` branch | Medium — needs the LCD + light sensor, no dedicated PCB required | Decide hardware appetite first: englmaxi's is the cheap way to get *a* display on the current dongle as-is; Prospector is the nicer end state but means sourcing/soldering its board. ## 2. Zephyr 4 line Already on it via unpinned `zmk@main` (Zephyr 4.1 since [zmk.dev/blog/2025/12/09/zephyr-4-1](https://zmk.dev/blog/2025/12/09/zephyr-4-1), see #1). Two open questions: - ZMK itself recommends [pinning to a release](https://zmk.dev/blog/2025/06/20/pinned-zmk) instead of riding `main` — no v0.4 release exists yet, so staying on `main` means staying in "tester" territory a while longer. - Whichever display module gets picked has to target the same line (branches noted above). ## 3. CI `.github/workflows/build-user-config.yml` is a vendored/patched copy of upstream's workflow (artifact-upload v3 pin for Forgejo, see `ma3s7r0/nix-config#82`) — adding a shield doesn't touch it, `build.yaml`'s matrix already drives it. What will need changes once a display module is picked: - `config/west.yml`: add the module's remote + project entry. - `build.yaml`: wire the display shield into the `totem_dongle` build entry (check each module's docs for exact mechanism — Prospector uses a separate `prospector_adapter` shield, the OLED/LVGL ones may instead be Kconfig/snippet-driven). - Re-check the Forgejo artifact-v3 workaround still holds once the matrix grows. ## 4. DE/EN/programming keymap Current `config/totem.keymap` has 5 layers: BASE (QWERTY + home-row mods), NAV, SYM (already covers most programming symbols), ADJ, GER. GER is a thin AltGr passthrough (`&kp RA(Q)` = Ä, etc.) that only works if the host OS keyboard layout maps those AltGr combos — not documented anywhere which OS layout that assumes. Open questions to explore: - Keep QWERTY base vs. switch base layout entirely (e.g. Colemak-DH/Graphite) — a much bigger relearn, treat as a separate decision, ask before committing. - GER as AltGr-passthrough (simple, host-layout-dependent) vs. combos/tap-dances that don't depend on OS config. - Audit SYM for programming gaps (pipe, tilde, backtick placement, common digraphs) now that it'd be used daily for both languages. ## 5. Training schedule Deferred until the keymap decisions in #4 land — a schedule for a keymap that's still being designed gets thrown out. Once base layout is decided: - If base layout changes: staged keybr.com/monkettype-custom-layout drills, few short sessions/day, ramping up over ~2-3 weeks. - If only new layers land (GER rework, no base change): shorter drills targeting just the new layer's muscle memory, ZMK Studio for live iteration without reflashing.
Author
Owner

Research pass — findings, corrections, and a revised plan

Worked through all five sections. Several premises in the original body turned out to be wrong; those are corrected below rather than edited out. Everything here is a proposal for review, not a decision — and claims that could not be verified are marked as such.


Corrections to the premises

The Totem has never been used — and neither has the committed keymap.
The daily driver is the Sofle (ma3s7r0/zmk-sofle-eyelash-config). From its keymap and README: plain QWERTY, 58 keys with a number row, no home-row mods, no hold-taps, no combos, no macros, momentary layers (&mo). Mods sit on the bottom row and outer columns.

This reframes §4 and §5 entirely. This is not a migration — there is no Totem muscle memory to preserve. But there is also zero experience with the two things config/totem.keymap leans on hardest: home-row mods and sticky layers (&sl). The git history (try sticky layer only, try combining layer tap with sticky layers, stuff) confirms it is a series of untested experiments, not a trained setup.

§4: the GER layer's host-layout assumption is now documented — and it is proven in production.
Verified against xkeyboard-config 2.47 and 2.48 (symbols/us): us(intl) maps AltGr+q/s/y/p/5 → ä ß ü ö €, and us(altgr-intl) includes it plus level3(ralt_switch). That matches all five GER bindings exactly. The assumed host layout is us + variant altgr-intl + lv3:ralt_switch.

Not a theoretical scheme: the Sofle's layer_1 uses the identical RA(Q)/RA(S)/RA(Y)/RA(P)/RA(5) and is in daily German use.

One gap worth a look, not an alarm: in nix-config, only modules/nixos/keyboard.nix sets the variant. home-manager/niri/default.nix sets layout = "us,ua,ru" with no variant and no lv3 option, and niri does not read services.xserver.xkb. Either something else supplies it or niri isn't used for German prose. Fix belongs in the compositor config, not the keymap.

§1 missed a blocker that applies to every display option.
totem_dongle.overlay includes totem.dtsi, so the dongle declares a full GPIO matrix it has no keys for: rows &xiao_d 0,1,2,3 + cols &xiao_d 8,9,10,5,4 — every XIAO pin except D6/D7. That consumes D4/D5 (I2C) and D8/D9/D10 (SPI), i.e. both buses every display needs. It fails at runtime, not build time. Needs a mock kscan on the dongle:

/ { chosen { zmk,kscan = &mock_kscan; };
    mock_kscan: kscan_mock { compatible = "zmk,kscan-mock"; columns = <0>; rows = <0>; events = <0>; };
};

keeping zmk,physical-layout on the real transform so Studio still shows the TOTEM layout. Worth doing whether or not a display is ever added.

New gaps found in the keymap:

  • No - and no _ on any layer. Only KP_MINUS/KP_PLUS on NAV. Shift+KP_MINUS is not _, so underscore is unreachable — snake_case, kebab-case, shell flags. The Sofle's sym layer has MINUS, UNDER, PIPE, PLUS, EQUAL and all brackets, so this is a genuine regression against the incumbent.
  • The NAV numpad uses KP_N*, which is NumLock-dependent; niri does not set NumLock.
  • config/boards/shields/totem/totem.keymap is dead weight — a completely different upstream default (10 layers: Base/Media/Nav/Mouse/Sym/Num/Fun/Button/Game/Gameplus). config/totem.keymap is the live one.
  • ASCII-art comments have drifted from the bindings (BASE doesn't document &mt ESC Q, &lt GER GRAVE, or the 0+1 ESC combo; NAV shows blanks where ESC/BT_CLR/EQUAL sit).
  • The HRM hold-trigger-key-positions are correct against the 38-key position map.

Tooling gap vs. the Sofle repo: that repo has a Nix flake (nix build .#firmware, nix run .#draw) and keymap-drawer wired up. This one has neither and can only build via CI.


§1 + §2 + §3 — display: englmaxi, and CI is unaffected

Ranking for this setup:

Module Zephyr 4.1 / zmk@main Verdict
englmaxi/zmk-dongle-display main targets it (README: main with zmk main, v0.3 if pinned) Recommended
carrefinho/prospector main is Zephyr 3.5; 4.1 on WIP branch feat/new-status-screens, last traced ~Feb 2026, merge status unverified Defer
janpfischer/zmk-dongle-screen (YADS) README states latest release is not 4.1-compatible; needs upgrade-4.1 (issue #29), unverified Defer
ashin115/zmk-dongle documents pre-HWMv2 seeeduino_xiao_ble, no maintenance signal Skip

englmaxi is cheapest (~€3 SSD1306 128×64 I2C, 4 wires), lightest on flash/RAM (monochrome, no colour LVGL), and ships its own SSD1306 node on xiao_i2c — no display devicetree to write. Needs only D4/D5 freed, i.e. the mock-kscan fix. Integration: a west.yml remote+project, then shield: totem_dongle dongle_display on the dongle row.

Riding unpinned zmk@main while depending on someone's unmerged WIP branch is the specific risk that rules out the other two for now.

§3 CI: no change needed. Verified build-user-config.yml:109 passes -DSHIELD="$shield" through verbatim, so a space-separated second shield works as-is. The upload-artifact v3 pin is untouched, artifact count stays 4. Only effect: the module cache key hashes **/west.yml and **/build.yaml, so the first build after the change is a cold cache.

Useful prior art if Prospector/YADS is revisited: azumafuji/totem-yads already builds TOTEM + YADS on Zephyr 4.1.


§4 — layout: two real candidates

Since there is no Totem habit to protect, the cost of adopting an alt layout is unusually low: the ergo-board learning cost gets paid once either way, so learning QWERTY-on-38-keys now and switching later is the one option that clearly pays twice.

PUQ30 (AdNW project) — read off the driver's on-screen keyboard:

p  u  ◌  ,  q      g  c  l  m  f
h  i  e  a  o      d  t  r  n  s
k  y  .  '  x      j  v  w  b  z

= diacritics key (identified by elimination: 26 letters + , . ' = 29, leaving one slot). Name self-checks (p u … , q). Home row is identical to AdNW's h i e a o / d t r n s.

Why it is the best structural fit: designed for a straight/matrix board with thumb-shift and exactly 30 character keys, so it needs no modification. Zero wasted core slots, and , . ' are included. The diacritics slot takes &mo GER directly, so the existing AltGr scheme supplies umlauts — no host driver, no coupling.

For contrast, the ISO 4-row German layouts all need surgery. Grids extracted from xkeyboard-config symbols/de (verified against 2.47 and 2.48):

Layout wasted core slots displaced letter fit
PUQ30 0 no modification needed
Bone 3 (ä ö ü) q (~0.02% DE) clean, but modified
Noted 3 j (~0.27%) clean; only one optimized on mixed DE/EN
AdNW 3 f (~1.7% DE) frequent letter — bad
KOY 4 (incl. ß) f + z worst
Neo 2 3 y fine for DE, bad for EN and vim

Caveat: those five are 4-row ISO definitions optimized across the full board. Lifting out a 3×10 core and relocating the displaced letter yields a modified layout — the published metrics no longer apply. This caveat does not apply to PUQ30.

The alternative is Colemak-DH, on ecosystem grounds: it is the only candidate with published German-corpus analysis, and keybr/monkeytype support it natively. Modern top layouts sit within ~0.3–0.4pp SFB of each other — below perceptibility — so ecosystem may matter more than metrics.

Genuinely open:

  • No SFB/effort numbers for PUQ30 against anything, on any corpus. Now that the grid is known, dariogoetz/keyboard_layout_optimizer (German-native, multi-layer aware) can score it against a 60/40 DE/EN corpus plus a dump of own source. Roughly an afternoon.
  • What PUO30 differs in — unknown.
  • Trainer support for PUQ30 — almost certainly absent (unverified). Would mean driving ngram-type and Monkeytype off the firmware layout with emulation off, losing keybr's adaptive letter introduction.

Also: RALT must not get swallowed by a home-row mod, and the current base layer has &hmr RALT SEMI on the right pinky.


§5 — training plan (now writable)

Sequencing: adopt the target alphabet from day one. The NCSU learning-curve study found split-QWERTY typists within 10% of baseline after 20 trials, while changing the key layout was dramatically slower. Community reports: 2–3 weeks to comfort on a compact board with QWERTY vs 2–3 months to parity on a new alphabet. Learning QWERTY-on-Totem first costs weeks and trains the interference to be fought later.

With a concession that lands where the Sofle habits already are: alphabet + minimal board, not alphabet + full Miryoku.

  • No home-row mods for ~6 weeks. HRMs take 2–3 months to settle alone; stacked on an alphabet switch, every misfire is ambiguous. Mods on thumbs and outer columns — which is what the Sofle does. Then stage in via ZSA's on-ramp: shift on the index fingers only, adding outward. Stopping at shift is a legitimate endpoint.
  • &mo, not &sl. Momentary-on-thumb is the lower-cognition default and matches existing habit; sticky is for rare layers. The current keymap uses &sl NAV / &sl SYM.
  • Three layers to start (base + nav/num + sym), not five.
  • If HRMs are enabled later, urob's rule is require-prior-idle-ms ≥ 10500 / relaxed WPM260–350 ms at a beginner's 30–40 wpm, not the 150 currently in the keymap.
Phase Board time Keymap state Milestone
W1–2 30 min/day drills; Sofle for real work Alphabet + thumbs only; one nav/num layer via &mo. No HRMs, no combos. ~20–30 wpm
W3–4 Real prose moves to the Totem Add symbol layer on the other thumb ~30–40 wpm
W5–6 Totem full-time for typing Introduce German — umlauts/ß via the GER layer, drilled explicitly ~40–50 wpm
W7–10 Full-time incl. coding Shift-only HRM on index fingers; walk require-prior-idle-ms down Sofle parity ~W9–12
W11+ Remaining HRMs one pair at a time; redesign symbol layer from real friction; combos last

Two 30-min sessions beat one 60-min block. Accuracy 97–99% before pushing speed. The Sofle stays QWERTY throughout — different hardware keeps the two layouts in separate motor contexts, which is the standard arrangement and is free here.

Tools: keybr.com (weeks 1–2, adaptive letter introduction — only if the layout is supported), ngram-type + ngram-type-ger for targeted bigrams incl. German ch/sch/ei/en, Monkeytype for real words (german 10k; stop-on-error = word; layout emulator off since the layout lives in firmware), KeyZen/typing.io for symbols last.

Counter-argument, stated honestly: Getreuer holds that speed is a function of practice, not layout, and that the split columnar board, a good symbol layer, and vim each matter more than the alphabet. The RSI literature is confounded. The payoff is comfort, not wpm.


Revised plan — next steps

No-regrets, independent of every open decision:

  1. Port the Nix flake + keymap-drawer from the Sofle repo. Highest leverage — local builds and rendered keymaps before touching any layout. Currently CI-only.
  2. Fix the dongle mock kscan. Correctness fix on its own; prerequisite for any display.
  3. Add - and _; reconsider KP_N* → plain number keycodes.
  4. Delete config/boards/shields/totem/totem.keymap (confirm against a build first).
  5. Refresh the drifted ASCII-art comments — or drop them in favour of keymap-drawer output.

Decision gates:

  • §4 layout — PUQ30 vs Colemak-DH. Either run the optimizer against a real DE/EN+code corpus, or call it on ecosystem grounds. Everything in §5 depends on this.
  • §1 display — order the ~€3 OLED, or defer.
  • §2 pinning — still on unpinned zmk@main with no v0.4 release. englmaxi tracks main, so no pressure to pin; revisit if a display module forces a WIP branch.
## Research pass — findings, corrections, and a revised plan Worked through all five sections. Several premises in the original body turned out to be wrong; those are corrected below rather than edited out. Everything here is a **proposal for review**, not a decision — and claims that could not be verified are marked as such. --- ### Corrections to the premises **The Totem has never been used — and neither has the committed keymap.** The daily driver is the Sofle (`ma3s7r0/zmk-sofle-eyelash-config`). From its keymap and README: plain QWERTY, 58 keys with a number row, **no home-row mods, no hold-taps, no combos, no macros**, momentary layers (`&mo`). Mods sit on the bottom row and outer columns. This reframes §4 and §5 entirely. This is **not a migration** — there is no Totem muscle memory to preserve. But there is also zero experience with the two things `config/totem.keymap` leans on hardest: home-row mods and sticky layers (`&sl`). The git history (`try sticky layer only`, `try combining layer tap with sticky layers`, `stuff`) confirms it is a series of untested experiments, not a trained setup. **§4: the GER layer's host-layout assumption is now documented — and it is proven in production.** Verified against xkeyboard-config 2.47 and 2.48 (`symbols/us`): `us(intl)` maps AltGr+q/s/y/p/5 → ä ß ü ö €, and `us(altgr-intl)` includes it plus `level3(ralt_switch)`. That matches all five GER bindings exactly. The assumed host layout is **`us` + variant `altgr-intl` + `lv3:ralt_switch`**. Not a theoretical scheme: the Sofle's `layer_1` uses the identical `RA(Q)`/`RA(S)`/`RA(Y)`/`RA(P)`/`RA(5)` and is in daily German use. One gap worth a look, not an alarm: in `nix-config`, only `modules/nixos/keyboard.nix` sets the variant. `home-manager/niri/default.nix` sets `layout = "us,ua,ru"` with no variant and no `lv3` option, and niri does not read `services.xserver.xkb`. Either something else supplies it or niri isn't used for German prose. Fix belongs in the compositor config, not the keymap. **§1 missed a blocker that applies to every display option.** `totem_dongle.overlay` includes `totem.dtsi`, so the **dongle declares a full GPIO matrix it has no keys for**: rows `&xiao_d 0,1,2,3` + cols `&xiao_d 8,9,10,5,4` — every XIAO pin except D6/D7. That consumes **D4/D5 (I2C)** and **D8/D9/D10 (SPI)**, i.e. both buses every display needs. It fails at *runtime*, not build time. Needs a mock kscan on the dongle: ```dts / { chosen { zmk,kscan = &mock_kscan; }; mock_kscan: kscan_mock { compatible = "zmk,kscan-mock"; columns = <0>; rows = <0>; events = <0>; }; }; ``` keeping `zmk,physical-layout` on the real transform so Studio still shows the TOTEM layout. Worth doing whether or not a display is ever added. **New gaps found in the keymap:** - **No `-` and no `_` on any layer.** Only `KP_MINUS`/`KP_PLUS` on NAV. `Shift+KP_MINUS` is not `_`, so underscore is unreachable — snake_case, kebab-case, shell flags. The Sofle's `sym` layer has `MINUS`, `UNDER`, `PIPE`, `PLUS`, `EQUAL` and all brackets, so this is a genuine regression against the incumbent. - The NAV numpad uses `KP_N*`, which is NumLock-dependent; niri does not set NumLock. - `config/boards/shields/totem/totem.keymap` is **dead weight** — a completely different upstream default (10 layers: Base/Media/Nav/Mouse/Sym/Num/Fun/Button/Game/Gameplus). `config/totem.keymap` is the live one. - ASCII-art comments have drifted from the bindings (BASE doesn't document `&mt ESC Q`, `&lt GER GRAVE`, or the 0+1 ESC combo; NAV shows blanks where `ESC`/`BT_CLR`/`EQUAL` sit). - The HRM `hold-trigger-key-positions` **are** correct against the 38-key position map. **Tooling gap vs. the Sofle repo:** that repo has a Nix flake (`nix build .#firmware`, `nix run .#draw`) and keymap-drawer wired up. This one has neither and can only build via CI. --- ### §1 + §2 + §3 — display: englmaxi, and CI is unaffected Ranking for this setup: | Module | Zephyr 4.1 / `zmk@main` | Verdict | |---|---|---| | [englmaxi/zmk-dongle-display](https://github.com/englmaxi/zmk-dongle-display) | `main` targets it (README: `main` with zmk main, `v0.3` if pinned) | **Recommended** | | [carrefinho/prospector](https://github.com/carrefinho/prospector-zmk-module) | `main` is Zephyr 3.5; 4.1 on WIP branch `feat/new-status-screens`, last traced ~Feb 2026, **merge status unverified** | Defer | | [janpfischer/zmk-dongle-screen](https://github.com/janpfischer/zmk-dongle-screen) (YADS) | README states latest release is **not** 4.1-compatible; needs `upgrade-4.1` (issue #29), **unverified** | Defer | | [ashin115/zmk-dongle](https://github.com/ashin115/zmk-dongle) | documents pre-HWMv2 `seeeduino_xiao_ble`, no maintenance signal | Skip | englmaxi is cheapest (~€3 SSD1306 128×64 I2C, 4 wires), lightest on flash/RAM (monochrome, no colour LVGL), and **ships its own SSD1306 node on `xiao_i2c`** — no display devicetree to write. Needs only D4/D5 freed, i.e. the mock-kscan fix. Integration: a `west.yml` remote+project, then `shield: totem_dongle dongle_display` on the dongle row. Riding unpinned `zmk@main` while depending on someone's unmerged WIP branch is the specific risk that rules out the other two for now. **§3 CI: no change needed.** Verified `build-user-config.yml:109` passes `-DSHIELD="$shield"` through verbatim, so a space-separated second shield works as-is. The upload-artifact v3 pin is untouched, artifact count stays 4. Only effect: the module cache key hashes `**/west.yml` and `**/build.yaml`, so the first build after the change is a cold cache. Useful prior art if Prospector/YADS is revisited: **azumafuji/totem-yads** already builds TOTEM + YADS on Zephyr 4.1. --- ### §4 — layout: two real candidates Since there is no Totem habit to protect, the cost of adopting an alt layout is unusually low: the ergo-board learning cost gets paid once either way, so learning QWERTY-on-38-keys now and switching later is the one option that clearly **pays twice**. **PUQ30** ([AdNW project](http://www.adnw.de/index.php?n=Main.OptimierungF%C3%BCrDieGeradeTastaturMitDaumen-Shift)) — read off the driver's on-screen keyboard: ``` p u ◌ , q g c l m f h i e a o d t r n s k y . ' x j v w b z ``` `◌` = diacritics key (identified by elimination: 26 letters + `,` `.` `'` = 29, leaving one slot). Name self-checks (`p u … , q`). Home row is **identical to AdNW's** `h i e a o / d t r n s`. Why it is the best structural fit: designed for a **straight/matrix board with thumb-shift and exactly 30 character keys**, so it needs no modification. **Zero wasted core slots**, and `,` `.` `'` are included. The diacritics slot takes `&mo GER` directly, so the existing AltGr scheme supplies umlauts — no host driver, no coupling. For contrast, the ISO 4-row German layouts all need surgery. Grids extracted from xkeyboard-config `symbols/de` (verified against 2.47 and 2.48): | Layout | wasted core slots | displaced letter | fit | |---|---|---|---| | **PUQ30** | **0** | — | no modification needed | | Bone | 3 (ä ö ü) | `q` (~0.02% DE) | clean, but modified | | Noted | 3 | `j` (~0.27%) | clean; only one optimized on mixed DE/EN | | AdNW | 3 | `f` (~1.7% DE) | frequent letter — bad | | KOY | 4 (incl. ß) | `f` + `z` | worst | | Neo 2 | 3 | `y` | fine for DE, bad for EN and vim | **Caveat:** those five are 4-row ISO definitions optimized across the full board. Lifting out a 3×10 core and relocating the displaced letter yields a *modified* layout — **the published metrics no longer apply.** This caveat does not apply to PUQ30. **The alternative is Colemak-DH**, on ecosystem grounds: it is the only candidate with published German-corpus analysis, and keybr/monkeytype support it natively. Modern top layouts sit within ~0.3–0.4pp SFB of each other — below perceptibility — so ecosystem may matter more than metrics. Genuinely open: - No SFB/effort numbers for PUQ30 against anything, on any corpus. Now that the grid is known, [dariogoetz/keyboard_layout_optimizer](https://github.com/dariogoetz/keyboard_layout_optimizer) (German-native, multi-layer aware) can score it against a 60/40 DE/EN corpus plus a dump of own source. Roughly an afternoon. - What PUO30 differs in — unknown. - **Trainer support for PUQ30 — almost certainly absent** (unverified). Would mean driving [ngram-type](https://github.com/ranelpadon/ngram-type) and Monkeytype off the firmware layout with emulation off, losing keybr's adaptive letter introduction. Also: `RALT` must not get swallowed by a home-row mod, and the current base layer has `&hmr RALT SEMI` on the right pinky. --- ### §5 — training plan (now writable) **Sequencing: adopt the target alphabet from day one.** The [NCSU learning-curve study](https://repository.lib.ncsu.edu/items/4a7a11aa-30be-40dd-bd9f-88ba6ef44445) found split-QWERTY typists within 10% of baseline after 20 trials, while changing the *key layout* was dramatically slower. Community reports: 2–3 weeks to comfort on a compact board with QWERTY vs 2–3 months to parity on a new alphabet. Learning QWERTY-on-Totem first costs weeks and trains the interference to be fought later. **With a concession that lands where the Sofle habits already are: alphabet + *minimal* board, not alphabet + full Miryoku.** - **No home-row mods for ~6 weeks.** HRMs take 2–3 months to settle alone; stacked on an alphabet switch, every misfire is ambiguous. Mods on thumbs and outer columns — which is what the Sofle does. Then stage in via [ZSA's on-ramp](https://blog.zsa.io/layout-buffet-home-row-mods/): shift on the index fingers only, adding outward. Stopping at shift is a legitimate endpoint. - **`&mo`, not `&sl`.** Momentary-on-thumb is the lower-cognition default and matches existing habit; sticky is for *rare* layers. The current keymap uses `&sl NAV` / `&sl SYM`. - **Three layers to start** (base + nav/num + sym), not five. - If HRMs are enabled later, urob's rule is `require-prior-idle-ms ≥ 10500 / relaxed WPM` → **260–350 ms** at a beginner's 30–40 wpm, not the 150 currently in the keymap. | Phase | Board time | Keymap state | Milestone | |---|---|---|---| | W1–2 | 30 min/day drills; Sofle for real work | Alphabet + thumbs only; one nav/num layer via `&mo`. No HRMs, no combos. | ~20–30 wpm | | W3–4 | Real prose moves to the Totem | Add symbol layer on the other thumb | ~30–40 wpm | | W5–6 | Totem full-time for typing | Introduce German — umlauts/ß via the GER layer, drilled explicitly | ~40–50 wpm | | W7–10 | Full-time incl. coding | Shift-only HRM on index fingers; walk `require-prior-idle-ms` down | Sofle parity ~W9–12 | | W11+ | — | Remaining HRMs one pair at a time; redesign symbol layer from real friction; combos last | — | Two 30-min sessions beat one 60-min block. Accuracy 97–99% before pushing speed. **The Sofle stays QWERTY throughout** — different hardware keeps the two layouts in separate motor contexts, which is the standard arrangement and is free here. Tools: keybr.com (weeks 1–2, adaptive letter introduction — **only if the layout is supported**), [ngram-type](https://github.com/ranelpadon/ngram-type) + [ngram-type-ger](https://github.com/ghaarm/ngram-type-ger) for targeted bigrams incl. German `ch`/`sch`/`ei`/`en`, Monkeytype for real words (`german 10k`; stop-on-error = `word`; layout emulator **off** since the layout lives in firmware), KeyZen/typing.io for symbols last. **Counter-argument, stated honestly:** [Getreuer](https://getreuer.info/posts/keyboards/alt-layouts/index.html) holds that speed is a function of practice, not layout, and that the split columnar board, a good symbol layer, and vim each matter more than the alphabet. The RSI literature is confounded. The payoff is comfort, not wpm. --- ### Revised plan — next steps **No-regrets, independent of every open decision:** 1. **Port the Nix flake + keymap-drawer from the Sofle repo.** Highest leverage — local builds and rendered keymaps before touching any layout. Currently CI-only. 2. **Fix the dongle mock kscan.** Correctness fix on its own; prerequisite for any display. 3. **Add `-` and `_`**; reconsider `KP_N*` → plain number keycodes. 4. **Delete `config/boards/shields/totem/totem.keymap`** (confirm against a build first). 5. Refresh the drifted ASCII-art comments — or drop them in favour of keymap-drawer output. **Decision gates:** - **§4 layout — PUQ30 vs Colemak-DH.** Either run the optimizer against a real DE/EN+code corpus, or call it on ecosystem grounds. Everything in §5 depends on this. - **§1 display — order the ~€3 OLED, or defer.** - **§2 pinning** — still on unpinned `zmk@main` with no v0.4 release. englmaxi tracks `main`, so no pressure to pin; revisit if a display module forces a WIP branch.
Author
Owner

§4 update — the Kissboard 30-key layouts, with grids and metrics

Source: PUQ-Vars documentation (Modifizierter NeoVars für Truly Ergonomic Keyboard, rev. 254 / 01.16), pages 1 and 3, supplied from the PDF. This supersedes the "genuinely open" items in the previous comment.

There are three, not one

All computed with Andreas Wettstein's optimizer — the same tool and objective family as AdNW and KOY, so these are properly optimized layouts, not hand-tweaks. Doc's own framing: "Umlautfreie Belegungen für 30 Zeichentasten … Bi- und Trigrammoptimierung, getrennte Silben. Deutsch-Englisch 1:1."

That last part is the decisive property: optimized on a 1:1 German/English corpus. No English-optimized layout (Colemak-DH, Gallium, Graphite) offers this, and the earlier note that no German-corpus scoring exists for them still stands.

PUQ30            PUO30            OUMF30
p u ¨ , q        p u o . ¨        ¨ o u m f
h i e a o        h i a e ,        i a e s g
k y . ' x        k y x ' q        y x ' , q

g c l m f        g c l m f        v w c l k
d t r n s        d t r n s        d t n r h
j v w b z        j v w b z        j p . z b

Each is 26 letters + the diacritics key ¨ + , . ' = exactly 30. PUQ30 and PUO30 share an identical right hand. The PUQ30 grid here matches the independent reading from the driver's on-screen keyboard in the previous comment.

Metrics (Wettstein model — lower is better)

Gesamtaufwand EN DE combined Lageaufwand EN DE Kollisionen EN DE benachbart EN DE Handwechsel EN
PUQ30 197.2 185.4 382.5 167.0 157.1 0.86 0.36 7.7 11.8 72.4
PUO30 200.2 182.2 382.3 168.1 158.8 0.88 0.36 9.9 6.9 72.4
OUMF30 205.2 194.7 399.9 165.7 156.3 1.05 0.76 13.9 13.4 64.6

PUQ30 is the pick for balanced DE+EN. The doc states it: PUO30 suits German better (e and i not adjacent — h i a e , vs PUQ30's h i e a o) but is "deutlich schlechter in Englisch"; PUQ30's German/English ratio is balanced and "Der Lageaufwand ist bei PUQ in jedem Fall besser", which the table confirms in both languages. Combined total effort is a tie (382.5 vs 382.3, 0.05%), so position effort breaks it.

OUMF30 trades hand alternation for same-hand runs (Handwechsel 64.6 vs 72.4) at ~4.5% worse total effort and the highest adjacent-finger load. Only for a deliberate roll preference.

PUQ30 row distribution: ~70% home row (EN), ~67% (DE), bottom row under 9%.

Comparison caveat: these figures are not comparable to Colemak-DH's ~1.39% SFB or Gallium's ~0.6–1.0%. Wettstein's model measures its own quantities (Gesamtaufwand / Lageaufwand / Kollisionen), not oxeylyzer-style SFB. PUQ30's Kollisionen of 0.86 EN / 0.36 DE looks Gallium-class if the unit is percent-of-bigrams, but the unit is unverified — no cross-model claim should be made either way.

The diacritics key: postfix, and host-side

Mechanism, now confirmed: type the letter first, then the keya☠→ä, s☠→ß, c☠→ç. Repeated presses cycle diacritics (i☠☠☠→î) with per-language ordering menus; Shift turns it into a Compose key (AE♫→Æ, oe♫→œ, tm♫→™). Its position was co-optimized (~1.7% of German characters, comparable to f), not parked in a leftover slot.

It is implemented host-side in AutoHotKey (PUQ-Vars is a NeoVars fork, GPLv3/CC-BY-SA) — Windows only, therefore irrelevant here; the layout would be implemented in ZMK regardless.

ZMK cannot do postfix natively (it would need to know what was just typed). Two substitutes:

  1. Put &mo GER / &lt GER in the ¨ slot and keep the existing AltGr passthrough. Same key, same finger, prefix-hold instead of postfix-tap. Recommended — it is what the Sofle already does.
  2. Combos: letter position + ¨ position → RA(Q). Same two keys as upstream but simultaneous rather than sequential. Adds timing ambiguity, so a later refinement, consistent with the "combos last" note in §5.

Either way the base alpha arrangement is untouched — the coupling question raised earlier is closed: there is none.

Totem fit

Concept is "Kissboard": 30 character keys plus mod keys, dropping the TECK's two outermost keys, with the freed pair holding ' and the diacritics key.

The Totem has 30 keys in the 3×5+3×5 block, plus 2 outer bottom-row keys, plus 6 thumbs. So the core maps exactly onto the alpha block and both outer keys are spare — currently &lt GER GRAVE and &kp SINGLE_QUOTE, and since ' moves in-core, genuinely free (grave, a layer key, or whatever the symbol layer wants).

Revised §4 decision

The layout choice is now PUQ30 vs Colemak-DH, and the trade is no longer metrics-vs-structure — PUQ30 has real optimizer numbers on a 1:1 DE/EN corpus, which is strictly better evidence for this use case than anything available for the English layouts. The remaining gap is purely ecosystem:

  • Colemak-DH: native keybr/monkeytype support, published German analysis, large community, off-the-shelf vim remaps.
  • PUQ30: better-matched optimization and geometry, 2 spare keys, umlaut mechanism already in production on the Sofle — but no trainer support (unverified, likely none), meaning ngram-type + Monkeytype driven off the firmware layout with emulation off, losing keybr's adaptive letter introduction.

Running dariogoetz's optimizer is no longer needed to decide — PUQ30's numbers exist. It would only be useful to score PUQ30 against Colemak-DH in a single model, which remains the one genuinely unanswered comparison.

Also still open: whether OUMF30/PUO30 are worth a second look if the roll/alternation preference turns out to differ from assumption. Neither changes the near-term plan.

No change to the next steps in the previous comment — the Nix flake port remains first and is independent of this decision.

## §4 update — the Kissboard 30-key layouts, with grids and metrics Source: **PUQ-Vars documentation** (Modifizierter NeoVars für Truly Ergonomic Keyboard, rev. 254 / 01.16), pages 1 and 3, supplied from the PDF. This supersedes the "genuinely open" items in the previous comment. ### There are three, not one All computed with **Andreas Wettstein's optimizer** — the same tool and objective family as AdNW and KOY, so these are properly optimized layouts, not hand-tweaks. Doc's own framing: *"Umlautfreie Belegungen für 30 Zeichentasten … Bi- und Trigrammoptimierung, getrennte Silben. **Deutsch-Englisch 1:1**."* That last part is the decisive property: **optimized on a 1:1 German/English corpus.** No English-optimized layout (Colemak-DH, Gallium, Graphite) offers this, and the earlier note that no German-corpus scoring exists for them still stands. ``` PUQ30 PUO30 OUMF30 p u ¨ , q p u o . ¨ ¨ o u m f h i e a o h i a e , i a e s g k y . ' x k y x ' q y x ' , q g c l m f g c l m f v w c l k d t r n s d t r n s d t n r h j v w b z j v w b z j p . z b ``` Each is 26 letters + the diacritics key `¨` + `,` `.` `'` = exactly 30. **PUQ30 and PUO30 share an identical right hand.** The PUQ30 grid here matches the independent reading from the driver's on-screen keyboard in the previous comment. ### Metrics (Wettstein model — lower is better) | | Gesamtaufwand EN | DE | **combined** | Lageaufwand EN | DE | Kollisionen EN | DE | benachbart EN | DE | Handwechsel EN | |---|---|---|---|---|---|---|---|---|---|---| | **PUQ30** | 197.2 | 185.4 | **382.5** | **167.0** | **157.1** | 0.86 | 0.36 | **7.7** | 11.8 | 72.4 | | PUO30 | 200.2 | **182.2** | 382.3 | 168.1 | 158.8 | 0.88 | 0.36 | 9.9 | **6.9** | 72.4 | | OUMF30 | 205.2 | 194.7 | 399.9 | 165.7 | 156.3 | 1.05 | 0.76 | 13.9 | 13.4 | 64.6 | **PUQ30 is the pick for balanced DE+EN.** The doc states it: PUO30 suits German better (`e` and `i` not adjacent — `h i a e ,` vs PUQ30's `h i e a o`) but is *"deutlich schlechter in Englisch"*; PUQ30's German/English ratio is balanced and *"Der Lageaufwand ist bei PUQ in jedem Fall besser"*, which the table confirms in both languages. Combined total effort is a tie (382.5 vs 382.3, 0.05%), so position effort breaks it. OUMF30 trades hand alternation for same-hand runs (Handwechsel 64.6 vs 72.4) at ~4.5% worse total effort and the highest adjacent-finger load. Only for a deliberate roll preference. PUQ30 row distribution: **~70% home row (EN), ~67% (DE)**, bottom row under 9%. **Comparison caveat:** these figures are **not** comparable to Colemak-DH's ~1.39% SFB or Gallium's ~0.6–1.0%. Wettstein's model measures its own quantities (Gesamtaufwand / Lageaufwand / Kollisionen), not oxeylyzer-style SFB. PUQ30's Kollisionen of 0.86 EN / 0.36 DE *looks* Gallium-class if the unit is percent-of-bigrams, but the unit is **unverified** — no cross-model claim should be made either way. ### The diacritics key: postfix, and host-side Mechanism, now confirmed: **type the letter first, then the key** — `a☠`→ä, `s☠`→ß, `c☠`→ç. Repeated presses cycle diacritics (`i☠☠☠`→î) with per-language ordering menus; Shift turns it into a Compose key (`AE♫`→Æ, `oe♫`→œ, `tm♫`→™). Its position was **co-optimized** (~1.7% of German characters, comparable to `f`), not parked in a leftover slot. It is implemented host-side in **AutoHotKey** (PUQ-Vars is a NeoVars fork, GPLv3/CC-BY-SA) — **Windows only**, therefore irrelevant here; the layout would be implemented in ZMK regardless. ZMK cannot do postfix natively (it would need to know what was just typed). Two substitutes: 1. **Put `&mo GER` / `&lt GER` in the `¨` slot** and keep the existing AltGr passthrough. Same key, same finger, prefix-hold instead of postfix-tap. Recommended — it is what the Sofle already does. 2. **Combos**: letter position + `¨` position → `RA(Q)`. Same two keys as upstream but simultaneous rather than sequential. Adds timing ambiguity, so a later refinement, consistent with the "combos last" note in §5. Either way the base alpha arrangement is untouched — the coupling question raised earlier is closed: **there is none.** ### Totem fit Concept is *"Kissboard"*: 30 character keys plus mod keys, dropping the TECK's two outermost keys, with the freed pair holding `'` and the diacritics key. The Totem has 30 keys in the 3×5+3×5 block, **plus** 2 outer bottom-row keys, **plus** 6 thumbs. So the core maps exactly onto the alpha block and **both outer keys are spare** — currently `&lt GER GRAVE` and `&kp SINGLE_QUOTE`, and since `'` moves in-core, genuinely free (grave, a layer key, or whatever the symbol layer wants). ### Revised §4 decision The layout choice is now **PUQ30 vs Colemak-DH**, and the trade is no longer metrics-vs-structure — PUQ30 has real optimizer numbers on a 1:1 DE/EN corpus, which is strictly better evidence for this use case than anything available for the English layouts. The remaining gap is **purely ecosystem**: - Colemak-DH: native keybr/monkeytype support, published German analysis, large community, off-the-shelf vim remaps. - PUQ30: better-matched optimization and geometry, 2 spare keys, umlaut mechanism already in production on the Sofle — but **no trainer support** (unverified, likely none), meaning ngram-type + Monkeytype driven off the firmware layout with emulation off, losing keybr's adaptive letter introduction. Running dariogoetz's optimizer is **no longer needed to decide** — PUQ30's numbers exist. It would only be useful to score PUQ30 against Colemak-DH in a *single* model, which remains the one genuinely unanswered comparison. Also still open: whether OUMF30/PUO30 are worth a second look if the roll/alternation preference turns out to differ from assumption. Neither changes the near-term plan. **No change to the next steps in the previous comment** — the Nix flake port remains first and is independent of this decision.
Author
Owner

Prep for step 1 — porting the Nix flake + keymap-drawer from the Sofle

Groundwork so this can be picked up cold. Source: ma3s7r0/zmk-sofle-eyelash-config (branch main; display-tweaks also touches flake.nix and build.sh, so diff both before copying — it adds a patches/ directory applied to the display modules, which this repo does not need).

What exists there

file role
flake.nix (149 lines) the whole build — packages, devShell, draw app
flake.lock pins nixpkgs-unstable + zmk-nix
keymap_drawer.config.yaml drawer styling (fonts, key sizes, held-key colours)
keymap-drawer/eyelash_sofle.{yaml,svg} committed drawer output
build.sh (78 lines) wrapper
glyphs.sh (56 lines) glyph handling for the drawer
.github/workflows/draw.yml present but deliberately disabled on Forgejo
zephyr/module.yml makes that repo a Zephyr module — not needed here, see below

It builds on lilyinstarlight/zmk-nix via zmk.buildKeyboard, exposing firmware, keymap, flash, update, default, a devShells.default, and an apps.draw.

The one real risk: zmk-nix vs xiao_ble//zmk on Zephyr 4.1

The Sofle flake hardcodes board = "nice_nano_v2" and its config/west.yml pins ZMK to v0.3.0 — deliberately, because that repo's own board definitions do not survive Zephyr 4.1 / hardware-model-v2.

This repo is the opposite case: unpinned zmk@main, i.e. on the 4.1 line, and build.yaml uses the board qualifier xiao_ble//zmk (see #1). So the open question, and the thing to settle first:

Does zmk-nix's buildKeyboard accept a qualified board name (xiao_ble//zmk) and work against ZMK main/Zephyr 4.1?

If it does not, the options are: patch/override zmk-nix, or pin ZMK here too — the latter conflicts with the current display plan, since englmaxi's main targets the 4.1 line. Resolve this before writing any of the rest; it determines whether the port is an afternoon or a project.

Other adaptations needed

  • zephyrDepsHash — a fixed-output hash of the west dependency tree. It will differ (different west.yml, different ZMK revision). Regenerate with nix run .#update; the Sofle's value is meaningless here.
  • Targets — replace the four Sofle targets with this repo's, mirroring build.yaml: totem_left, totem_right, totem_dongle, settings_reset, all on xiao_ble//zmk. Keep the flake's "Mirrors build.yaml. Keep the two in sync." comment honest.
  • Studio on three of four targets. Here totem_left, totem_right and totem_dongle all pass -DCONFIG_ZMK_STUDIO=y (only settings_reset does not), versus one of four on the Sofle. So the flake's nanopb / pkg_resources workaround will apply to three targets — that postConfigure sed exists because nanopb's generator imports pkg_resources, which setuptools dropped in 81 while nixpkgs is on 83. Carry it over; it is not optional.
  • Studio locking differs. The Sofle passes -DCONFIG_ZMK_STUDIO_LOCKING=n (unlocked). This repo does notconfig/totem.keymap has a real &studio_unlock binding on the ADJ layer. So drop extraCmakeFlags and leave locking at its default.
  • The dongle needs snippet: studio-rpc-usb-uart. The Sofle flake never passes a snippet — it relies on enableZmkStudio. Check whether enableZmkStudio already implies the snippet, or whether it must be passed explicitly for the dongle target.
  • keymap-drawer path differs. The Sofle passes -d boards/shields/eyelash_sofle/eyelash_sofle-layouts.dtsi. There is no separate -layouts.dtsi here — the physical layout (default_layout / key_physical_attrs) lives in config/boards/shields/totem/totem.dtsi. Point -d there. Same rationale applies: reading the layout from devicetree avoids the network lookup a zmk_keyboard: key would trigger.
  • Drawer input is config/totem.keymap, output keymap-drawer/totem.{yaml,svg}.
  • No zephyr/module.yml needed. The Sofle keeps its shield at the repo root (boards/shields/eyelash_sofle/) and needs the module manifest for ZMK to find it. This repo keeps its shield inside config/ (config/boards/shields/totem/), where ZMK_CONFIG already picks it up. Simpler — do not copy that file.
  • sourceFilesBySuffices — the Sofle filters .conf .defconfig .dtsi .h .keymap .overlay .shield .yaml .yml. That set covers everything here; config/info.json is drawer/QMK metadata and is not part of the ZMK build, so .json need not be added unless the drawer is pointed at it.

Deliberately skip

.github/workflows/draw.yml — disabled upstream for a Forgejo reason that applies identically here: caksoylar/keymap-drawer's draw-zmk.yml line 112 is continue-on-error: ${{ !inputs.fail_on_error }}, and Forgejo types continue-on-error as a strict bool and rejects the expression. Valid on GitHub, fatal on git.cl12.de, not fixable from the calling file. Regenerate the drawing locally via nix run .#draw and commit the output, as the Sofle does.

Note also that the Sofle README states its GitHub workflows do not run on Forgejo at all and local builds are the supported path — whereas this repo's build.yml + vendored build-user-config.yml do work (that was #1's whole point). So the flake here is additive: local iteration alongside working CI, not a replacement for it.

Verification

  1. nix build .#firmware → four UF2s named per build.yaml.
  2. nix run .#drawkeymap-drawer/totem.{yaml,svg}, and the SVG should show &mt ESC Q, &lt GER GRAVE and the 0+1 ESC combo that the drifted ASCII art omits — which is the point of steps 4/5 in the plan.
  3. nix develop → toolchain shell.
  4. Confirm CI still passes, since build.yaml is untouched.

Suggested branch

Off totem-dongle. Keep the flake port as its own change, separate from the mock-kscan fix and the -/_ addition — the flake is what makes those two verifiable locally, so it should land first and alone.

## Prep for step 1 — porting the Nix flake + keymap-drawer from the Sofle Groundwork so this can be picked up cold. Source: `ma3s7r0/zmk-sofle-eyelash-config` (branch `main`; `display-tweaks` also touches `flake.nix` and `build.sh`, so **diff both before copying** — it adds a `patches/` directory applied to the display modules, which this repo does not need). ### What exists there | file | role | |---|---| | `flake.nix` (149 lines) | the whole build — packages, devShell, `draw` app | | `flake.lock` | pins `nixpkgs-unstable` + `zmk-nix` | | `keymap_drawer.config.yaml` | drawer styling (fonts, key sizes, held-key colours) | | `keymap-drawer/eyelash_sofle.{yaml,svg}` | committed drawer output | | `build.sh` (78 lines) | wrapper | | `glyphs.sh` (56 lines) | glyph handling for the drawer | | `.github/workflows/draw.yml` | present but **deliberately disabled** on Forgejo | | `zephyr/module.yml` | makes that repo a Zephyr module — **not needed here**, see below | It builds on [`lilyinstarlight/zmk-nix`](https://github.com/lilyinstarlight/zmk-nix) via `zmk.buildKeyboard`, exposing `firmware`, `keymap`, `flash`, `update`, `default`, a `devShells.default`, and an `apps.draw`. ### The one real risk: `zmk-nix` vs `xiao_ble//zmk` on Zephyr 4.1 The Sofle flake hardcodes `board = "nice_nano_v2"` and its `config/west.yml` **pins ZMK to `v0.3.0`** — deliberately, because that repo's own board definitions do not survive Zephyr 4.1 / hardware-model-v2. This repo is the opposite case: unpinned `zmk@main`, i.e. **on** the 4.1 line, and `build.yaml` uses the **board qualifier** `xiao_ble//zmk` (see #1). So the open question, and the thing to settle first: > **Does `zmk-nix`'s `buildKeyboard` accept a qualified board name (`xiao_ble//zmk`) and work against ZMK `main`/Zephyr 4.1?** If it does not, the options are: patch/override `zmk-nix`, or pin ZMK here too — the latter conflicts with the current display plan, since englmaxi's `main` targets the 4.1 line. **Resolve this before writing any of the rest**; it determines whether the port is an afternoon or a project. ### Other adaptations needed - **`zephyrDepsHash`** — a fixed-output hash of the west dependency tree. It *will* differ (different `west.yml`, different ZMK revision). Regenerate with `nix run .#update`; the Sofle's value is meaningless here. - **Targets** — replace the four Sofle targets with this repo's, mirroring `build.yaml`: `totem_left`, `totem_right`, `totem_dongle`, `settings_reset`, all on `xiao_ble//zmk`. Keep the flake's "Mirrors build.yaml. Keep the two in sync." comment honest. - **Studio on three of four targets.** Here `totem_left`, `totem_right` and `totem_dongle` all pass `-DCONFIG_ZMK_STUDIO=y` (only `settings_reset` does not), versus one of four on the Sofle. So the flake's **nanopb / `pkg_resources` workaround will apply to three targets** — that `postConfigure` sed exists because nanopb's generator imports `pkg_resources`, which setuptools dropped in 81 while nixpkgs is on 83. Carry it over; it is not optional. - **Studio locking differs.** The Sofle passes `-DCONFIG_ZMK_STUDIO_LOCKING=n` (unlocked). This repo does **not** — `config/totem.keymap` has a real `&studio_unlock` binding on the ADJ layer. So drop `extraCmakeFlags` and leave locking at its default. - **The dongle needs `snippet: studio-rpc-usb-uart`.** The Sofle flake never passes a snippet — it relies on `enableZmkStudio`. **Check whether `enableZmkStudio` already implies the snippet**, or whether it must be passed explicitly for the dongle target. - **keymap-drawer path differs.** The Sofle passes `-d boards/shields/eyelash_sofle/eyelash_sofle-layouts.dtsi`. There is no separate `-layouts.dtsi` here — the physical layout (`default_layout` / `key_physical_attrs`) lives in **`config/boards/shields/totem/totem.dtsi`**. Point `-d` there. Same rationale applies: reading the layout from devicetree avoids the network lookup a `zmk_keyboard:` key would trigger. - **Drawer input** is `config/totem.keymap`, output `keymap-drawer/totem.{yaml,svg}`. - **No `zephyr/module.yml` needed.** The Sofle keeps its shield at the repo root (`boards/shields/eyelash_sofle/`) and needs the module manifest for ZMK to find it. This repo keeps its shield **inside** `config/` (`config/boards/shields/totem/`), where `ZMK_CONFIG` already picks it up. Simpler — do not copy that file. - **`sourceFilesBySuffices`** — the Sofle filters `.conf .defconfig .dtsi .h .keymap .overlay .shield .yaml .yml`. That set covers everything here; `config/info.json` is drawer/QMK metadata and is not part of the ZMK build, so `.json` need not be added unless the drawer is pointed at it. ### Deliberately skip `.github/workflows/draw.yml` — disabled upstream for a Forgejo reason that applies identically here: `caksoylar/keymap-drawer`'s `draw-zmk.yml` line 112 is `continue-on-error: ${{ !inputs.fail_on_error }}`, and Forgejo types `continue-on-error` as a strict bool and rejects the expression. Valid on GitHub, fatal on git.cl12.de, not fixable from the calling file. **Regenerate the drawing locally via `nix run .#draw` and commit the output**, as the Sofle does. Note also that the Sofle README states its GitHub workflows do not run on Forgejo at all and local builds are the supported path — whereas **this** repo's `build.yml` + vendored `build-user-config.yml` *do* work (that was #1's whole point). So the flake here is **additive**: local iteration alongside working CI, not a replacement for it. ### Verification 1. `nix build .#firmware` → four UF2s named per `build.yaml`. 2. `nix run .#draw` → `keymap-drawer/totem.{yaml,svg}`, and the SVG should show `&mt ESC Q`, `&lt GER GRAVE` and the 0+1 ESC combo that the drifted ASCII art omits — which is the point of steps 4/5 in the plan. 3. `nix develop` → toolchain shell. 4. Confirm CI still passes, since `build.yaml` is untouched. ### Suggested branch Off `totem-dongle`. Keep the flake port as its own change, separate from the mock-kscan fix and the `-`/`_` addition — the flake is what makes those two verifiable locally, so it should land first and alone.
Author
Owner

Steps 1 and 2 done, and §1 reverses: Prospector is the pick, not englmaxi

Branch nix-flake off totem-dongle, four commits, not pushed. Hardware fact that changes the plan: the Prospector board is already owned. Home = Prospector, travel = bare dongle, so both dongle images are wanted, not one.

§1 correction — the reason to defer Prospector has expired

The previous comment deferred it on "merge status unverified" for feat/new-status-screens, last traced ~Feb 2026. Checked the branch directly. Its HEAD is ed98221f (2026-02-17), "update readme, use new xiao_ble//zmk board target" — it targets exactly this repo's line. The README's install instructions now literally say:

  - board: xiao_ble//zmk
    shield: [YOUR KEYBOARD SHIELD]_dongle prospector_adapter

It also ships boards/shields/prospector_adapter/boards/xiao_ble_zmk.overlay, i.e. a board overlay written for the ZMK variant. Not a WIP that happens to compile — it was ported to this target on purpose.

Prior art found for the exact combination: eigatech/zmk-config@totem-prospector, last touched 2026-04-20 ("update board variant"). Same TOTEM shield tree as this repo, same board qualifier, same module branch. The structure below follows it.

Checked GEIGEIGEIST/zmk-config-totem for comparison: upstream has no dongle at all, so there is nothing newer to take from there. The stale totem-prospector@origin branch in this repo is an older copy of eigatech's, safe to delete as the original body assumed.

The pin conflict is worse than reported, and it is a bug today

Previously flagged as D4/D5 + D8/D9/D10. The real overlap, resolved through seeed_xiao_connector.dtsi (D0=P0.02, D1=P0.03, D2=P0.28, D3=P0.29, D4=P0.04, D5=P0.05, D6=P1.11, D7=P1.12, D8=P1.13, D9=P1.14, D10=P1.15):

Pin Prospector TOTEM dongle kscan
D2 APDS9960 int-gpios row 2
D3 display reset row 3
D4 i2c1 SDA (P0.04) col 4
D5 i2c1 SCL (P0.05) col 3
D6 backlight PWM (P1.11)
D7 display DC
D8 SPI3 SCK (P1.13) col 0
D9 display CS col 1
D10 SPI3 MOSI (P1.15) col 2

Seven collisions, not five. Prospector occupies every pin except D0 and D1. Devicetree does not diagnose this, so it fails at runtime, and it is a defect on the bare dongle too — it currently drives nine GPIOs for a board with no keys.

What changed

fix(keymap): restore the missing ADJ binding) — unrelated to the display, found by keymap-drawer on its first run. ADJ had 37 bindings where every other layer has 38, so the whole bottom row was shifted one slot and the last key fell off; &bt BT_PRV sat under RESET/BOOTLOAD instead of under BT_CLR/BT_NXT. The ASCII art above the block already showed the intended placement. Confirms §5's "the committed keymap has never been tested" reading — this could not have survived a day of use.

ci: select the ZMK variant of xiao_ble#1's stated fix was never actually committed. build.yaml still asked for bare xiao_ble. Now xiao_ble//zmk, confirmed by the local build log: Board: xiao_ble, qualifiers: nrf52840/zmk.

build: port the Nix flake and keymap-drawer from the Sofle config — step 1 of the plan.

feat(dongle): free the dongle's pins and add a Prospector buildkscan0 loses its compatible and row-gpios in totem.dtsi; the halves declare the matrix driver themselves; totem_dongle.overlay overrides zmk,kscan with a mock. It still includes totem.dtsi for the transform and physical layout, so Studio keeps showing TOTEM.

Both dongles build from one shield:

totem_dongle      bare XIAO, travel
totem_prospector  same firmware plus ST7789 and light sensor

prospector_adapter only decides whether the display code compiles in — same board, same keymap, so either image runs on either XIAO. Status-screen choice sits in config/prospector_adapter.conf; verified the build merges it for the Prospector target only.

The module is pinned to ed98221f rather than tracking the branch. Riding a WIP branch tip would break the flake's zephyrDepsHash at random times for no benefit.

Step-1 open questions, answered

  • zmk-nix + xiao_ble//zmk on Zephyr 4.1: works, no patching, no pinning. board is passed verbatim to west build -b.
  • enableZmkStudio does imply studio-rpc-usb-uart — and unconditionally, on every target, which contradicts build.yaml. Bypassed: extraCmakeFlags + explicit protobuf/grpcio inputs, with snippets set per target.
  • -d points at config/boards/shields/totem/totem.dtsi; no separate -layouts.dtsi here.
  • No zephyr/module.yml needed — the shield lives under config/, where ZMK_CONFIG finds it.
  • Studio locking left at default; &studio_unlock is real here.

Not ported: build.sh (docker, redundant now CI works and the flake builds locally), the Sofle's raw_binding_map and its per-binding CSS, and the vendored MDI glyphs plus glyphs.sh — no legend here uses a glyph yet, so drawing is offline-safe as is.

Verification

All five images built locally via nix build .#firmware:

totem_left.uf2            393216
totem_right.uf2           393216
totem_dongle.uf2          527872
totem_prospector.uf2      906240
totem_settings_reset.uf2  116224
  • Prospector links display_st7789v.c, apds9960.c, brightness.c, custom_status_screen.c.
  • Bare dongle links kscan_mock.c; halves still link kscan_gpio_matrix.c. Pin conflict gone.
  • Prospector RAM is at 93.4% (244826 / 256 KB), flash 56.1%, with Studio enabled. It fits, with little headroom. CONFIG_LV_Z_VDB_SIZE=25 is upstream's documented escape hatch if a future change tips it over.
  • nix run .#draw regenerates keymap-drawer/totem.{yaml,svg}.

Note on a pre-existing quirk

-DCONFIG_ZMK_STUDIO=y without the snippet is a no-op: totem_left/totem_right run zero nanopb steps and are 138 KB smaller than the dongle, so Studio is not actually compiled into the halves. That is existing build.yaml behaviour, mirrored faithfully rather than silently changed. Either drop the flag from the two peripherals or add the snippet — separate decision.

Revised next steps

  1. Port the Nix flake + keymap-drawer. Done.
  2. Fix the dongle mock kscan. Done, and it was blocking the Prospector outright.
  3. Add - and _; reconsider KP_N* → plain number keycodes. Still open, still independent.
  4. Delete config/boards/shields/totem/totem.keymap. Still open — now cheap to confirm against a local build.
  5. Drop the drifted ASCII art in favour of the drawer output.
  6. New: add display-name to each keymap layer. The Prospector layer widget reads it and falls back to the bare layer index without it.
  7. New: pairing order matters — after flashing, pair left first, then right, or the peripheral battery widget shows them swapped.

§1 is now decided by hardware rather than by cost, so englmaxi and YADS drop out. §2 pinning is unchanged: still unpinned zmk@main, and now with one pinned module hanging off it. §4 and §5 are untouched by any of this.

## Steps 1 and 2 done, and §1 reverses: Prospector is the pick, not englmaxi Branch `nix-flake` off `totem-dongle`, four commits, not pushed. Hardware fact that changes the plan: the Prospector board is already owned. Home = Prospector, travel = bare dongle, so **both** dongle images are wanted, not one. ### §1 correction — the reason to defer Prospector has expired The previous comment deferred it on "merge status unverified" for `feat/new-status-screens`, last traced ~Feb 2026. Checked the branch directly. Its HEAD is `ed98221f` (2026-02-17), **"update readme, use new xiao_ble//zmk board target"** — it targets exactly this repo's line. The README's install instructions now literally say: ```yaml - board: xiao_ble//zmk shield: [YOUR KEYBOARD SHIELD]_dongle prospector_adapter ``` It also ships `boards/shields/prospector_adapter/boards/xiao_ble_zmk.overlay`, i.e. a board overlay written for the ZMK variant. Not a WIP that happens to compile — it was ported to this target on purpose. Prior art found for the exact combination: **[eigatech/zmk-config@totem-prospector](https://github.com/eigatech/zmk-config/tree/totem-prospector)**, last touched 2026-04-20 ("update board variant"). Same TOTEM shield tree as this repo, same board qualifier, same module branch. The structure below follows it. Checked GEIGEIGEIST/zmk-config-totem for comparison: **upstream has no dongle at all**, so there is nothing newer to take from there. The stale `totem-prospector@origin` branch in this repo is an older copy of eigatech's, safe to delete as the original body assumed. ### The pin conflict is worse than reported, and it is a bug today Previously flagged as D4/D5 + D8/D9/D10. The real overlap, resolved through `seeed_xiao_connector.dtsi` (D0=P0.02, D1=P0.03, D2=P0.28, D3=P0.29, D4=P0.04, D5=P0.05, D6=P1.11, D7=P1.12, D8=P1.13, D9=P1.14, D10=P1.15): | Pin | Prospector | TOTEM dongle kscan | |---|---|---| | D2 | APDS9960 `int-gpios` | row 2 | | D3 | display reset | row 3 | | D4 | `i2c1` SDA (P0.04) | col 4 | | D5 | `i2c1` SCL (P0.05) | col 3 | | D6 | backlight PWM (P1.11) | — | | D7 | display DC | — | | D8 | SPI3 SCK (P1.13) | col 0 | | D9 | display CS | col 1 | | D10 | SPI3 MOSI (P1.15) | col 2 | Seven collisions, not five. Prospector occupies **every pin except D0 and D1**. Devicetree does not diagnose this, so it fails at runtime, and it is a defect on the bare dongle too — it currently drives nine GPIOs for a board with no keys. ### What changed **`fix(keymap): restore the missing ADJ binding)`** — unrelated to the display, found by keymap-drawer on its first run. ADJ had 37 bindings where every other layer has 38, so the whole bottom row was shifted one slot and the last key fell off; `&bt BT_PRV` sat under RESET/BOOTLOAD instead of under BT_CLR/BT_NXT. The ASCII art above the block already showed the intended placement. Confirms §5's "the committed keymap has never been tested" reading — this could not have survived a day of use. **`ci: select the ZMK variant of xiao_ble`** — #1's stated fix was never actually committed. `build.yaml` still asked for bare `xiao_ble`. Now `xiao_ble//zmk`, confirmed by the local build log: `Board: xiao_ble, qualifiers: nrf52840/zmk`. **`build: port the Nix flake and keymap-drawer from the Sofle config`** — step 1 of the plan. **`feat(dongle): free the dongle's pins and add a Prospector build`** — `kscan0` loses its `compatible` and `row-gpios` in `totem.dtsi`; the halves declare the matrix driver themselves; `totem_dongle.overlay` overrides `zmk,kscan` with a mock. It still includes `totem.dtsi` for the transform and physical layout, so Studio keeps showing TOTEM. Both dongles build from one shield: ``` totem_dongle bare XIAO, travel totem_prospector same firmware plus ST7789 and light sensor ``` `prospector_adapter` only decides whether the display code compiles in — same board, same keymap, so either image runs on either XIAO. Status-screen choice sits in `config/prospector_adapter.conf`; verified the build merges it for the Prospector target only. The module is **pinned to `ed98221f`** rather than tracking the branch. Riding a WIP branch tip would break the flake's `zephyrDepsHash` at random times for no benefit. ### Step-1 open questions, answered - **`zmk-nix` + `xiao_ble//zmk` on Zephyr 4.1: works, no patching, no pinning.** `board` is passed verbatim to `west build -b`. - **`enableZmkStudio` does imply `studio-rpc-usb-uart`** — and unconditionally, on every target, which contradicts `build.yaml`. Bypassed: `extraCmakeFlags` + explicit protobuf/grpcio inputs, with `snippets` set per target. - **`-d`** points at `config/boards/shields/totem/totem.dtsi`; no separate `-layouts.dtsi` here. - **No `zephyr/module.yml`** needed — the shield lives under `config/`, where `ZMK_CONFIG` finds it. - **Studio locking** left at default; `&studio_unlock` is real here. Not ported: `build.sh` (docker, redundant now CI works and the flake builds locally), the Sofle's `raw_binding_map` and its per-binding CSS, and the vendored MDI glyphs plus `glyphs.sh` — no legend here uses a glyph yet, so drawing is offline-safe as is. ### Verification All five images built locally via `nix build .#firmware`: ``` totem_left.uf2 393216 totem_right.uf2 393216 totem_dongle.uf2 527872 totem_prospector.uf2 906240 totem_settings_reset.uf2 116224 ``` - Prospector links `display_st7789v.c`, `apds9960.c`, `brightness.c`, `custom_status_screen.c`. - Bare dongle links `kscan_mock.c`; halves still link `kscan_gpio_matrix.c`. Pin conflict gone. - **Prospector RAM is at 93.4%** (244826 / 256 KB), flash 56.1%, with Studio enabled. It fits, with little headroom. `CONFIG_LV_Z_VDB_SIZE=25` is upstream's documented escape hatch if a future change tips it over. - `nix run .#draw` regenerates `keymap-drawer/totem.{yaml,svg}`. ### Note on a pre-existing quirk `-DCONFIG_ZMK_STUDIO=y` **without** the snippet is a no-op: `totem_left`/`totem_right` run zero nanopb steps and are 138 KB smaller than the dongle, so Studio is not actually compiled into the halves. That is existing `build.yaml` behaviour, mirrored faithfully rather than silently changed. Either drop the flag from the two peripherals or add the snippet — separate decision. ### Revised next steps 1. ~~Port the Nix flake + keymap-drawer.~~ Done. 2. ~~Fix the dongle mock kscan.~~ Done, and it was blocking the Prospector outright. 3. Add `-` and `_`; reconsider `KP_N*` → plain number keycodes. Still open, still independent. 4. Delete `config/boards/shields/totem/totem.keymap`. Still open — now cheap to confirm against a local build. 5. Drop the drifted ASCII art in favour of the drawer output. 6. **New:** add `display-name` to each keymap layer. The Prospector layer widget reads it and falls back to the bare layer index without it. 7. **New:** pairing order matters — after flashing, pair left first, then right, or the peripheral battery widget shows them swapped. §1 is now decided by hardware rather than by cost, so englmaxi and YADS drop out. §2 pinning is unchanged: still unpinned `zmk@main`, and now with one pinned module hanging off it. §4 and §5 are untouched by any of this.
Author
Owner

Pushed. Steps 3, 4 and 6 also done — plus a correction to my own last comment

totem-dongle is now at be1e7e6c, four commits ahead of where it was.

Correction: #1's fix was committed, and the vendored workflow does exist

My previous comment claimed "#1's stated fix was never actually committed." Wrong. The local clone was simply stale — jj git fetch had been failing silently. origin/totem-dongle already carried three commits I had not seen:

d3239ab  build: select the zmk variant of xiao_ble
39d0924  ci: vendor zmk's main build workflow with upload-artifact v3
5c5dfc4  ci: call the vendored build workflow

So the original issue body was right on both counts: build.yaml already used xiao_ble//zmk, and .github/workflows/build-user-config.yml is a real vendored copy with the artifact-v3 pin. My duplicate ci: select the ZMK variant of xiao_ble commit has been abandoned; the rest rebased onto the true tip, with the build.yaml conflict resolved in favour of the five-entry matrix. Rebuilt all five images after the rebase before pushing.

Landed

f608abd fix(keymap): restore the missing ADJ binding
0f20287 build: port the Nix flake and keymap-drawer from the Sofle config
c3fd34a feat(dongle): free the dongle's pins and add a Prospector build
be1e7e6 fix(keymap): reachable -, _, =, +, ~, |; drop the keypad codes

Step 3 — the symbols, and where they went

- and _ were unreachable on every layer, as reported. They now sit on SYM's left home row, which until now held a second copy of the arrow cluster that SYM's right half already has — and NAV has a third. SYM is entered by a right-thumb sticky, so the left hand is free and the symbols land on the strongest fingers:

~  _  -  =  +      ←  ↓  ↑  →  |

| goes on the right pinky, next to the \ already on the row below. That closes every gap the earlier audit named: - _ = + ~ | were all missing, \ and the shifted-number row were already there.

KP_N* dropped. NAV used KP_N0KP_N9 plus KP_PLUS/KP_MINUS/KP_MULTIPLY. All NumLock-dependent, and niri never sets NumLock, so what the host actually produced was a coin flip. Now plain N0N9, PLUS, MINUS, ASTRK.

Step 6 — display-name, and it was overdue anyway

label on a zmk,keymap layer is marked deprecated in ZMK's own binding (zmk,keymap.yaml), not merely superseded. All five layers now use display-name, which is also the property the Prospector's layer widget reads — without it the screen falls back to bare layer indices.

Step 4 — dead keymap deleted

config/boards/shields/totem/totem.keymap is gone. It was the upstream 10-layer default (Base/Media/Nav/Mouse/Sym/Num/Fun/Button/Game/Gameplus), shadowed by config/totem.keymap and never built. Confirmed by building all five images after removal.

Step 5 — needs a decision, not a commit

NAV and SYM's ASCII art is updated to match the new bindings. Along the way the art turned out to have been lying in more places than the earlier audit caught — on NAV alone: ESC and BT CLR shown as blanks, = shown as blank, DEL shown as blank, BSPC missing, and the whole thumb row wrong.

BASE's art is still wrong and I left it alone, because fixing it is the wrong move if the art is going away: &mt ESC Q shows as plain Q, &lt GER GRAVE as plain `, the sticky thumbs as plain NAV/SYM, and the 0+1 ESC combo is absent entirely.

The tradeoff, stated plainly: the art is nice to have next to the bindings, but it has drifted on every layer, it cannot express hold-taps, stickies or combos, and keymap-drawer/totem.svg now shows all of them correctly and regenerates with one command. Keeping both means maintaining the same information twice, by hand, in the format that is worse at it.

Ask: drop the per-layer art in favour of the drawer output, or keep it and I fix BASE too? Not doing either silently.

Verification

All five images rebuilt after every step:

totem_left.uf2            393216
totem_right.uf2           393216
totem_dongle.uf2          527872
totem_prospector.uf2      906240
totem_settings_reset.uf2  116224

nix build .#keymap and nix run .#draw both clean; the committed SVG now shows the new SYM row.

What is left

  • §4 layout — PUQ30 vs Colemak-DH. Untouched and still the gate on §5. Nothing done above presumes an answer; if PUQ30 wins, the SYM row above survives the change since it is not part of the 30-key core.
  • Step 5 above — awaiting the call.
  • §2 pinning — unchanged: unpinned zmk@main with one pinned module (prospector-zmk-module at ed98221f) hanging off it.
  • Prospector RAM still at 93.4%. Anything added to the display build should be checked against that.
  • Pairing order after flashing: left first, then right.
## Pushed. Steps 3, 4 and 6 also done — plus a correction to my own last comment `totem-dongle` is now at `be1e7e6c`, four commits ahead of where it was. ### Correction: #1's fix *was* committed, and the vendored workflow *does* exist My previous comment claimed "#1's stated fix was never actually committed." **Wrong.** The local clone was simply stale — `jj git fetch` had been failing silently. `origin/totem-dongle` already carried three commits I had not seen: ``` d3239ab build: select the zmk variant of xiao_ble 39d0924 ci: vendor zmk's main build workflow with upload-artifact v3 5c5dfc4 ci: call the vendored build workflow ``` So the original issue body was right on both counts: `build.yaml` already used `xiao_ble//zmk`, and `.github/workflows/build-user-config.yml` is a real vendored copy with the artifact-v3 pin. My duplicate `ci: select the ZMK variant of xiao_ble` commit has been abandoned; the rest rebased onto the true tip, with the `build.yaml` conflict resolved in favour of the five-entry matrix. Rebuilt all five images after the rebase before pushing. ### Landed | | | |---|---| | `f608abd` | `fix(keymap): restore the missing ADJ binding` | | `0f20287` | `build: port the Nix flake and keymap-drawer from the Sofle config` | | `c3fd34a` | `feat(dongle): free the dongle's pins and add a Prospector build` | | `be1e7e6` | `fix(keymap): reachable -, _, =, +, ~, \|; drop the keypad codes` | ### Step 3 — the symbols, and where they went `-` and `_` were unreachable on every layer, as reported. They now sit on **SYM's left home row**, which until now held a *second* copy of the arrow cluster that SYM's right half already has — and NAV has a third. SYM is entered by a right-thumb sticky, so the left hand is free and the symbols land on the strongest fingers: ``` ~ _ - = + ← ↓ ↑ → | ``` `|` goes on the right pinky, next to the `\` already on the row below. That closes every gap the earlier audit named: `- _ = + ~ |` were all missing, `\` and the shifted-number row were already there. **KP_N\* dropped.** NAV used `KP_N0`–`KP_N9` plus `KP_PLUS`/`KP_MINUS`/`KP_MULTIPLY`. All NumLock-dependent, and niri never sets NumLock, so what the host actually produced was a coin flip. Now plain `N0`–`N9`, `PLUS`, `MINUS`, `ASTRK`. ### Step 6 — `display-name`, and it was overdue anyway `label` on a `zmk,keymap` layer is **marked deprecated in ZMK's own binding** (`zmk,keymap.yaml`), not merely superseded. All five layers now use `display-name`, which is also the property the Prospector's layer widget reads — without it the screen falls back to bare layer indices. ### Step 4 — dead keymap deleted `config/boards/shields/totem/totem.keymap` is gone. It was the upstream 10-layer default (Base/Media/Nav/Mouse/Sym/Num/Fun/Button/Game/Gameplus), shadowed by `config/totem.keymap` and never built. Confirmed by building all five images after removal. ### Step 5 — needs a decision, not a commit NAV and SYM's ASCII art is updated to match the new bindings. Along the way the art turned out to have been lying in more places than the earlier audit caught — on NAV alone: `ESC` and `BT CLR` shown as blanks, `=` shown as blank, `DEL` shown as blank, `BSPC` missing, and the whole thumb row wrong. **BASE's art is still wrong and I left it alone**, because fixing it is the wrong move if the art is going away: `&mt ESC Q` shows as plain `Q`, `&lt GER GRAVE` as plain `` ` ``, the sticky thumbs as plain `NAV`/`SYM`, and the 0+1 ESC combo is absent entirely. The tradeoff, stated plainly: the art is nice to have next to the bindings, but it has drifted on every layer, it cannot express hold-taps, stickies or combos, and `keymap-drawer/totem.svg` now shows all of them correctly and regenerates with one command. Keeping both means maintaining the same information twice, by hand, in the format that is worse at it. **Ask:** drop the per-layer art in favour of the drawer output, or keep it and I fix BASE too? Not doing either silently. ### Verification All five images rebuilt after every step: ``` totem_left.uf2 393216 totem_right.uf2 393216 totem_dongle.uf2 527872 totem_prospector.uf2 906240 totem_settings_reset.uf2 116224 ``` `nix build .#keymap` and `nix run .#draw` both clean; the committed SVG now shows the new SYM row. ### What is left - **§4 layout — PUQ30 vs Colemak-DH.** Untouched and still the gate on §5. Nothing done above presumes an answer; if PUQ30 wins, the SYM row above survives the change since it is not part of the 30-key core. - **Step 5** above — awaiting the call. - **§2 pinning** — unchanged: unpinned `zmk@main` with one pinned module (`prospector-zmk-module` at `ed98221f`) hanging off it. - Prospector RAM still at 93.4%. Anything added to the display build should be checked against that. - Pairing order after flashing: left first, then right.
Author
Owner

README added — and the GER layer is broken on the daily driver

totem-dongle at 6ef25618.

I was wrong twice. Both corrections matter.

1. -DCONFIG_ZMK_STUDIO=y on the halves is not dead config. I said it was a no-op and offered to drop it. I tested that by removing it and diffing — the image changed: 1248 B less flash, and Zephyr's subsys/pm/device.c + device_system_managed.c no longer compiled. The reason is in zmk/app/src/studio/Kconfig:

menuconfig ZMK_STUDIO
    select ZMK_STUDIO_RPC if !ZMK_SPLIT || ZMK_SPLIT_ROLE_CENTRAL
    select PM_DEVICE # Needed for physical layout switching

Peripherals are deliberately excluded from the RPC — which is exactly why they run zero nanopb steps and looked inert to me — but PM_DEVICE is selected unconditionally, and physical-layout switching needs it. Dropping the flag would have broken layout switching from Studio. Reverted, and a comment now sits at the call site in flake.nix so nobody cleans it up again. Delete that item from the plan; there is nothing to decide.

2. §4's "not documented anywhere which OS layout that assumes" is now documented — and the assumption does not hold where it matters.

Verified all five bindings against xkeyboard-config 2.47 (symbols/us):

binding level-3 symbol keycode
&kp RA(Q) adiaeresis ä <AD01>
&kp RA(S) ssharp ß <AC02>
&kp RA(Y) udiaeresis ü <AD06>
&kp RA(P) odiaeresis ö <AD10>
&kp RA(5) EuroSign <AE05>

All five correct for us + variant altgr-intl. us(intl) supplies the symbols, and altgr-intl ends with include "level3(ralt_switch)", so the variant carries the level-3 shift itself — no separate lv3:ralt_switch option is needed, contrary to what the earlier comment implied.

The niri gap is real, and worse than "either something else supplies it"

The earlier comment hedged: "Either something else supplies it or niri isn't used for German prose." Checked. Nothing supplies it.

nix-config/modules/nixos/keyboard.nix is correct:

services.xserver.xkb = {
  layout = "us";
  variant = "altgr-intl";
  options = "eurosign:e,terminate:ctrl_alt_bksp,grp:shifts_toggle,lv3:ralt_switch";
};

nix-config/home-manager/niri/default.nix:36 is not:

input.keyboard.xkb = {
  layout = "us,ua,ru";
  options = "grp:alt_shift_toggle";
};

niri does not read services.xserver.xkb. So on the Wayland session:

  • No variant at all. Plain us has no level-3 symbols on q/s/y/p/5.
  • options is replaced, not mergedlv3:ralt_switch and eurosign:e are both gone, so RightAlt stays a plain Alt_R modifier.
  • Net effect: every GER binding emits Alt+letter, which applications read as a shortcut. Not a wrong character — a keystroke that triggers menus.
  • Separately: grp:alt_shift_toggle across three layouts is a live hazard for this keymap. The base layer puts Alt and Shift on home-row mods (&hml LALT A, &hml LSHFT F, &hmr RSHFT J, &hmr RALT SEMI). An HRM misfire that lands Alt+Shift together silently switches the host to Ukrainian or Russian.

The fix belongs in nix-config, not here — positional variant list, altgr-intl applying to the first layout only:

input.keyboard.xkb = {
  layout = "us,ua,ru";
  variant = "altgr-intl,,";
  options = "grp:alt_shift_toggle,eurosign:e";
};

and grp:alt_shift_toggle is worth reconsidering independently of the above. Not touching that repo without a say-so — want me to?

This is the single highest-impact finding so far: it means the German half of the keymap has never actually worked under niri, regardless of what the firmware does. Worth confirming by hand before I change anything: on the Wayland session, does AltGr+q produce ä or open a menu?

README

The repo had none. README.md now covers what was only in issue comments and in my head:

  • which of the four XIAOs runs which of the five images, and that the two dongle images are interchangeable at the hardware level;
  • flashing, and the left-then-right pairing order the Prospector's battery widget depends on;
  • the us + altgr-intl requirement above — the actual point of writing it;
  • local build commands and the zephyrDepsHash refresh;
  • the caveats: unpinned ZMK vs pinned module, 93.4% Prospector RAM, the mock kscan, and the Studio-flag correction.

It embeds keymap-drawer/totem.svg and says plainly that the drawing is authoritative and the in-file ASCII art is not.

Still open

  • Step 5 — drop the per-layer ASCII art or keep it and fix BASE. Unanswered; the README now leans on the drawer, which is an argument for dropping, but I am not deciding it silently.
  • §4 layout — PUQ30 vs Colemak-DH. Untouched, still the gate on §5.
  • The niri fix in nix-config — needs your go-ahead, and ideally a manual confirmation of the symptom first.
## README added — and the GER layer is broken on the daily driver `totem-dongle` at `6ef25618`. ### I was wrong twice. Both corrections matter. **1. `-DCONFIG_ZMK_STUDIO=y` on the halves is *not* dead config.** I said it was a no-op and offered to drop it. I tested that by removing it and diffing — the image *changed*: 1248 B less flash, and Zephyr's `subsys/pm/device.c` + `device_system_managed.c` no longer compiled. The reason is in `zmk/app/src/studio/Kconfig`: ```kconfig menuconfig ZMK_STUDIO select ZMK_STUDIO_RPC if !ZMK_SPLIT || ZMK_SPLIT_ROLE_CENTRAL select PM_DEVICE # Needed for physical layout switching ``` Peripherals are deliberately excluded from the RPC — which is exactly why they run zero nanopb steps and looked inert to me — but `PM_DEVICE` is selected unconditionally, and physical-layout switching needs it. **Dropping the flag would have broken layout switching from Studio.** Reverted, and a comment now sits at the call site in `flake.nix` so nobody cleans it up again. Delete that item from the plan; there is nothing to decide. **2. §4's "not documented anywhere which OS layout that assumes" is now documented — and the assumption does not hold where it matters.** Verified all five bindings against xkeyboard-config 2.47 (`symbols/us`): | binding | level-3 symbol | keycode | |---|---|---| | `&kp RA(Q)` | `adiaeresis` ä | `<AD01>` | | `&kp RA(S)` | `ssharp` ß | `<AC02>` | | `&kp RA(Y)` | `udiaeresis` ü | `<AD06>` | | `&kp RA(P)` | `odiaeresis` ö | `<AD10>` | | `&kp RA(5)` | `EuroSign` € | `<AE05>` | All five correct for **`us` + variant `altgr-intl`**. `us(intl)` supplies the symbols, and `altgr-intl` ends with `include "level3(ralt_switch)"`, so the variant carries the level-3 shift itself — **no separate `lv3:ralt_switch` option is needed**, contrary to what the earlier comment implied. ### The niri gap is real, and worse than "either something else supplies it" The earlier comment hedged: *"Either something else supplies it or niri isn't used for German prose."* Checked. Nothing supplies it. `nix-config/modules/nixos/keyboard.nix` is correct: ```nix services.xserver.xkb = { layout = "us"; variant = "altgr-intl"; options = "eurosign:e,terminate:ctrl_alt_bksp,grp:shifts_toggle,lv3:ralt_switch"; }; ``` `nix-config/home-manager/niri/default.nix:36` is not: ```nix input.keyboard.xkb = { layout = "us,ua,ru"; options = "grp:alt_shift_toggle"; }; ``` niri does not read `services.xserver.xkb`. So on the Wayland session: - **No `variant` at all.** Plain `us` has no level-3 symbols on q/s/y/p/5. - **`options` is replaced, not merged** — `lv3:ralt_switch` and `eurosign:e` are both gone, so RightAlt stays a plain `Alt_R` modifier. - Net effect: every GER binding emits **Alt+letter**, which applications read as a shortcut. Not a wrong character — a keystroke that triggers menus. - Separately: **`grp:alt_shift_toggle` across three layouts is a live hazard for this keymap.** The base layer puts Alt and Shift on home-row mods (`&hml LALT A`, `&hml LSHFT F`, `&hmr RSHFT J`, `&hmr RALT SEMI`). An HRM misfire that lands Alt+Shift together silently switches the host to Ukrainian or Russian. The fix belongs in `nix-config`, not here — positional variant list, `altgr-intl` applying to the first layout only: ```nix input.keyboard.xkb = { layout = "us,ua,ru"; variant = "altgr-intl,,"; options = "grp:alt_shift_toggle,eurosign:e"; }; ``` and `grp:alt_shift_toggle` is worth reconsidering independently of the above. **Not touching that repo without a say-so** — want me to? This is the single highest-impact finding so far: it means the German half of the keymap has never actually worked under niri, regardless of what the firmware does. Worth confirming by hand before I change anything: on the Wayland session, does AltGr+q produce `ä` or open a menu? ### README The repo had none. `README.md` now covers what was only in issue comments and in my head: - which of the four XIAOs runs which of the five images, and that the two dongle images are interchangeable at the hardware level; - flashing, and the left-then-right pairing order the Prospector's battery widget depends on; - **the `us` + `altgr-intl` requirement above** — the actual point of writing it; - local build commands and the `zephyrDepsHash` refresh; - the caveats: unpinned ZMK vs pinned module, 93.4% Prospector RAM, the mock kscan, and the Studio-flag correction. It embeds `keymap-drawer/totem.svg` and says plainly that the drawing is authoritative and the in-file ASCII art is not. ### Still open - **Step 5** — drop the per-layer ASCII art or keep it and fix BASE. Unanswered; the README now leans on the drawer, which is an argument for dropping, but I am not deciding it silently. - **§4 layout — PUQ30 vs Colemak-DH.** Untouched, still the gate on §5. - **The niri fix** in `nix-config` — needs your go-ahead, and ideally a manual confirmation of the symptom first.
Author
Owner

§5 rewritten against the verified Sofle baseline

Step 5 is done (cd10b8c3 — per-layer ASCII grids dropped, drawing is authoritative). What remains is §4 and §5, and §5 can now be written properly because the baseline has been checked rather than summarised.

Baseline, read from the source not the README summary

One correction to my own reading first: the Sofle's keymap_drawer.config.yaml carries a raw_binding_map full of &hm LEFT_GUI A and &ltq 1 BACKSPACE. That is borrowed boilerplate from englmaxi's config (the file credits it), not a description of the keymap. layer0 row 3 is &kp LSHFT &kp A &kp S &kp D &kp F &kp G — plain &kp throughout, and the README states it outright: "no hold-taps, combos or macros." The earlier premise stands. Dropping that map during the flake port was the right call.

Hardware, from the README rather than assumed: EC11 encoder on the left, 5-way hat switch on the right (matrix column 7, rows 0–4) — not a trackball. Arrows on the hat on qwerty, mouse movement on the other layers.

Sofle (daily) TOTEM
positions 5 rows incl. number row 38
numbers dedicated row NAV layer
mods dedicated keys — outer columns + bottom row home-row mods
layer access &mo momentary &sl sticky
arrows 5-way hat, live on base NAV / SYM
mouse 5-way hat on layers 1/3 none
hold-taps none, anywhere &mt, &hml, &hmr, &lt
combos none one (0+1 → ESC)
German RA(Q/S/Y/P/5) layer_1 same scheme on GER

Settled

  • Encoder / scroll / volume: not a requirement. Removed from consideration.
  • Mouse keys on the TOTEM: future feature, not now. Noted as a candidate, not scheduled. ZMK pointing would need CONFIG_ZMK_POINTING plus somewhere to put &mmv/&mkp — cheapest on an existing layer, and worth revisiting only once the layout has settled.
  • Sofle stays in service. Confirmed, and for the right reason: the first weeks are training-only, with real work staying on the Sofle. That is the standard arrangement and it is free here, because two different boards keep the two alphabets in separate motor contexts.
  • Numbers on a layer: accepted.

The number-layer / window-manager friction, concretely

Worth naming because the obvious workaround is bad. With numbers on NAV, Mod+1 becomes hold-a-home-row-mod plus a sticky layer plus a digit. That is a genuinely awkward three-part action, and worse, &sl is one-shot — it is consumed by the next key press, which may be the modifier rather than the digit.

The clean fix is to bind the whole chord, not compose it: put &kp LG(N1)&kp LG(N9) directly on a layer, so switching desktop is one key press on a layer with no modifier held at all. No HRM involvement, no sticky-layer interaction.

Less urgent than it looked, since i3 is no longer the daily desktop — Plasma's defaults sit on Ctrl+F1…F4, and the ADJ layer already carries F1–F12. Decide it when the WM is settled; it does not gate anything.

The blocker nobody has named: the committed keymap is not trainable

Everything in §5 says start minimal. The keymap in this repo is the opposite of minimal, and starting from it contradicts the plan on four counts:

  1. Home-row mods on day one&hml/&hmr across all eight home positions, against a baseline with zero hold-tap experience of any kind.
  2. require-prior-idle-ms = <150>. urob's rule is 10500 / relaxed WPM; at a beginner's 30–40 wpm that is 260–350 ms. 150 is tuned for someone already fast, and will misfire constantly at 25 wpm.
  3. Sticky layers (&sl NAV, &sl SYM) where the existing habit is momentary &mo.
  4. Extra hold-taps on top&mt ESC Q and &lt GER GRAVE, both on keys that also carry a letter.

So the first deliverable of §5 is not a schedule, it is a stripped starting keymap: target alphabet, mods on thumbs and outer keys the way the Sofle has them, &mo for layers, one nav/number layer, no HRMs, no combos, no &mt/&lt. The full keymap becomes the destination, reached in stages, not the starting point.

Revised schedule

Given the Sofle keeps all real work, the risk that made a gradual alphabet change tempting is gone — which is exactly why the alphabet should change on day one rather than after a QWERTY-on-38-keys detour that would train interference to be unlearned later.

Phase TOTEM Keymap state Milestone
0 Build the stripped keymap above
1–2 wk 2×15 min drills/day; Sofle for everything Alphabet + thumbs; one nav/num layer, &mo. No HRMs. ~20–30 wpm
3–4 wk drills + low-stakes prose Add symbol layer on the other thumb ~30–40 wpm
5–6 wk most prose on TOTEM German via GER, drilled explicitly ~40–50 wpm
7–10 wk full-time incl. coding Shift-only HRM on the index fingers; walk require-prior-idle-ms down from 350 Sofle parity
11+ Remaining HRMs one pair at a time; combos last; &mt/&lt last of all

Two 15–30 min sessions beat one long block. Accuracy 97–99% before pushing speed. Stopping permanently at shift-only HRMs is a legitimate endpoint.

Tools: ngram-type + ngram-type-ger for bigrams including ch/sch/ei/en, Monkeytype (german 10k, stop-on-error = word, layout emulator off — the layout lives in firmware), symbols last. keybr only if the chosen alphabet is supported.

This still gates on §4, and here is the recommendation

Phase 0 cannot be built without knowing the alphabet.

The ecosystem argument for Colemak-DH is weaker than it looked, specifically because the Sofle stays in service. Its main concrete benefit is keybr's adaptive letter introduction, which is a weeks-1–2 tool; ngram-type and Monkeytype cover everything after that and are layout-agnostic. Against that, PUQ30 has real optimizer numbers on a 1:1 German/English corpus — which no English-optimized layout offers — an exact 30-key fit with zero wasted slots, two spare outer keys, and umlauts already handled by the GER scheme that is proven in daily use.

Recommendation: PUQ30. Say yes and I will build the phase-0 keymap; say Colemak-DH and I will build that instead. Either way the SYM row added in be1e7e6c survives, since it is not part of the 30-key core.

## §5 rewritten against the verified Sofle baseline Step 5 is done (`cd10b8c3` — per-layer ASCII grids dropped, drawing is authoritative). What remains is §4 and §5, and §5 can now be written properly because the baseline has been checked rather than summarised. ### Baseline, read from the source not the README summary One correction to my own reading first: the Sofle's `keymap_drawer.config.yaml` carries a `raw_binding_map` full of `&hm LEFT_GUI A` and `&ltq 1 BACKSPACE`. That is **borrowed boilerplate from englmaxi's config** (the file credits it), not a description of the keymap. `layer0` row 3 is `&kp LSHFT &kp A &kp S &kp D &kp F &kp G` — plain `&kp` throughout, and the README states it outright: *"no hold-taps, combos or macros."* The earlier premise stands. Dropping that map during the flake port was the right call. Hardware, from the README rather than assumed: EC11 encoder on the left, **5-way hat switch** on the right (matrix column 7, rows 0–4) — not a trackball. Arrows on the hat on `qwerty`, mouse movement on the other layers. | | Sofle (daily) | TOTEM | |---|---|---| | positions | 5 rows incl. number row | 38 | | numbers | dedicated row | NAV layer | | mods | dedicated keys — outer columns + bottom row | home-row mods | | layer access | `&mo` momentary | `&sl` sticky | | arrows | 5-way hat, **live on base** | NAV / SYM | | mouse | 5-way hat on layers 1/3 | none | | hold-taps | **none, anywhere** | `&mt`, `&hml`, `&hmr`, `&lt` | | combos | none | one (0+1 → ESC) | | German | `RA(Q/S/Y/P/5)` layer_1 | same scheme on GER | ### Settled - **Encoder / scroll / volume: not a requirement.** Removed from consideration. - **Mouse keys on the TOTEM: future feature, not now.** Noted as a candidate, not scheduled. ZMK pointing would need `CONFIG_ZMK_POINTING` plus somewhere to put `&mmv`/`&mkp` — cheapest on an existing layer, and worth revisiting only once the layout has settled. - **Sofle stays in service.** Confirmed, and for the right reason: the first weeks are training-only, with real work staying on the Sofle. That is the standard arrangement and it is free here, because two different boards keep the two alphabets in separate motor contexts. - **Numbers on a layer: accepted.** ### The number-layer / window-manager friction, concretely Worth naming because the obvious workaround is bad. With numbers on NAV, `Mod+1` becomes hold-a-home-row-mod **plus** a sticky layer **plus** a digit. That is a genuinely awkward three-part action, and worse, `&sl` is one-shot — it is consumed by the next key press, which may be the modifier rather than the digit. The clean fix is to **bind the whole chord**, not compose it: put `&kp LG(N1)` … `&kp LG(N9)` directly on a layer, so switching desktop is one key press on a layer with no modifier held at all. No HRM involvement, no sticky-layer interaction. Less urgent than it looked, since i3 is no longer the daily desktop — Plasma's defaults sit on `Ctrl+F1…F4`, and the ADJ layer already carries F1–F12. Decide it when the WM is settled; it does not gate anything. ### The blocker nobody has named: the committed keymap is not trainable Everything in §5 says *start minimal*. The keymap in this repo is the opposite of minimal, and starting from it contradicts the plan on four counts: 1. **Home-row mods on day one** — `&hml`/`&hmr` across all eight home positions, against a baseline with zero hold-tap experience of any kind. 2. **`require-prior-idle-ms = <150>`.** urob's rule is `10500 / relaxed WPM`; at a beginner's 30–40 wpm that is **260–350 ms**. 150 is tuned for someone already fast, and will misfire constantly at 25 wpm. 3. **Sticky layers** (`&sl NAV`, `&sl SYM`) where the existing habit is momentary `&mo`. 4. **Extra hold-taps on top** — `&mt ESC Q` and `&lt GER GRAVE`, both on keys that also carry a letter. So the first deliverable of §5 is not a schedule, it is a **stripped starting keymap**: target alphabet, mods on thumbs and outer keys the way the Sofle has them, `&mo` for layers, one nav/number layer, no HRMs, no combos, no `&mt`/`&lt`. The full keymap becomes the destination, reached in stages, not the starting point. ### Revised schedule Given the Sofle keeps all real work, the risk that made a gradual alphabet change tempting is gone — which is exactly why the alphabet should change on day one rather than after a QWERTY-on-38-keys detour that would train interference to be unlearned later. | Phase | TOTEM | Keymap state | Milestone | |---|---|---|---| | 0 | — | Build the stripped keymap above | — | | 1–2 wk | 2×15 min drills/day; **Sofle for everything** | Alphabet + thumbs; one nav/num layer, `&mo`. No HRMs. | ~20–30 wpm | | 3–4 wk | drills + low-stakes prose | Add symbol layer on the other thumb | ~30–40 wpm | | 5–6 wk | most prose on TOTEM | German via GER, drilled explicitly | ~40–50 wpm | | 7–10 wk | full-time incl. coding | Shift-only HRM on the index fingers; walk `require-prior-idle-ms` down from 350 | Sofle parity | | 11+ | — | Remaining HRMs one pair at a time; combos last; `&mt`/`&lt` last of all | — | Two 15–30 min sessions beat one long block. Accuracy 97–99% before pushing speed. Stopping permanently at shift-only HRMs is a legitimate endpoint. Tools: ngram-type + ngram-type-ger for bigrams including `ch`/`sch`/`ei`/`en`, Monkeytype (`german 10k`, stop-on-error = `word`, **layout emulator off** — the layout lives in firmware), symbols last. keybr only if the chosen alphabet is supported. ### This still gates on §4, and here is the recommendation Phase 0 cannot be built without knowing the alphabet. The ecosystem argument for Colemak-DH is **weaker than it looked**, specifically because the Sofle stays in service. Its main concrete benefit is keybr's adaptive letter introduction, which is a weeks-1–2 tool; ngram-type and Monkeytype cover everything after that and are layout-agnostic. Against that, PUQ30 has real optimizer numbers on a **1:1 German/English corpus** — which no English-optimized layout offers — an exact 30-key fit with zero wasted slots, two spare outer keys, and umlauts already handled by the GER scheme that is proven in daily use. **Recommendation: PUQ30.** Say yes and I will build the phase-0 keymap; say Colemak-DH and I will build that instead. Either way the SYM row added in `be1e7e6c` survives, since it is not part of the 30-key core.
Author
Owner

Addendum to §5 — two Sofles, and what the fallback actually costs

New constraint: there are two Sofles, one on-prem and one in the home office, and the QWERTY fallback is wanted deliberately — a board to switch to under stress or in an emergency, when there is no capacity to fight an unfamiliar layout.

That is a reasonable thing to want and it is cheap to have. Three points follow, one of which inverts part of the plan above.

The separation is about as clean as it gets

58-key row-staggered with a number row versus 38-key column-staggered with layers. Different hand span, different thumb positions, different key travel path. Motor-context separation by hardware works best when the two boards feel nothing alike, and these do not. Keeping QWERTY on both Sofles and never putting it on the TOTEM is the right arrangement.

The honest cost: the fallback is free only while it stays rare

Occasional fallback costs nothing measurable. Regular fallback keeps refreshing the competing motor pattern and the new layout never crosses into fluent — you end up with two mediocre layouts instead of one good one and one fluent one.

The failure mode is specific and worth naming: switching on a capacity judgement made in the moment. "Do I have the headroom for the TOTEM right now?" is a question that gets answered no exactly when you are tired, rushed, or behind — which is most of the time during weeks 3–8, which is precisely the stretch where the reps have to accumulate.

The fix is to make the switch contextual, not discretionary: a rule decided in advance and not re-litigated under pressure. Something like TOTEM at the home desk for everything; Sofle on-prem; Sofle anywhere if a real deadline is on fire. The emergency escape hatch stays fully intact — it is just no longer a decision you make forty times a day.

Suggested tripwire: if the TOTEM goes untouched for a full week during phases 1–4, that is the signal the rule has quietly stopped being followed, not that the layout is wrong.

The travel-board inversion

This is the part that contradicts the plan above. The hardware split we built is bare dongle = travel, Prospector = desk. But travel is the worst possible early context for the new layout — unfamiliar rooms, client sites, conference talks, other people waiting. Exactly where a half-learned alphabet fails and where a fallback is least available, since the Sofles are both bolted to desks.

So: keep the TOTEM at a desk through phases 1–4, and do not travel with it until phase 5 at the earliest, when it is comfortably past parity. The bare-dongle image is built and ready; it just should not get used early. Nothing to change in the firmware — this is a usage rule, not a config.

Effect on §4

It strengthens the PUQ30 recommendation rather than weakening it. PUQ30's only real disadvantage was ecosystem — no keybr, likely no trainer support at all. A permanent, always-available QWERTY board at both work sites de-risks exactly that: the downside of picking the less-supported layout is "drilling is more manual", not "work stops". With the fallback guaranteed, the layout with better evidence on a 1:1 DE/EN corpus is the safer pick, not the braver one.

No change to the schedule table or to the phase-0 stripped-keymap deliverable. Still waiting on the §4 call before building it.

## Addendum to §5 — two Sofles, and what the fallback actually costs New constraint: there are **two Sofles**, one on-prem and one in the home office, and the QWERTY fallback is wanted deliberately — a board to switch to under stress or in an emergency, when there is no capacity to fight an unfamiliar layout. That is a reasonable thing to want and it is cheap to have. Three points follow, one of which inverts part of the plan above. ### The separation is about as clean as it gets 58-key row-staggered with a number row versus 38-key column-staggered with layers. Different hand span, different thumb positions, different key travel path. Motor-context separation by hardware works best when the two boards feel nothing alike, and these do not. Keeping QWERTY on both Sofles and never putting it on the TOTEM is the right arrangement. ### The honest cost: the fallback is free only while it stays rare Occasional fallback costs nothing measurable. Regular fallback keeps refreshing the competing motor pattern and the new layout never crosses into fluent — you end up with two mediocre layouts instead of one good one and one fluent one. The failure mode is specific and worth naming: **switching on a capacity judgement made in the moment.** "Do I have the headroom for the TOTEM right now?" is a question that gets answered *no* exactly when you are tired, rushed, or behind — which is most of the time during weeks 3–8, which is precisely the stretch where the reps have to accumulate. The fix is to make the switch **contextual, not discretionary**: a rule decided in advance and not re-litigated under pressure. Something like *TOTEM at the home desk for everything; Sofle on-prem; Sofle anywhere if a real deadline is on fire.* The emergency escape hatch stays fully intact — it is just no longer a decision you make forty times a day. Suggested tripwire: if the TOTEM goes untouched for a **full week** during phases 1–4, that is the signal the rule has quietly stopped being followed, not that the layout is wrong. ### The travel-board inversion This is the part that contradicts the plan above. The hardware split we built is bare dongle = travel, Prospector = desk. But **travel is the worst possible early context for the new layout** — unfamiliar rooms, client sites, conference talks, other people waiting. Exactly where a half-learned alphabet fails and where a fallback is least available, since the Sofles are both bolted to desks. So: keep the TOTEM at a **desk** through phases 1–4, and do not travel with it until phase 5 at the earliest, when it is comfortably past parity. The bare-dongle image is built and ready; it just should not get used early. Nothing to change in the firmware — this is a usage rule, not a config. ### Effect on §4 It **strengthens the PUQ30 recommendation** rather than weakening it. PUQ30's only real disadvantage was ecosystem — no keybr, likely no trainer support at all. A permanent, always-available QWERTY board at both work sites de-risks exactly that: the downside of picking the less-supported layout is "drilling is more manual", not "work stops". With the fallback guaranteed, the layout with better evidence on a 1:1 DE/EN corpus is the safer pick, not the braver one. No change to the schedule table or to the phase-0 stripped-keymap deliverable. Still waiting on the §4 call before building it.
Author
Owner

§4 decision aid — PUQ30 vs Colemak-DH, side by side

Base layers only, 30-key core:

PUQ30
    p  u  ¨  ,  q       g  c  l  m  f
    h  i  e  a  o       d  t  r  n  s
    k  y  .  '  x       j  v  w  b  z

Colemak-DH
    q  w  f  p  b       j  l  u  y  ;
    a  r  s  t  g       m  n  e  i  o
    z  x  c  d  v       k  h  ,  .  /

Both are exactly 30 keys, and neither carries a full punctuation set:

letters plus missing from core
PUQ30 26 ¨ , . ' ; /
Colemak-DH 26 ; , . / '

The TOTEM has two spare outer keys (currently &lt GER GRAVE and &kp SINGLE_QUOTE), so either layout's leftovers are covered without touching the core.

PUQ30

For

  • Optimized on a 1:1 German/English corpus with Wettstein's optimizer — the same tool family as AdNW and KOY. No English-first layout offers this, and usage here is roughly balanced.
  • Built for exactly this geometry: 30 keys, matrix, thumb shift. Zero wasted slots, no adaptation, no displaced letter.
  • , . ' in-core. ~70% home row (EN), ~67% (DE); bottom row under 9%.
  • The ¨ slot was co-optimized at ~1.7% of German characters, not parked in a leftover position, and it takes &mo GER directly — reusing the RA(Q/S/Y/P/5) scheme already in daily production on the Sofle.
  • Home row h i e a o / d t r n s is identical to AdNW's, which has a long track record.

Against

  • No trainer support. No keybr adaptive letter introduction; Monkeytype with the layout emulator off. Drilling becomes manual via ngram-type and Monkeytype word lists.
  • Tiny community: no vim/tmux remap sets, no ZMK prior art, nobody to compare notes with.
  • Metrics exist only inside Wettstein's model. They are not comparable to the SFB percentages quoted for Colemak-DH or Gallium, so there is no way to sanity-check the choice against the wider ecosystem's numbers.
  • Worse adjacent-finger load in German, by its own documentation. benachbart DE is 11.8 against PUO30's 6.9, because i and e sit side by side and ei/ie are pervasive in German. This is the one metric where PUQ30 clearly loses, and it is the reason the doc says PUO30 suits German better.

Colemak-DH

For

  • Mature ecosystem: keybr, Monkeytype and typing.io support it natively; published German-corpus analysis exists; off-the-shelf vim/tmux remaps; plenty of ZMK configs to crib from.
  • The DH modification exists specifically for column-staggered boards like the TOTEM — it is not a 4-row layout squeezed into 3.
  • z x c v stay in their QWERTY positions, so Ctrl+Z/X/C/V keep their muscle memory. This was missed in the earlier analysis and it is a daily-friction item rather than a theoretical one. PUQ30's bottom-left is k y . ' x — all four move.

Against

  • Optimized for English. German performance is incidental rather than designed for, which means optimizing for the minority half of the typing.
  • No ' in the 30-key core.
  • Umlauts have no designed home. The GER layer still works, but as a bolt-on rather than a co-optimized slot.

The honest frame

Modern well-designed layouts sit within a fraction of a percentage point of each other on effort metrics — below the threshold of perceptibility. Getreuer's position that the column-staggered board, a good symbol layer and vim each matter more than the alphabet is probably correct.

So this is corpus fit versus ecosystem, not comfort.

Recommendation, unchanged: PUQ30

The corpus match is the one property that cannot be retrofitted later; trainer support can be worked around with more manual drilling. The two-Sofle QWERTY fallback (see the previous comment) converts the ecosystem gap from a risk into an inconvenience — the downside becomes "drilling is more manual", not "work stops".

The strongest argument the other way is Ctrl+Z/X/C/V. If editor shortcut use is heavy, that is a real, permanent, daily cost that no amount of drilling removes, and it deserves weighing properly before committing.

Either choice leaves the SYM row from be1e7e6c intact, since it is not part of the 30-key core. Once the call is made, phase 0 is the stripped starting keymap described in the §5 rewrite: target alphabet, mods on thumbs and outer keys as on the Sofle, &mo layers, one nav/number layer, no HRMs, no combos, require-prior-idle-ms parked at 350 for when they land in phase 4.

## §4 decision aid — PUQ30 vs Colemak-DH, side by side Base layers only, 30-key core: ``` PUQ30 p u ¨ , q g c l m f h i e a o d t r n s k y . ' x j v w b z Colemak-DH q w f p b j l u y ; a r s t g m n e i o z x c d v k h , . / ``` Both are exactly 30 keys, and neither carries a full punctuation set: | | letters | plus | missing from core | |---|---|---|---| | PUQ30 | 26 | `¨` `,` `.` `'` | `;` `/` | | Colemak-DH | 26 | `;` `,` `.` `/` | `'` | The TOTEM has two spare outer keys (currently `&lt GER GRAVE` and `&kp SINGLE_QUOTE`), so either layout's leftovers are covered without touching the core. ### PUQ30 **For** - Optimized on a **1:1 German/English corpus** with Wettstein's optimizer — the same tool family as AdNW and KOY. No English-first layout offers this, and usage here is roughly balanced. - Built for exactly this geometry: 30 keys, matrix, thumb shift. Zero wasted slots, no adaptation, no displaced letter. - `,` `.` `'` in-core. ~70% home row (EN), ~67% (DE); bottom row under 9%. - The `¨` slot was **co-optimized** at ~1.7% of German characters, not parked in a leftover position, and it takes `&mo GER` directly — reusing the `RA(Q/S/Y/P/5)` scheme already in daily production on the Sofle. - Home row `h i e a o / d t r n s` is identical to AdNW's, which has a long track record. **Against** - **No trainer support.** No keybr adaptive letter introduction; Monkeytype with the layout emulator off. Drilling becomes manual via ngram-type and Monkeytype word lists. - Tiny community: no vim/tmux remap sets, no ZMK prior art, nobody to compare notes with. - Metrics exist **only inside Wettstein's model**. They are not comparable to the SFB percentages quoted for Colemak-DH or Gallium, so there is no way to sanity-check the choice against the wider ecosystem's numbers. - **Worse adjacent-finger load in German, by its own documentation.** `benachbart` DE is 11.8 against PUO30's 6.9, because `i` and `e` sit side by side and `ei`/`ie` are pervasive in German. This is the one metric where PUQ30 clearly loses, and it is the reason the doc says PUO30 suits German better. ### Colemak-DH **For** - Mature ecosystem: keybr, Monkeytype and typing.io support it natively; published German-corpus analysis exists; off-the-shelf vim/tmux remaps; plenty of ZMK configs to crib from. - The DH modification exists specifically for column-staggered boards like the TOTEM — it is not a 4-row layout squeezed into 3. - **`z x c v` stay in their QWERTY positions**, so Ctrl+Z/X/C/V keep their muscle memory. This was missed in the earlier analysis and it is a daily-friction item rather than a theoretical one. PUQ30's bottom-left is `k y . ' x` — all four move. **Against** - Optimized for English. German performance is incidental rather than designed for, which means optimizing for the minority half of the typing. - No `'` in the 30-key core. - Umlauts have no designed home. The GER layer still works, but as a bolt-on rather than a co-optimized slot. ### The honest frame Modern well-designed layouts sit within a fraction of a percentage point of each other on effort metrics — below the threshold of perceptibility. [Getreuer's position](https://getreuer.info/posts/keyboards/alt-layouts/index.html) that the column-staggered board, a good symbol layer and vim each matter more than the alphabet is probably correct. So this is **corpus fit versus ecosystem**, not comfort. ### Recommendation, unchanged: PUQ30 The corpus match is the one property that cannot be retrofitted later; trainer support can be worked around with more manual drilling. The two-Sofle QWERTY fallback (see the previous comment) converts the ecosystem gap from a risk into an inconvenience — the downside becomes "drilling is more manual", not "work stops". The strongest argument the other way is **Ctrl+Z/X/C/V**. If editor shortcut use is heavy, that is a real, permanent, daily cost that no amount of drilling removes, and it deserves weighing properly before committing. Either choice leaves the SYM row from `be1e7e6c` intact, since it is not part of the 30-key core. Once the call is made, phase 0 is the stripped starting keymap described in the §5 rewrite: target alphabet, mods on thumbs and outer keys as on the Sofle, `&mo` layers, one nav/number layer, no HRMs, no combos, `require-prior-idle-ms` parked at 350 for when they land in phase 4.
Author
Owner

§5 decided: train on the TOTEM directly. Plus three corrections to my own analysis.

Decision: option (b) — go straight to the TOTEM with the phase-0 stripped keymap. Both Sofles stay stock QWERTY. The alternative, learning the new alphabet on a Sofle first, was considered seriously and rejected; reasoning below, because it was closer than the outcome suggests.

Retraction 1 — Ctrl+Z/X/C/V is not a real argument

I listed it as the strongest case for Colemak-DH. It mostly dissolves under scrutiny.

The keymap has &hml LCTRL D and &hmr RCTRL K, both with hold-trigger-key-positions restricted to the opposite hand. So Ctrl+letter is always a cross-hand chord. On the Sofle it is same-hand — dedicated LCTRL on the outer column plus left-hand z x c v. The chord shape is therefore being relearned regardless of which alphabet is chosen; preserving the letter positions preserves one half of a gesture whose other half changes anyway.

What survives is minor: Colemak-DH keeps all four on the left hand, so all four use right Ctrl — consistent. PUQ30 has x on the left and z, c, v on the right, so x uses right Ctrl and the others use left. A small inconsistency, absorbed by feel within days.

Downgraded from "strongest counter-argument" to "minor."

hjkl — one half is safe, the other is lost on both layouts

Worth separating, because they behave completely differently:

  • Layer arrow keys are unaffected. &kp LEFT/DOWN/UP/RIGHT are not letters. They stay exactly where they are put, on any alphabet. Nothing at risk.
  • vim hjkl breaks on both candidates. Neither preserves the adjacency, and adjacency is the entire point:
h j k l
Colemak-DH R index, row3 R inner-idx, row1 R inner-idx, row3 R index, row1
PUQ30 L pinky, home R inner-idx, row3 L pinky, row3 R middle, row1

Colemak-DH crushes all four onto two fingers across two rows; PUQ30 splits them across both hands. Both unusable as-is. The difference is prior art only: Colemak users converged on n e i o (right home row) and there are configs to copy; on PUQ30 the equivalent would be t r n s, defined from scratch.

Third option, probably the best one: do not remap vim at all — use the nav layer's real arrows. Layout-independent, already built, and it sidesteps the plugin problem below entirely.

Remap effort is smaller than "vi, tmux and fish" implies

The distinction that shrinks it:

  • Letter-based bindings follow the letter — zero work. Ctrl+A, Ctrl+E, dd, yy, gg, ciw all still mean what they meant; the key simply lives elsewhere, and that is relearned as part of learning the alphabet. No config at all.
  • Position-based bindings break — only where physical adjacency was the point.

That reduces the actual work to:

effort
fish Zero on the default emacs bindings — all letter-based Ctrl chords. Only fish_vi_key_bindings lands in the vim case.
tmux ~15 min of bind lines, or use prefix + arrows, which already works.
vim/nvim Remap is ~10 lines. Real cost is plugins with their own keymaps (file trees, telescope, fugitive, which-key) that assume hjkl and ignore the remap. Afternoon for core config, then papercuts for a few weeks. Near zero if the arrow-layer route is taken instead.

Same on either layout; Colemak-DH just offers more to copy-paste.

Trainers — the ecosystem gap is smaller than assumed

  • Monkeytype needs nothing. The layout lives in firmware, so with the emulator off it already receives the correct characters. Adding a layout JSON (frontend/static/layouts/, rows 1–5, each key [unshifted, shifted, altgr, shift+altgr], plus the name in LayoutNameSchema) buys only the on-screen keymap display — a real early learning aid, about an hour's work, fork or PR.
  • keybr is self-hostable. Open source; layout definitions in packages/keybr-keyboard-generator/layout, npm run generate-layouts, then an entry in the Layout class. The hosted site has no custom-layout upload — issue #463 is that exact request, still open — so it means running a fork. Roughly half a day.
  • Cheaper route to what keybr is actually for. Its value is adaptive letter introduction. That can be approximated with no fork: a script filtering a German+English wordlist to words using only an allowed letter set, staged (start with the ten home-row letters, add two per stage), fed to Monkeytype custom text. Works today, covers both languages, and is layout-agnostic so it survives whichever way §4 goes. ~40 lines; can be built on request.

Net effect: the ecosystem argument for Colemak-DH is weaker than the earlier comparison claimed.

Retraction 2 — the AdNW family is not ruled out by the key count

The question was whether the other AdNW-adjacent layouts are all unusable at 30 keys. The answer splits:

The 4-row ISO layouts genuinely are a poor fit. Neo 2, Bone, AdNW, KOY and Noted were optimized across a full ISO board where ä ö ü (and ß for KOY) hold core keys. Extracting a 3×10 core evicts 3–4 slots and forces a letter out — f for AdNW (~1.7% of German), f+z for KOY, q for Bone, j for Noted, y for Neo 2. Worse, relocating a letter invalidates the published metrics, since the layout was optimized as a unit. Adopting one means adopting a modified layout with no numbers behind it.

But the family already produced 30-key members. PUQ30, PUO30 and OUMF30 come from the same optimizer and the same objective family, designed for exactly 30 keys with thumb shift. Those are the candidates.

OUMF30 is out: ~4.5% worse total effort (399.9 vs ~382) and the highest adjacent-finger load in both languages, bought in exchange for same-hand rolls over alternation (Handwechsel 64.6 vs 72.4). Only for a deliberate roll preference.

Retraction 3 — PUQ30 vs PUO30 is much closer than I presented

EN total DE total combined Lage EN Lage DE adjacent EN adjacent DE
PUQ30 197.2 185.4 382.5 167.0 157.1 7.7 11.8
PUO30 200.2 182.2 382.3 168.1 158.8 9.9 6.9

They tie on combined effort — 0.05% apart. PUQ30 wins English and position effort by ~1–1.5%. PUO30 wins German by ~1.7% and wins German adjacent-finger load by 42% (6.9 vs 11.8), which is the largest single gap anywhere in the table. The cause is precisely the i/e adjacency in PUQ30's h i e a o against PUO30's h i a e , — and ei/ie are pervasive in German.

Calling PUQ30 the clear pick overstated it.

The fact that de-risks this: PUQ30 and PUO30 have an identical right hand. Switching later is a left-hand-only relearn, roughly half a layout change. Not a one-way door.

Why the Sofle-first route lost

The proposal: learn the new alphabet on one Sofle first, move to the TOTEM once partly trained.

Two of my three objections were answered outright. The two Sofles are not identical — one is Cherry MX, one low-profile Choc, so the tactile cue is strong and hands know which board they are on; and carrying both means the QWERTY fallback travels too. Both objections withdrawn.

The third stood. The reason to train on a Sofle is that only the alphabet changes. The proposed fix for PUQ30 not fitting a 58-key row-staggered board was to strip the number row and outer column down to ~3×5+thumbs — but that changes the board as well, and lands on hardware that is still row-staggered, where PUQ30's ~70% home-row figure and travel numbers do not hold. That path pays for both approaches at once: a second keymap, a modified keyboard, and still not the TOTEM's geometry.

The deciding evidence is the NCSU study already cited here: split-board QWERTY typists were within 10% of baseline after ~20 trials, while changing the alphabet was dramatically slower. The board is the cheap variable. Separating it out buys little and costs an extra adaptation cycle.

Two coherent versions existed — Sofle fully stock with an alphabet-only substitution, or straight to the TOTEM. The middle path (a stripped Sofle) was the one to avoid. (b) chosen.

Where this leaves things

Settled: encoders irrelevant; mouse keys a future candidate; both Sofles stay stock QWERTY and travel together as the fallback; numbers on a layer accepted; TOTEM stays at a desk through phases 1–4; asymmetric friction — TOTEM centred, Sofle to the side.

Phase 0 is now the only blocker, and it needs the alphabet call: PUQ30 or PUO30. The deciding question is the German/English split in prose specifically, as opposed to code. If German prose dominates and English is mostly code and comments, PUO30's German advantage and much lower adjacent-finger load likely outweigh PUQ30's English edge. If genuinely balanced, PUQ30's better position effort in both languages breaks the tie.

Colemak-DH is now the outside option rather than the co-favourite: its ecosystem edge shrank once Monkeytype turned out to need nothing and keybr turned out to be self-hostable, and its Ctrl+ZXCV edge mostly dissolved.

## §5 decided: train on the TOTEM directly. Plus three corrections to my own analysis. **Decision: option (b) — go straight to the TOTEM with the phase-0 stripped keymap.** Both Sofles stay stock QWERTY. The alternative, learning the new alphabet on a Sofle first, was considered seriously and rejected; reasoning below, because it was closer than the outcome suggests. ### Retraction 1 — Ctrl+Z/X/C/V is not a real argument I listed it as the strongest case for Colemak-DH. It mostly dissolves under scrutiny. The keymap has `&hml LCTRL D` and `&hmr RCTRL K`, both with `hold-trigger-key-positions` restricted to the **opposite hand**. So Ctrl+letter is always a cross-hand chord. On the Sofle it is same-hand — dedicated `LCTRL` on the outer column plus left-hand `z x c v`. The chord shape is therefore being relearned regardless of which alphabet is chosen; preserving the letter positions preserves one half of a gesture whose other half changes anyway. What survives is minor: Colemak-DH keeps all four on the left hand, so all four use right Ctrl — consistent. PUQ30 has `x` on the left and `z`, `c`, `v` on the right, so `x` uses right Ctrl and the others use left. A small inconsistency, absorbed by feel within days. **Downgraded from "strongest counter-argument" to "minor."** ### hjkl — one half is safe, the other is lost on both layouts Worth separating, because they behave completely differently: - **Layer arrow keys are unaffected.** `&kp LEFT/DOWN/UP/RIGHT` are not letters. They stay exactly where they are put, on any alphabet. Nothing at risk. - **vim `hjkl` breaks on both candidates.** Neither preserves the adjacency, and adjacency is the entire point: | | h | j | k | l | |---|---|---|---|---| | Colemak-DH | R index, row3 | R inner-idx, row1 | R inner-idx, row3 | R index, row1 | | PUQ30 | **L** pinky, home | R inner-idx, row3 | **L** pinky, row3 | R middle, row1 | Colemak-DH crushes all four onto two fingers across two rows; PUQ30 splits them across both hands. Both unusable as-is. The difference is prior art only: Colemak users converged on `n e i o` (right home row) and there are configs to copy; on PUQ30 the equivalent would be `t r n s`, defined from scratch. **Third option, probably the best one:** do not remap vim at all — use the nav layer's real arrows. Layout-independent, already built, and it sidesteps the plugin problem below entirely. ### Remap effort is smaller than "vi, tmux and fish" implies The distinction that shrinks it: - **Letter-based bindings follow the letter — zero work.** `Ctrl+A`, `Ctrl+E`, `dd`, `yy`, `gg`, `ciw` all still mean what they meant; the key simply lives elsewhere, and that is relearned as part of learning the alphabet. No config at all. - **Position-based bindings break** — only where physical adjacency was the point. That reduces the actual work to: | | effort | |---|---| | fish | **Zero** on the default emacs bindings — all letter-based Ctrl chords. Only `fish_vi_key_bindings` lands in the vim case. | | tmux | ~15 min of `bind` lines, or use `prefix + arrows`, which already works. | | vim/nvim | Remap is ~10 lines. Real cost is **plugins with their own keymaps** (file trees, telescope, fugitive, which-key) that assume hjkl and ignore the remap. Afternoon for core config, then papercuts for a few weeks. Near zero if the arrow-layer route is taken instead. | Same on either layout; Colemak-DH just offers more to copy-paste. ### Trainers — the ecosystem gap is smaller than assumed - **Monkeytype needs nothing.** The layout lives in firmware, so with the emulator off it already receives the correct characters. Adding a layout JSON (`frontend/static/layouts/`, rows 1–5, each key `[unshifted, shifted, altgr, shift+altgr]`, plus the name in `LayoutNameSchema`) buys only the on-screen keymap display — a real early learning aid, about an hour's work, fork or PR. - **keybr is self-hostable.** Open source; layout definitions in `packages/keybr-keyboard-generator/layout`, `npm run generate-layouts`, then an entry in the `Layout` class. The hosted site has no custom-layout upload — [issue #463](https://github.com/aradzie/keybr.com/issues/463) is that exact request, still open — so it means running a fork. Roughly half a day. - **Cheaper route to what keybr is actually for.** Its value is adaptive letter introduction. That can be approximated with no fork: a script filtering a German+English wordlist to words using only an allowed letter set, staged (start with the ten home-row letters, add two per stage), fed to **Monkeytype custom text**. Works today, covers both languages, and is layout-agnostic so it survives whichever way §4 goes. ~40 lines; can be built on request. **Net effect: the ecosystem argument for Colemak-DH is weaker than the earlier comparison claimed.** ### Retraction 2 — the AdNW family is not ruled out by the key count The question was whether the other AdNW-adjacent layouts are all unusable at 30 keys. The answer splits: **The 4-row ISO layouts genuinely are a poor fit.** Neo 2, Bone, AdNW, KOY and Noted were optimized across a full ISO board where `ä ö ü` (and `ß` for KOY) hold core keys. Extracting a 3×10 core evicts 3–4 slots and forces a letter out — `f` for AdNW (~1.7% of German), `f`+`z` for KOY, `q` for Bone, `j` for Noted, `y` for Neo 2. Worse, relocating a letter **invalidates the published metrics**, since the layout was optimized as a unit. Adopting one means adopting a modified layout with no numbers behind it. **But the family already produced 30-key members.** PUQ30, PUO30 and OUMF30 come from the same optimizer and the same objective family, designed for exactly 30 keys with thumb shift. Those are the candidates. **OUMF30 is out:** ~4.5% worse total effort (399.9 vs ~382) and the highest adjacent-finger load in both languages, bought in exchange for same-hand rolls over alternation (Handwechsel 64.6 vs 72.4). Only for a deliberate roll preference. ### Retraction 3 — PUQ30 vs PUO30 is much closer than I presented | | EN total | DE total | combined | Lage EN | Lage DE | adjacent EN | adjacent DE | |---|---|---|---|---|---|---|---| | PUQ30 | **197.2** | 185.4 | 382.5 | **167.0** | **157.1** | **7.7** | 11.8 | | PUO30 | 200.2 | **182.2** | **382.3** | 168.1 | 158.8 | 9.9 | **6.9** | They **tie on combined effort** — 0.05% apart. PUQ30 wins English and position effort by ~1–1.5%. PUO30 wins German by ~1.7% and wins German adjacent-finger load by **42%** (6.9 vs 11.8), which is the largest single gap anywhere in the table. The cause is precisely the `i`/`e` adjacency in PUQ30's `h i e a o` against PUO30's `h i a e ,` — and `ei`/`ie` are pervasive in German. Calling PUQ30 the clear pick overstated it. **The fact that de-risks this:** PUQ30 and PUO30 have an **identical right hand**. Switching later is a left-hand-only relearn, roughly half a layout change. Not a one-way door. ### Why the Sofle-first route lost The proposal: learn the new alphabet on one Sofle first, move to the TOTEM once partly trained. Two of my three objections were answered outright. The two Sofles are **not** identical — one is Cherry MX, one low-profile Choc, so the tactile cue is strong and hands know which board they are on; and carrying both means the QWERTY fallback travels too. Both objections withdrawn. The third stood. The reason to train on a Sofle is that *only the alphabet changes*. The proposed fix for PUQ30 not fitting a 58-key row-staggered board was to strip the number row and outer column down to ~3×5+thumbs — but that changes the board as well, and lands on hardware that is still **row-staggered**, where PUQ30's ~70% home-row figure and travel numbers do not hold. That path pays for both approaches at once: a second keymap, a modified keyboard, and still not the TOTEM's geometry. The deciding evidence is the [NCSU study](https://repository.lib.ncsu.edu/items/4a7a11aa-30be-40dd-bd9f-88ba6ef44445) already cited here: split-board QWERTY typists were within 10% of baseline after ~20 trials, while changing the alphabet was dramatically slower. **The board is the cheap variable.** Separating it out buys little and costs an extra adaptation cycle. Two coherent versions existed — Sofle fully stock with an alphabet-only substitution, or straight to the TOTEM. The middle path (a stripped Sofle) was the one to avoid. **(b) chosen.** ### Where this leaves things Settled: encoders irrelevant; mouse keys a future candidate; both Sofles stay stock QWERTY and travel together as the fallback; numbers on a layer accepted; TOTEM stays at a desk through phases 1–4; asymmetric friction — TOTEM centred, Sofle to the side. **Phase 0 is now the only blocker, and it needs the alphabet call: PUQ30 or PUO30.** The deciding question is the German/English split in *prose* specifically, as opposed to code. If German prose dominates and English is mostly code and comments, PUO30's German advantage and much lower adjacent-finger load likely outweigh PUQ30's English edge. If genuinely balanced, PUQ30's better position effort in both languages breaks the tie. Colemak-DH is now the outside option rather than the co-favourite: its ecosystem edge shrank once Monkeytype turned out to need nothing and keybr turned out to be self-hostable, and its Ctrl+ZXCV edge mostly dissolved.
Author
Owner

§4 scored: all candidates in one model, with a crossover

tools/layout_score.py (8c45c8fd). The comparison that never existed — PUQ30, PUO30, OUMF30, Colemak-DH and QWERTY on the same axes, against a weighted corpus, swept across the German share of prose.

Corpus

Component Weight Source Real or proxy
code 30% OSS Kotlin (ktor, kotlinx.coroutines) + Java (guava) as minority proxy — work code inaccessible
CLI 10% atuin history, 8040 commands, 452k chars, secret-bearing lines filtered real
English prose swept Claude Code transcripts, 10,842 messages, 693k chars, median 47 chars real, and the exact register
German prose swept dariogoetz deu_mixed_1m ngram tables (1M-token Leipzig-derived) proxy

Kotlin-weighted over Java deliberately. Nothing sensitive is retained — the script reduces everything to frequency counts.

Scored on the 26 letters plus , and . — the subset every candidate holds in core — so no layout is flattered by simply lacking a character. What each pushes onto a layer is reported as cover%.

Calibration check: QWERTY lands at 7.0–8.6% SFB and Colemak-DH at 2.36% for pure English. Published figures are ~6.5% and ~1.4–1.9% for prose alone; ours run higher because 40% of the corpus is code and CLI, which are SFB-dense. Directionally right, so the model is behaving.

Results

=== German share of prose: 0% ===
layout         SFB%  adjac%  sciss%   LSB%   alt%  home%  cover%
puq30          2.23    8.75    0.73   1.53  70.08  66.17   98.25
puo30          2.38   10.48    0.94   0.87  70.08  59.74   98.25
oumf30         2.50   15.69    0.90   2.90  62.47  61.30   98.25
colemak-dh     2.36   15.97    0.27   2.15  58.23  64.01   99.75
qwerty         7.04   21.78    5.79   6.65  50.33  30.73   99.75

=== German share of prose: 50% ===
puq30          1.96    9.92    0.68   1.63  69.94  67.70   98.74
puo30          2.09    9.97    0.80   1.06  69.94  62.69   98.74
oumf30         2.59   15.49    0.77   3.40  62.04  64.65   98.74
colemak-dh     2.75   18.24    0.32   2.27  57.08  65.22   99.82
qwerty         7.82   22.82    5.75   7.65  50.50  30.51   99.82

=== German share of prose: 80% ===
puq30          1.80   10.62    0.65   1.69  69.86  68.78   99.03
puo30          1.91    9.66    0.71   1.17  69.86  64.44   99.03
oumf30         2.65   15.38    0.69   3.71  61.78  66.64   99.03
colemak-dh     2.99   19.61    0.35   2.35  56.40  65.94   99.86

Colemak-DH is out, and for a reason the earlier comparison missed

Adjacent-finger load of 16–20%, roughly double the AdNW-family layouts. And it degrades as German rises — SFB 2.36 → 3.14, adjacent 15.97 → 20.51 from 0% to 100% German. That is what an English-optimized layout does when it meets German. It also has the lowest hand alternation (58% → 56%).

It wins two things: scissors (0.27–0.38 against ~0.7) and coverage (99.8% vs 98.7%, from holding ; and / in core). Neither offsets double the adjacent-finger load.

OUMF30 is confirmed out — worst adjacent load among the three AdNW variants and the lowest alternation, as its documentation implies.

The crossover, and it is real

PUQ30 and PUO30 trade places on adjacent-finger load at ~52–55% German prose:

German share PUQ30 adjac% PUO30 adjac%
0% 8.75 10.48
40% 9.69 10.07
50% 9.92 9.97
60% 10.15 9.87
80% 10.62 9.66

This independently confirms the Wettstein benachbart signal (11.8 vs 6.9 for German) in a completely different model. The i/e adjacency in h i e a o is real and it does cost, in German specifically.

But PUQ30 wins everything else, at every share

Metric @ 50% German PUQ30 PUO30
SFB% 1.96 2.09
home row% 67.70 62.69
scissors% 0.68 0.80
LSB% 1.63 1.06

Finger load, 50/50 prose (L4 = left pinky … R4 = right pinky):

layout           L4     L3     L2     L1     R1     R2     R3     R4
puq30           7.1   11.5   15.1   13.5   19.8   12.2   11.9    9.0
puo30           7.1   11.5   12.6   16.0   19.8   12.2   11.9    9.0
colemak-dh      7.0    8.5   12.1   19.5   19.1   16.6   10.3    6.8
qwerty          7.0    8.1   20.3   22.0   17.9    9.1   11.8    3.7

PUQ30 spreads the left hand better; PUO30 pushes 2.5pp more onto the left index. Right hands are identical, as expected — they are the same.

The sizes matter here. PUQ30's home-row advantage is 5 percentage points and holds at every German share. PUO30's adjacent-finger advantage is 0.3pp at 60% German and 1.0pp at 80%. The persistent 5pp beats the conditional 1pp.

Verdict: PUQ30

Now on evidence rather than on a reading of a PDF. It wins SFB, home row, scissors and finger balance at every plausible corpus mix, and only loses adjacent-finger load above ~55% German — by a margin far smaller than what it wins elsewhere.

If you later find adjacent-finger strain is the thing that actually bothers you, PUO30 remains a left-hand-only relearn, since the right hands are identical.

Drill list, as a by-product

Worst bigrams on PUQ30 at 50/50 prose, weighted by frequency and row distance — this is what ngram practice should target instead of generic language ngrams:

sfb:      ct  e.  ui  tc  gt  rl  ph  mb  wr  rw  kp  cd  dc  oa  tg
scissor:  bl  yp  zm  py  ku

e. and oa are the ones that will bite daily — sentence endings after e, and o/a sharing the left index.

Limitations, stated

  • German and code are proxies. German letter statistics are stable across writers so that one is low-risk; the code sample is idiomatic OSS Kotlin rather than your work code.
  • The 30/10/60 code/CLI/prose weighting is assumed, not measured.
  • The model is oxeylyzer-style: no column-stagger geometry, no thumb keys, no shift handling, case folded. It compares layouts against each other; the absolute numbers are not publishable figures.

Phase 0 is unblocked. Say the word and I build the stripped PUQ30 keymap.

## §4 scored: all candidates in one model, with a crossover `tools/layout_score.py` (`8c45c8fd`). The comparison that never existed — PUQ30, PUO30, OUMF30, Colemak-DH and QWERTY on the same axes, against a weighted corpus, swept across the German share of prose. ### Corpus | Component | Weight | Source | Real or proxy | |---|---|---|---| | code | 30% | OSS Kotlin (ktor, kotlinx.coroutines) + Java (guava) as minority | proxy — work code inaccessible | | CLI | 10% | atuin history, 8040 commands, 452k chars, secret-bearing lines filtered | **real** | | English prose | swept | Claude Code transcripts, 10,842 messages, 693k chars, median 47 chars | **real**, and the exact register | | German prose | swept | dariogoetz `deu_mixed_1m` ngram tables (1M-token Leipzig-derived) | proxy | Kotlin-weighted over Java deliberately. Nothing sensitive is retained — the script reduces everything to frequency counts. Scored on the 26 letters plus `,` and `.` — the subset every candidate holds in core — so no layout is flattered by simply lacking a character. What each pushes onto a layer is reported as `cover%`. **Calibration check:** QWERTY lands at 7.0–8.6% SFB and Colemak-DH at 2.36% for pure English. Published figures are ~6.5% and ~1.4–1.9% for prose alone; ours run higher because 40% of the corpus is code and CLI, which are SFB-dense. Directionally right, so the model is behaving. ### Results ``` === German share of prose: 0% === layout SFB% adjac% sciss% LSB% alt% home% cover% puq30 2.23 8.75 0.73 1.53 70.08 66.17 98.25 puo30 2.38 10.48 0.94 0.87 70.08 59.74 98.25 oumf30 2.50 15.69 0.90 2.90 62.47 61.30 98.25 colemak-dh 2.36 15.97 0.27 2.15 58.23 64.01 99.75 qwerty 7.04 21.78 5.79 6.65 50.33 30.73 99.75 === German share of prose: 50% === puq30 1.96 9.92 0.68 1.63 69.94 67.70 98.74 puo30 2.09 9.97 0.80 1.06 69.94 62.69 98.74 oumf30 2.59 15.49 0.77 3.40 62.04 64.65 98.74 colemak-dh 2.75 18.24 0.32 2.27 57.08 65.22 99.82 qwerty 7.82 22.82 5.75 7.65 50.50 30.51 99.82 === German share of prose: 80% === puq30 1.80 10.62 0.65 1.69 69.86 68.78 99.03 puo30 1.91 9.66 0.71 1.17 69.86 64.44 99.03 oumf30 2.65 15.38 0.69 3.71 61.78 66.64 99.03 colemak-dh 2.99 19.61 0.35 2.35 56.40 65.94 99.86 ``` ### Colemak-DH is out, and for a reason the earlier comparison missed Adjacent-finger load of **16–20%**, roughly double the AdNW-family layouts. And it **degrades as German rises** — SFB 2.36 → 3.14, adjacent 15.97 → 20.51 from 0% to 100% German. That is what an English-optimized layout does when it meets German. It also has the lowest hand alternation (58% → 56%). It wins two things: scissors (0.27–0.38 against ~0.7) and coverage (99.8% vs 98.7%, from holding `;` and `/` in core). Neither offsets double the adjacent-finger load. OUMF30 is confirmed out — worst adjacent load among the three AdNW variants and the lowest alternation, as its documentation implies. ### The crossover, and it is real **PUQ30 and PUO30 trade places on adjacent-finger load at ~52–55% German prose:** | German share | PUQ30 adjac% | PUO30 adjac% | |---|---|---| | 0% | **8.75** | 10.48 | | 40% | **9.69** | 10.07 | | 50% | **9.92** | 9.97 | | 60% | 10.15 | **9.87** | | 80% | 10.62 | **9.66** | This **independently confirms the Wettstein `benachbart` signal** (11.8 vs 6.9 for German) in a completely different model. The `i`/`e` adjacency in `h i e a o` is real and it does cost, in German specifically. ### But PUQ30 wins everything else, at every share | Metric @ 50% German | PUQ30 | PUO30 | |---|---|---| | SFB% | **1.96** | 2.09 | | home row% | **67.70** | 62.69 | | scissors% | **0.68** | 0.80 | | LSB% | 1.63 | **1.06** | Finger load, 50/50 prose (L4 = left pinky … R4 = right pinky): ``` layout L4 L3 L2 L1 R1 R2 R3 R4 puq30 7.1 11.5 15.1 13.5 19.8 12.2 11.9 9.0 puo30 7.1 11.5 12.6 16.0 19.8 12.2 11.9 9.0 colemak-dh 7.0 8.5 12.1 19.5 19.1 16.6 10.3 6.8 qwerty 7.0 8.1 20.3 22.0 17.9 9.1 11.8 3.7 ``` PUQ30 spreads the left hand better; PUO30 pushes 2.5pp more onto the left index. Right hands are identical, as expected — they are the same. **The sizes matter here.** PUQ30's home-row advantage is **5 percentage points** and holds at every German share. PUO30's adjacent-finger advantage is 0.3pp at 60% German and 1.0pp at 80%. The persistent 5pp beats the conditional 1pp. ### Verdict: PUQ30 Now on evidence rather than on a reading of a PDF. It wins SFB, home row, scissors and finger balance at every plausible corpus mix, and only loses adjacent-finger load above ~55% German — by a margin far smaller than what it wins elsewhere. If you later find adjacent-finger strain is the thing that actually bothers you, PUO30 remains a **left-hand-only relearn**, since the right hands are identical. ### Drill list, as a by-product Worst bigrams on PUQ30 at 50/50 prose, weighted by frequency and row distance — this is what ngram practice should target instead of generic language ngrams: ``` sfb: ct e. ui tc gt rl ph mb wr rw kp cd dc oa tg scissor: bl yp zm py ku ``` `e.` and `oa` are the ones that will bite daily — sentence endings after `e`, and `o`/`a` sharing the left index. ### Limitations, stated - German and code are **proxies**. German letter statistics are stable across writers so that one is low-risk; the code sample is idiomatic OSS Kotlin rather than your work code. - The 30/10/60 code/CLI/prose weighting is assumed, not measured. - The model is oxeylyzer-style: no column-stagger geometry, no thumb keys, no shift handling, case folded. It compares layouts against each other; the absolute numbers are not publishable figures. Phase 0 is unblocked. Say the word and I build the stripped PUQ30 keymap.
Author
Owner

Phase 0 built: PUQ30 on a stripped keymap — and a binding that never worked

totem-dongle at 37623fb9. All five images build; drawing regenerated.

A bug found while placing the GER layer, in both repos

&kp RA(5) — the € binding — sends nothing at all. Not a wrong character: nothing.

RA(x) expands to APPLY_MODS(MOD_RALT, x), which sets modifier bits in the top byte and leaves the rest untouched. keys.h defines N5 as ZMK_HID_USAGE(HID_USAGE_KEY, HID_USAGE_KEY_KEYBOARD_5_AND_PERCENT), but a bare 5 is not defined anywhere — it stays the literal integer 5, giving usage page 0. And zmk_hid_press in app/src/hid.c:

int zmk_hid_press(uint32_t usage) {
    switch (ZMK_HID_USAGE_PAGE(usage)) {
    case HID_USAGE_KEY:      return zmk_hid_keyboard_press(ZMK_HID_USAGE_ID(usage));
    case HID_USAGE_CONSUMER: return zmk_hid_consumer_press(ZMK_HID_USAGE_ID(usage));
    }
    return -EINVAL;
}

Page 0 matches neither branch, so it returns -EINVAL and no keycode is emitted.

RA(Q), RA(S), RA(Y) and RA(P) were always fine — those letters are real ZMK_HID_USAGE macros. Only the numeric one is affected. Correct form is RA(N5), which is what phase 0 uses.

ma3s7r0/zmk-sofle-eyelash-config has the identical bug in layer_1 and needs the same one-character fix in its own repo. Worth a sanity check on the daily driver: does AltGr+5 currently produce or nothing?

The keymap

p  u  ¨  ,  q      g  c  l  m  f
h  i  e  a  o      d  t  r  n  s
k  y  .  '  x      j  v  w  b  z

with LCTRL and LGUI on the two outer keys, and thumbs LALT / NAV / SPACE | BSPC / LSHFT / RET.

Three layers only: BASE, NAV, GER.

  • No home-row mods, no sticky layers, no combos, no &mt, no &lt. The Sofle baseline has none of these — not one hold-tap anywhere — so there is nothing to build on and they all arrive later. Mods are plain keys on thumbs and outer keys, which is where the Sofle keeps them.
  • NAV is momentary &mo, held with the left thumb, so the numpad goes on the free right hand and the cursor cluster on the left.
  • Numbers are plain N0N9, never the KP_ variants — those depend on a NumLock nothing here sets.
  • The ¨ slot keeps its PUQ30 position but becomes &mo GER. ZMK cannot reproduce the upstream postfix behaviour, where the key follows the letter it modifies, so it is a prefix hold instead. Umlauts sit on the free right hand ordered by German frequency onto the stronger fingers: € ü ä ö ß.
  • &studio_unlock, &bootloader and &bt BT_CLR are on NAV, since there is no ADJ layer any more and Studio locking is still at its default.

No symbol layer. That is phase 2, and real work stays on the Sofle until then, so drills lose nothing by it. Esc and Tab living on NAV is the same trade.

The old QWERTY keymap is not lost — it is the destination, reached in stages, and sits at cd10b8c3.

What lands when

Phase Added
2 symbol layer on the free right thumb
4 home-row mods — shift on the index fingers first, then outward; require-prior-idle-ms starting at 350 and walked down, never the 150 that suited an already-fast typist
5+ combos, then &mt / &lt, last of all

Also in this push

tools/layout_score.py (47567ec3) — the scorer from the previous comment, as its own commit.

Next

Flash totem_prospector.uf2 to the desk dongle and totem_left/totem_right to the halves, then pair left first, then right. Phase 1 is 2×15 min drills a day with everything real staying on the Sofle.

Open, and worth deciding before drilling starts: whether to build the staged-wordlist generator for Monkeytype custom text. The drill list from the scorer is ready either way —

sfb:      ct  e.  ui  tc  gt  rl  ph  mb  wr  rw  kp  cd  dc  oa  tg
scissor:  bl  yp  zm  py  ku
## Phase 0 built: PUQ30 on a stripped keymap — and a binding that never worked `totem-dongle` at `37623fb9`. All five images build; drawing regenerated. ### A bug found while placing the GER layer, in **both** repos `&kp RA(5)` — the € binding — **sends nothing at all.** Not a wrong character: nothing. `RA(x)` expands to `APPLY_MODS(MOD_RALT, x)`, which sets modifier bits in the top byte and leaves the rest untouched. `keys.h` defines `N5` as `ZMK_HID_USAGE(HID_USAGE_KEY, HID_USAGE_KEY_KEYBOARD_5_AND_PERCENT)`, but a **bare `5` is not defined anywhere** — it stays the literal integer 5, giving usage page 0. And `zmk_hid_press` in `app/src/hid.c`: ```c int zmk_hid_press(uint32_t usage) { switch (ZMK_HID_USAGE_PAGE(usage)) { case HID_USAGE_KEY: return zmk_hid_keyboard_press(ZMK_HID_USAGE_ID(usage)); case HID_USAGE_CONSUMER: return zmk_hid_consumer_press(ZMK_HID_USAGE_ID(usage)); } return -EINVAL; } ``` Page 0 matches neither branch, so it returns `-EINVAL` and no keycode is emitted. `RA(Q)`, `RA(S)`, `RA(Y)` and `RA(P)` were always fine — those letters are real `ZMK_HID_USAGE` macros. Only the numeric one is affected. Correct form is **`RA(N5)`**, which is what phase 0 uses. **`ma3s7r0/zmk-sofle-eyelash-config` has the identical bug** in `layer_1` and needs the same one-character fix in its own repo. Worth a sanity check on the daily driver: does AltGr+5 currently produce `€` or nothing? ### The keymap ``` p u ¨ , q g c l m f h i e a o d t r n s k y . ' x j v w b z ``` with `LCTRL` and `LGUI` on the two outer keys, and thumbs `LALT / NAV / SPACE | BSPC / LSHFT / RET`. Three layers only: **BASE**, **NAV**, **GER**. - **No home-row mods, no sticky layers, no combos, no `&mt`, no `&lt`.** The Sofle baseline has none of these — not one hold-tap anywhere — so there is nothing to build on and they all arrive later. Mods are plain keys on thumbs and outer keys, which is where the Sofle keeps them. - **NAV is momentary `&mo`**, held with the left thumb, so the numpad goes on the free right hand and the cursor cluster on the left. - **Numbers are plain `N0`–`N9`**, never the `KP_` variants — those depend on a NumLock nothing here sets. - **The `¨` slot keeps its PUQ30 position** but becomes `&mo GER`. ZMK cannot reproduce the upstream postfix behaviour, where the key follows the letter it modifies, so it is a prefix hold instead. Umlauts sit on the free right hand ordered by German frequency onto the stronger fingers: `€ ü ä ö ß`. - **`&studio_unlock`, `&bootloader` and `&bt BT_CLR`** are on NAV, since there is no ADJ layer any more and Studio locking is still at its default. **No symbol layer.** That is phase 2, and real work stays on the Sofle until then, so drills lose nothing by it. Esc and Tab living on NAV is the same trade. The old QWERTY keymap is not lost — it is the *destination*, reached in stages, and sits at `cd10b8c3`. ### What lands when | Phase | Added | |---|---| | 2 | symbol layer on the free right thumb | | 4 | home-row mods — shift on the index fingers first, then outward; `require-prior-idle-ms` starting at **350** and walked down, never the 150 that suited an already-fast typist | | 5+ | combos, then `&mt` / `&lt`, last of all | ### Also in this push `tools/layout_score.py` (`47567ec3`) — the scorer from the previous comment, as its own commit. ### Next Flash `totem_prospector.uf2` to the desk dongle and `totem_left`/`totem_right` to the halves, then pair **left first, then right**. Phase 1 is 2×15 min drills a day with everything real staying on the Sofle. Open, and worth deciding before drilling starts: whether to build the staged-wordlist generator for Monkeytype custom text. The drill list from the scorer is ready either way — ``` sfb: ct e. ui tc gt rl ph mb wr rw kp cd dc oa tg scissor: bl yp zm py ku ```
Author
Owner

Resolved: niri now sets the altgr-intl variant

The open blocker from this thread — niri not carrying the host layout the GER
layer depends on — is fixed. ma3s7r0/nix-config commit 19775ac, 2026-08-27,
home-manager/niri/default.nix:

input.keyboard.xkb = {
  layout = "us";
  variant = "altgr-intl";
};

The cause was that niri does not read services.xserver.xkb, so
modules/nixos/keyboard.nix's us + altgr-intl + lv3:ralt_switch never
reached the Wayland session. Plain us has no level-3 symbols and leaves
RightAlt as Alt_R, so the whole GER layer emitted Alt+letter, which
applications take as a shortcut rather than a character.

Two details worth keeping, both verified against xkeyboard-config 2.47
symbols/us:

  • No lv3:ralt_switch option is needed. us(intl) supplies adiaeresis,
    ssharp, udiaeresis, odiaeresis and EuroSign at level 3 on q/s/y/p/5,
    and altgr-intl adds include "level3(ralt_switch)" itself — the variant
    carries the level-3 shift. This matches what the repo README already claims.
  • altgr-intl, not intl. intl leaves dead_acute, dead_diaeresis,
    dead_grave, dead_tilde and dead_circumflex at levels 1–2, which would
    stop ', ", `, ~ and ^ from being ordinary characters — bad on a
    keymap where ' is a base-layer alpha key. altgr-intl exists precisely to
    move those to level 3.

The same commit also dropped the ua and ru layouts that came in with the
imported niri config, and grp:alt_shift_toggle with them. That toggle was its
own hazard here: once home-row mods arrive in phase 4, Alt and Shift sit on the
home row, and a mistimed roll could silently switch the host to Ukrainian or
Russian. Generated config.kdl checked with niri validate.

Still open, and not covered by this

The Sofle's &kp RA(5) for remains broken there. A bare 5 is not a defined
usage macro in keys.h, so it yields usage page 0 and zmk_hid_press rejects it
— nothing is sent, regardless of how correct the host layout now is. The TOTEM
was fixed to RA(N5); ma3s7r0/zmk-sofle was not.

## Resolved: niri now sets the `altgr-intl` variant The open blocker from this thread — niri not carrying the host layout the GER layer depends on — is fixed. `ma3s7r0/nix-config` commit `19775ac`, 2026-08-27, `home-manager/niri/default.nix`: ```nix input.keyboard.xkb = { layout = "us"; variant = "altgr-intl"; }; ``` The cause was that niri does not read `services.xserver.xkb`, so `modules/nixos/keyboard.nix`'s `us` + `altgr-intl` + `lv3:ralt_switch` never reached the Wayland session. Plain `us` has no level-3 symbols and leaves RightAlt as `Alt_R`, so the whole GER layer emitted Alt+letter, which applications take as a shortcut rather than a character. Two details worth keeping, both verified against xkeyboard-config 2.47 `symbols/us`: - **No `lv3:ralt_switch` option is needed.** `us(intl)` supplies `adiaeresis`, `ssharp`, `udiaeresis`, `odiaeresis` and `EuroSign` at level 3 on `q/s/y/p/5`, and `altgr-intl` adds `include "level3(ralt_switch)"` itself — the variant carries the level-3 shift. This matches what the repo README already claims. - **`altgr-intl`, not `intl`.** `intl` leaves `dead_acute`, `dead_diaeresis`, `dead_grave`, `dead_tilde` and `dead_circumflex` at levels 1–2, which would stop `'`, `"`, `` ` ``, `~` and `^` from being ordinary characters — bad on a keymap where `'` is a base-layer alpha key. `altgr-intl` exists precisely to move those to level 3. The same commit also dropped the `ua` and `ru` layouts that came in with the imported niri config, and `grp:alt_shift_toggle` with them. That toggle was its own hazard here: once home-row mods arrive in phase 4, Alt and Shift sit on the home row, and a mistimed roll could silently switch the host to Ukrainian or Russian. Generated `config.kdl` checked with `niri validate`. ### Still open, and *not* covered by this The Sofle's `&kp RA(5)` for `€` remains broken there. A bare `5` is not a defined usage macro in `keys.h`, so it yields usage page 0 and `zmk_hid_press` rejects it — nothing is sent, regardless of how correct the host layout now is. The TOTEM was fixed to `RA(N5)`; `ma3s7r0/zmk-sofle` was not.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
ma3s7r0/zmk-config-dongle-totem#2
No description provided.