+
+
+Hands Down Promethium — full layer map
+
+
+
+
Hands Down · Promethium — full layer map
+
Canonical moutis HD system (github.com/moutis/HandsDown), rendered for a 3×6+3 Corne.
+Seven layers total; the six functional ones are shown (QWERTY is just a plain-QWERTY compatibility fallback).
+Small top-right badge = hold behavior (mod or layer). Layers are reached by holding a thumb/home key on the alpha layer.
+
+
+ alpha / letter
+ mod (hold)
+ layer-tap / switch
+ semantic key (SK_*)
+ symbol
+ number / nav
+ media
+ RGB / config
+ ▽ transparent
+
+
+
+
+
How to reach each layer from the alpha layer (all momentary, via layer-tap holds):
+SYM = hold Space · NAV = hold R (L-thumb) or Enter (R-thumb) · FUN = hold Bksp (L-thumb) or F · NUM = hold K · CFG = hold Mute/Play (outer thumbs), or the CFG combo (both inner-thumb pairs).
+Home-row mods on the alpha & FUN/NUM layers: pinky=⌃ ring=⌥ middle=⌘ index=⇧ (both hands).
+
+
+
+
diff --git a/users/raphaelmor/HDPM-parity.md b/users/raphaelmor/HDPM-parity.md
new file mode 100644
index 00000000..ae16042a
--- /dev/null
+++ b/users/raphaelmor/HDPM-parity.md
@@ -0,0 +1,242 @@
+# Hands Down Promethium — feature parity checklist
+
+Exhaustive list of everything the **canonical** moutis Hands Down repo supports for the
+**Promethium (pm)** variant, for porting into this firmware. Sourced from
+`../HandsDown/handsdown/pm-adaptive.c`, `../HandsDown/handsdown/pm-config.h`,
+`../HandsDown/moutis_combo_def.c`, `../HandsDown/moutis.h`. Updated against **main @ `a46d847`** (SemKeys-refactor / QWERTY-optional era).
+
+Legend: `[x]` already in this firmware · `[ ]` not yet · **(hold)** = distinct action when held past `COMBO_HOLD`/`LINGER_TIME`.
+
+> **Status (implemented).** Adaptives: **complete PM set incl. the 3-key rules** via `preprior` tracking,
+> gated to the Hands Down base (`ramo_adaptive.c`). **ADAPT_SHIFT** (comma-leader one-shot Shift), HDP-only.
+> Combos: `qu`/`z`/`th` + Shift-Tab + App-menu + diacritics/ligatures + `; : ! ?`, all at canonical positions,
+> HDP-gated (`ramo_combos.c`). Linger brackets `( {` + smart caps-word + Shift+Bksp→Del (`ramo_linger.c`,
+> `ramo_process_record.c`). Option-A thumbs; HD outer columns blanked. Mac-only → Semantic-Keys engine
+> intentionally **not** ported.
+
+> **Porting note.** Our HDP base now uses moutis's **canonical arrangement** (V W G M J top-left,
+> `# . / " '` top-right, F P D L X bottom-left, `- U O Y B` bottom-right), so combos map to his exact
+> positions. Kept customizations: GACS home-row mods, Hyper/Meh on fixed top positions (G/M, ./,/),
+> blanked outer pinky columns, Option-A thumbs (Colemak-aligned). Home rows match canonical (`S N T H K` / `, A E I C`).
+
+Canonical PM reference layout (positions → letters):
+```
+ V W G M J #$ .: /* "[ ']
+ S N T H K () ,; A E I C Z(RM5)
+ F P D L X [ ] -+ U O Y B Qu(RB5)
+ bsp R spc ret
+```
+
+---
+
+## 1. Adaptive keys (`pm-adaptive.c`)
+
+Fire only if the previous key was within `ADAPTIVE_TERM` (rolling window). All eliminate an
+SFB/scissor by rewriting the roll. "→ del X" = backspaces the already-typed key first.
+
+### Left hand
+- [x] **P D → PWD** — on `D` after `P`: insert `W` (then D). *(simplified from the old 3-key P·B·D in the refactor)*
+- [x] **P F → PS** — on `F` after `P`: type `S` instead of F
+- [x] **K G → KL** — on `G` after `K`: type `L` instead of G
+- [x] **W M G → WML** — on `G` after `W,M`: type `L` instead of G *(3-key, via preprior)*
+- [x] **M G → LG** — on `G` after `M`: del M, type `L`, then G
+- [x] **J G → JPG** — on `G` after `J`: insert `P`, then G
+- [x] **W G → WD** — on `G` after `W`: type `D` instead of G
+- [x] **K H → KN** — on `H` after `K`: type `N` instead of H
+- [x] **G J → G+"th"** — on `J` after `G`: type `th` (for "length")
+- [x] **V J / W J → …L** — on `J` after `W`: type `L` instead of J. *(`V J` **removed** — V+J is the `˜` tilde combo)*
+- [x] **M K → LK** — on `K` after `M`: del M, type `L`, then K
+- [x] **H K → NK** — on `K` after `H`: del H, type `N`, then K
+- [ ] **P/B/S then L → unshifted L** — skipped by choice (low value on this HDP arrangement)
+- [x] **G M → GL** — on `M` after `G`: type `L` instead of M
+- [x] **V M → VL** — on `M` after `V`: type `L` instead of M
+- [ ] **M W M → …L** — **removed**: unreachable, W+M is the `qu` combo (see below)
+- [ ] **W M → "lm"** — **removed**: unreachable, W+M is the `qu` combo. Combos resolve in
+ `pre_process_record_quantum`, *before* the adaptive engine runs — a roll fast enough for the
+ adaptive is also inside `COMBO_TERM`, so `qu` always won. Kept the combo; `Q` needs a home.
+- [x] **F P → SP** — on `P` after `F`: del F, type `S`, then P
+- [x] **G V → GT** — on `V` after `G`: type `T` instead of V
+- [x] **M V → LV** — on `V` after `M`: del M, type `L`, then V
+- [x] **G W → GD** — on `W` after `G`: type `D` instead of W
+- [x] **M W → MP** — on `W` after `M`: type `P` instead of W
+
+### Right hand
+- [x] **Y B → IB** — on `B` after `Y`: del Y, type `I`, then B
+- [x] **A E → AU** — on `E` after `A`: type `U` instead of E
+
+### Adaptive infrastructure
+- [x] **Base-gating** — adaptives run only when Hands Down is the active default layer (`ramo_adaptive.c`); Colemak types clean.
+- [x] **preprior tracking** — engine keeps `prior` AND `preprior` (enables the 3-key rules above)
+- [x] **ADAPT_SHIFT** (`,` leader) — HDP-only: `,` then a letter within `RAMO_ADAPT_SHIFT_TERM` → capitalizes it, deletes the comma (`ramo_process_record.c`)
+- [ ] **HD_MAGIC key** (`#`) + **HD_MAGIC_B** (`Bksp`) — "magic"/repeat-style text expansion (`.com`, `.org`, `.edu`, etc. via `adapt_magic.c`). Not pursued. Note: canonical `,,` toggles **Caps Lock**.
+
+---
+
+## 2. Combos (`moutis_combo_def.c` → `key_combos[]`)
+
+### 2a. Off-map alphas — *needed to type these letters ergonomically*
+- [x] **qu** (`W+M`, canonical) **(hold: delete u → q)**
+- [x] **z** (`N+H`)
+
+### 2b. H-digraph combos — `th` is **enabled** (`USE_HD_H_DIGRAPH_COMBO`, `HDP-defs.h`); the rest are optional
+- [x] **th** (`T+H`) — live on HDP; Colemak gets its own (`CMK_Th_keys`, `S+T` positions)
+- [ ] **ch** (`I+C`, or `N+T`)
+- [ ] **sh** (`S+N`)
+- [ ] **wh** (`W+G`)
+- [ ] **ph** (`Y+B`, or `P+L`)
+- [ ] **gh** (`G+M`)
+- [ ] **sch** (`S+N+T`)
+
+### 2c. Whitespace / editing keys
+- [x] **Tab** — on thumb (not a combo)
+- [x] **Shift-Tab** (`V+W+G`) — combo
+- [x] **App menu** (`M+J`) — combo
+- [ ] **Space / Enter** — on thumbs already (no combo needed)
+- [ ] **hard-Enter / page break** (`X+L+D`, `SK_HENT`) — needs SemKeys (skipped, Mac-only)
+- [ ] **Config layer MO(L_CFG)** — no CFG layer in our scheme
+- [ ] **Num-layer toggle** (`R + Space`) → `TG(L_NUM)` — optional, not added
+
+### 2d. Semantic editing combos (all via SemKeys — see §3)
+- [ ] **New** (`X+K`)
+- [ ] **Open** (`(-)+(,)`)
+- [ ] **Close** (`(#)+(,)`)
+- [ ] **Quit** (`J+K`)
+- [ ] **Find** (`S+K`) **(hold: find-selection)**
+- [ ] **Select All** (`F+L`)
+- [ ] **Select Word** (`F+X`)
+- [ ] **Undo** (`F+P`)
+- [ ] **Redo** (`F+P+D`)
+- [ ] **Copy** (`P+D`) **(hold: Cut)**
+- [ ] **Paste** (`D+L`) **(hold: Paste-and-Match)**
+
+### 2e. Symbol / punctuation combos (canonical top-right cluster) — HDP base only
+- [x] **;** (`,+A`)
+- [x] **:** (`.+"`)
+- [x] **!** (`.+/`)
+- [x] **?** (`/+"`)
+- [ ] `$ § @ _ ~ = –` — not added (belong on **SYM v2**, where there's room)
+
+### 2f. Ligatures — HDP base only, Mac ⌥ dead-keys
+- [x] **Œ** (`E+O` → ⌥Q)
+- [x] **Æ** (`A+U` → ⌥')
+
+### 2g. Dead-key diacritics (Mac ⌥) — HDP base only, canonical positions
+- [x] **´ acute** (`A+E` → ⌥E)
+- [x] **` grave** (`E+I` → ⌥`)
+- [x] **ˆ circumflex** (`A+I` → ⌥I)
+- [x] **¨ dieresis** (`U+O` → ⌥U)
+- [x] **˜ tilde / ñ** (`V+J` → ⌥N)
+- [x] **¸ cedilla** (`S+T` → ⌥C)
+- [ ] **¯ macron** (`E+C`) / **˚ ring** (`A+C`) — not added (rare)
+
+### 2h. Screen / system (via SemKeys)
+- [ ] **Esc** (`S+H`)
+- [ ] **Force-quit** (`(")+(#)`, `SK_KILL`)
+- [ ] **Screenshot → file** (`W+G+M+J`)
+- [ ] **Screenshot → clipboard** (`G+M+J`)
+- [ ] **Platform search / Siri** (`T+H+K`)
+- [ ] **Caps Word** (`Y+B`) — *combo only;* the feature itself is done, but **implemented differently** (see §3).
+ ⚠️ `Y+B` is already the **YB → IB adaptive** (`ramo_adaptive.c`). Porting this combo means moving one of them.
+- [ ] **Caps Lock** (`O+Y+B`)
+- [ ] **kana / eisuu toggles** (`I+E+A` / `N+T+H`)
+
+### 2i. Custom text-macro combos
+- [ ] **LeftCombo** (`W+G+M`) → user string **(hold: alt string)**
+- [ ] **RightCombo** (`(.)+(/)+(")`) → user string **(hold: alt string)**
+
+### 2j. Numpad combos (on L_NUM; position-based, ~22)
+- [ ] Enter, Bksp, Del, Tab, Shift-Tab on numpad
+- [ ] `– ~ … ( ) : % °` and currency `€ ¥ $ ¢ £ ÷` , `± ≠`
+- [ ] Clear (`C`), All-Clear (`Esc`), Num-Lock toggle
+
+### 2k. Pronoun combos — *optional (`EN_PRONOUN_COMBOS`), moutis calls them "unnecessary"*
+- [ ] `I` (`I+C`), `I'd` (`I+D`), `I'll` (`I+L`), `I'm` (`I+M`), `I've` (`I+V`) — most **(hold: +'ve)**
+- [ ] (`_ALL`) you'd / you'll / you're / you've / your, they / their / they're / they'll / they'd
+- [ ] (`EN_W_PRONOUNS`) we'd / we'll / we're / we've / where
+
+### 2l. Japanese youon combos — *optional (`JP_MODE_ENABLE`), ~50 kana*
+- [ ] きゃ〜りょ contracted-sound combos (skip unless you type Japanese)
+
+---
+
+## 3. Feature systems (`moutis.h` + engine files)
+
+- [x] **Home-row mods** (all base-layer alphas)
+- [x] **Thumb layer-taps** — Colemak: Esc→MEDIA, Spc→NAV, Tab→MOUSE, Ent→SYM, Bspc→NUM, Del→FUN. HDP (Option A): thumbs aligned to Colemak, only R→SYM (inner-right) & Ent→FUN (outer-right) differ; Del→**Shift+Bksp**.
+- [x] **Caps Word** — **different implementation, same intent.** We use QMK's built-in `caps_word` + the
+ `caps_word_press_user` hook (`ramo_linger.c`); moutis rolls his own engine (`moutis_casemods.c`,
+ ~144 lines, from Andrew Rae's pre-QMK-builtin implementation). Behavioral differences worth knowing:
+ | | Ours (QMK builtin) | Moutis (`moutis_casemods.c`) |
+ |---|---|---|
+ | Shifting | weak mods per letter | toggles real **Caps Lock** (`tap_code(KC_CAPS)`) — for iOS etc. |
+ | Activation | `CW_TOGG` on NAV | `,,` adaptive + `Y+B` combo |
+ | space → `_` | in `process_record` | in the engine, tracks `last_press_was_space` |
+ | double-space | *(nothing special)* | exits and **rewrites** `_` back to a real space |
+ | Auto-off | `CAPS_WORD_IDLE_TIMEOUT` (5 s default) | `STATE_RESET_TIME * 3` ≈ 1.8 s, in `matrix_scan` |
+ His Caps-Lock approach survives host-side remapping that weak-mod shifting doesn't; ours needs no
+ `matrix_scan` work and no LED-state round-trip. Only port his if the weak-mod version misbehaves somewhere.
+- [ ] **Semantic keys (`SK_*`)** — cross-platform abstraction. **Intentionally skipped — you're Mac-only**, so `⌘C/⌘V/⌘Z` on NAV stay as-is. — `moutis_semantickeys.c`
+- [x] **Linger keys** — hold `(` `{` → auto-paired with caret inside (`ramo_linger.c`, `RAMO_LINGER_TERM`). `"` is unmapped and `[` isn't on **any** layer yet (SYM has `{ } ( )` only) — both need a SYM v2 home.
+- [ ] **Key overrides** — manual Shift/Alt variants of punctuation. (Only Shift+Bksp→Del is done, for the Option-A thumb change.)
+- [ ] **OS platform switching** — N/A (Mac-only).
+- [ ] **Adaptive on/off toggle** — not needed: adaptives are auto-gated to the HD base, off on Colemak.
+- [ ] **Japanese input mode** — `JP_MODE_ENABLE` (not pursued).
+- [~] **Layer structure** — unchanged: our 8-layer scheme (COLEMAK/HDP/MEDIA/NAV/MOUSE/SYM/NUM/FUN). Not adopting canonical HD's separate layers.
+
+---
+
+## 4. Timings (`config.h`) — ours vs. canonical
+
+Deliberately **not** matched to moutis. His values are in `../HandsDown/config.h` + `moutis.h`.
+WPM equivalent = `12000 / ms` (5 chars/word), i.e. the speed *above* which a window still catches you.
+
+| Constant | Ours | Moutis | Note |
+|---|---|---|---|
+| `TAPPING_TERM` | 200 | 170 | |
+| **Adaptive window** | **175 ms** (~69 WPM) | 229 ms (`COMBO_HOLD*1.35`, ~52 WPM) | see below |
+| ADAPT_SHIFT window | 200 ms | 918 ms (`ADAPTIVE_TERM*4`) | |
+| `COMBO_TERM` | 50 (QMK default) | 42 (`TT/4`) | |
+| Combo hold / linger | 200 / 200 | 170 / 204 | |
+| `TAP_CODE_DELAY` | **20** | 20 | adopted — at 0, back-to-back taps in del-then-retype adaptives can drop or reorder |
+| `QUICK_TAP_TERM` | **0** | 170 (default) | intentional: disables double-tap-hold auto-repeat |
+| `PERMISSIVE_HOLD` | off | on | intentional (for now) |
+
+**Adaptive window — tuned empirically (`tt verify`), 50 → 100 → 175 ms.**
+The key insight: **adaptives fix SFBs and scissors, which are slow motions by definition** — so a
+window tuned to *roll* speed systematically fails the highest-value rules. At 100 ms, fast
+adjacent-finger rolls passed (`PD`, `WG`) while `KH` (same-finger, index→index) and `KG`
+(inner-column stretch) never fired at all. 175 ms clears those motions while staying well under
+moutis's always-armed 229 ms.
+
+**Window is measured with `record->event.time`, not `timer_read()`.** For a mod-tap/layer-tap,
+`process_record_user` only sees the record once the tap resolves (on release, or after
+`TAPPING_TERM`), so read-time would charge the hold duration against the window. 12 keys are
+mod-taps (`G M . /` + both home rows), i.e. most adaptive triggers.
+
+**3-key rules need two consecutive intervals inside the window**, so their failure rate is roughly
+the square of the 2-key rate — expect them to be the last to become reliable.
+
+**Known collisions to watch at 175 ms** — same-row rolls that appear in real words:
+`GM` in *pragma* / *progmem* · `PD` in *update* · `YB` in *keyboard* · `MW` in *firmware* · `KG` in *background*.
+None observed yet in practice. `MK` (*qmk*) is safe, being same-finger.
+
+**Combo-vs-adaptive collisions:** combos resolve in `pre_process_record_quantum`, before the adaptive
+engine. Any rule whose two keys are also a combo is unreachable — `W M`/`M W M` (`qu`) and `V J` (`˜`)
+were removed for this. Check new adaptives against `ramo_combos.c` before adding.
+
+
+---
+
+## Done vs. remaining
+**Done:** complete PM adaptives incl. 3-key rules (preprior) · ADAPT_SHIFT · `qu`/`z`/`th` · Shift-Tab · App-menu · diacritics + ligatures (`é è ê ü ñ ç œ æ`) · `; : ! ?` symbol combos · linger `( {` · smart caps-word · Shift+Bksp→Del · Option-A thumbs · canonical arrangement · blanked HD outer columns. **All HDP-gated where base-specific. Builds ~64 KB.**
+
+**Still open (by choice / next):**
+
+> **Current focus: learning the layout, not extending it.** The firmware is feature-complete for
+> daily driving. Everything below is deferred until real use shows it's needed — including the
+> adaptive-window collisions in §4.
+
+1. **SYM "Programming v2"** — bracket/operator layer for Swift/Odin/ObjC (mock in `hd-colemak.html`). Would also give `[` (currently on no layer) and `"` a home for linger.
+2. **Colemak-DH** — optional base swap (6 keys), mocked as a toggle.
+3. Nice-to-have combos: `ch/sh/wh/ph/gh` digraphs (`th` already on); `$ § @ ~ = –` (better on SYM v2).
+4. **Not pursued (Mac-only / design):** Semantic keys → so §2d editing & §2h screen combos (they need SemKeys), HD_MAGIC, macron/ring diacritics, pronouns (§2k), Japanese (§2l), numpad combos (§2j).
diff --git a/users/raphaelmor/LICENSE b/users/raphaelmor/LICENSE
new file mode 100644
index 00000000..f288702d
--- /dev/null
+++ b/users/raphaelmor/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/users/raphaelmor/QMK-feature-audit.md b/users/raphaelmor/QMK-feature-audit.md
new file mode 100644
index 00000000..8757dcd6
--- /dev/null
+++ b/users/raphaelmor/QMK-feature-audit.md
@@ -0,0 +1,328 @@
+# QMK feature audit — candidates for this firmware
+
+Survey of **everything QMK offers**, judged against *this* keymap (Colemak + Hands Down
+Promethium on a Corne v4.1). Companion to [`HDPM-parity.md`](HDPM-parity.md): that doc tracks
+parity with **moutis' Hands Down**, this one tracks parity with **QMK core**.
+
+Sources: QMK `../qmk` @ `3ef9860ee1` (`0.29.8-768`, 2026-07-22) · userspace `users/raphaelmor/`.
+
+Legend: `[x]` in this firmware · `[ ]` not here · **T1/T2** = recommended tier · **✗** = examined
+and rejected, with the reason.
+
+> **The framing.** `HDPM-parity.md:235-237` freezes feature work ("learning the layout, not
+> extending it"). So the bar below is not *"is this interesting"* but **"does it reduce friction
+> while learning, or fix something already broken?"**
+>
+> **The one-line finding:** this firmware is unusually complete on the **typing-transformation**
+> axis (adaptives, combos, linger, comma-shift, smart Caps Word) and near-empty on the
+> **tap-hold-correctness** and **feedback** axes. 16 home-row mod-taps run with *zero* misfire
+> protection. That asymmetry is where all the value is.
+
+---
+
+## 1. What's already on
+
+**`rules.mk`:** Caps Word · Combos · Tap Dance · Console + `KEYCODE_STRING` · introspection.
+
+**Inherited from `crkbd/rev4_1`:** RGB Matrix · split · Mouse Keys · OLED · encoders ·
+bootmagic · NKRO · extrakey.
+
+**Hand-rolled, with no QMK feature behind it:**
+
+| Behaviour | Where | QMK equivalent |
+|---|---|---|
+| Adaptive keys (24 rules) | `ramo_adaptive.c` | none — genuinely custom |
+| Linger brackets `( {` | `ramo_linger.c:10-38` | none |
+| SCREAMING_SNAKE Caps Word | `ramo_linger.c:80-97` | none (extends builtin Caps Word) |
+| Comma-leader one-shot Shift | `ramo_process_record.c:48-60` | loosely, one-shot mods |
+| Shift+Bspc → Del | `ramo_process_record.c:35-42` | **Key Overrides** → §3.3 |
+| Combo hold-to-linger | `ramo_combos.c:154-167` | none |
+
+---
+
+## 2. The full QMK catalog
+
+### 2a. Tap/hold and timing — `docs/tap_hold.md`
+- [x] `TAPPING_TERM` (200) · `QUICK_TAP_TERM` (0)
+- [ ] **`CHORDAL_HOLD`** — same-hand nested key forces the tap. → **T1, §3.1**
+- [ ] **`FLOW_TAP_TERM`** — suppress hold shortly after another key. → **T2, §4.1**
+- [ ] `PERMISSIVE_HOLD` / `HOLD_ON_OTHER_KEY_PRESS` — → **T1, pairs with §3.1**
+- [ ] **`DYNAMIC_TAPPING_TERM_ENABLE`** (`DT_UP`/`DT_DOWN`/`DT_PRNT`) → **T2, §4.3**
+- [ ] `RETRO_TAPPING` / Retro Shift — ✗ fights home-row mods
+- [ ] `SPECULATIVE_HOLD` (`docs/tap_hold.md:819`) — ✗ very new, perturbs the exact path the adaptives ride
+- [ ] `TAPPING_TERM_PER_KEY` · selectable debounce — not needed yet
+- **Achordion is not in QMK core.** It's Getreuer's userspace library; `CHORDAL_HOLD` is the
+ in-tree replacement. Don't go looking for it.
+
+### 2b. Text / typing aids
+- [x] Caps Word (`docs/features/caps_word.md`) — see §4.5 for two free upgrades
+- [x] Combos · Tap Dance · Send String
+- [ ] **Repeat / Alt-Repeat Key** (`QK_REP`/`QK_AREP`) → **T1, §3.2**
+- [ ] **Key Overrides** → **T1, §3.3**
+- [ ] **Layer Lock** (`QK_LLCK`) → **T2, §4.2**
+- [ ] Autocorrect — ✗ §5
+- [ ] Leader Key — ✗ §5
+- [ ] Unicode / Unicode Map / UCIS / `send_unicode_string()` — ✗ §5 (Mac ⌥ dead keys by design)
+- [ ] Auto Shift · Space Cadet · one-shot/sticky mods · Dynamic Macros · Key Lock (`QK_LOCK`) ·
+ Swap Hands · Secure · Grave Escape · Magic keycodes · Stenography — ✗ §5
+
+### 2c. Input devices
+- [x] Mouse Keys (tuned, `config.h:17-26`)
+- [~] **Encoders** — compiled in, *inert*, and pin-blocked → **§4.7**
+- [ ] Pointing Device · PS/2 Mouse · Digitizer · Joystick · Programmable Buttons · DIP Switch — no hardware / no need
+
+### 2d. Output / feedback
+- [x] RGB Matrix — static ledmap only, all 44 stock animations `#undef`'d (`config.h:56-101`)
+- [~] **OLED** — enabled at keyboard level, **no `oled_task_user`** → **T1, §3.4**
+- [ ] **WPM** (+ `SPLIT_WPM_ENABLE`) → **T2, §4.4**
+- [ ] LED Matrix · Backlight · LED Indicators · ST7565/HD44780 · Quantum Painter · Audio/Music/Clicky ·
+ MIDI · Sequencer · Haptic — not applicable or not wanted
+
+### 2e. Infrastructure
+- [x] Split + `SPLIT_LAYER_STATE_ENABLE` · introspection · Community Modules
+ (`elpekenin/ledmap` + `elpekenin/generics`, 2 of ~25)
+- [ ] Deferred execution — ✗ §5 (polling was a deliberate choice)
+- [ ] Remaining `SPLIT_*_ENABLE` flags: `LED_STATE` `MODS` `WPM` `OLED` `POINTING` `HAPTIC` `ACTIVITY`
+- [ ] EEPROM/NVM/wear-leveling · VIA · Raw HID · Tri Layer · OS Detection · Wireless/Battery — not needed
+
+---
+
+## 3. Tier 1 — worth doing
+
+### 3.1 Chordal Hold — the standout gap
+`#define CHORDAL_HOLD` · `docs/tap_hold.md:568`
+
+**Why.** 16 home-row mod-taps across two base layers at `TAPPING_TERM 200`, with
+`PERMISSIVE_HOLD` off, `HOLD_ON_OTHER_KEY_PRESS` off, and **no misfire protection whatsoever**
+(`HDPM-parity.md:202` records this as "intentional (for now)"). Chordal Hold settles a mod-tap as
+a *tap* whenever the next key is on the **same hand**, and leaves cross-hand chords to
+`HOLD_ON_OTHER_KEY_PRESS`/`PERMISSIVE_HOLD`. Hands Down Promethium is built around same-hand
+rolls, and the adaptive rules deliberately target same-hand SFBs and scissors (`KH`, `KG`, `MG`) —
+exactly the motions that misfire a mod today.
+
+**⚠️ It is not a one-liner on this board.** Three things verified in the tree:
+
+**(a) The auto-generated handedness table misses 10 of your 46 keys.**
+Handedness is generated as a *weak* `chordal_hold_layout`
+(`../qmk/lib/python/qmk/cli/generate/keyboard_c.py:207-239`), but the generator **reads only the
+first layout in `info.json`** — for `crkbd/rev4_1` that's `LAYOUT_split_3x5_3` (36 keys), while
+this keymap uses `LAYOUT_split_3x6_3_ex2` (46). The 10 uncovered matrix positions stay `0`, which
+is neither `'L'`, `'R'`, nor `'*'`:
+
+| Uncovered | Positions | HDP | Colemak |
+|---|---|---|---|
+| Outer pinky columns | `(0,0) (1,0) (2,0) (4,0) (5,0) (6,0)` → `L/R T5 M5 B5` | `KC_NO` | `KC_NO` |
+| The four `ex2` keys | `(0,6) (1,6) (4,6) (5,6)` → `LTA LMA RTA RMA` | `KC_NO` | **`LCTL_T(KC_A)`** |
+
+Per `get_chordal_hold_default` (`../qmk/quantum/action_tapping.c:940-951`) a `0` compares unequal
+to everything, so those keys always permit the hold — i.e. Chordal Hold is silently *off* for
+them. Almost entirely harmless here, since 6 of the 10 are `KC_NO` on both bases; the only live
+case is the four `ex2` keys on Colemak, which are all `LCTL_T(KC_A)` (`CMK-defs.h:77,79,93,95`).
+If §3.2 puts `QK_REP`/`QK_AREP` on those keys they become non-tap-hold and the issue disappears
+entirely.
+
+**(b) The thumbs get real `'L'`/`'R'` handedness, and that breaks every layer.**
+The geometry guess only assigns `'*'` to a spacebar or a dead-centre key; all six thumbs land
+firmly L or R. Every layer here puts its *payload* on the hand opposite its activating thumb —
+but its **same-side modifier block on the same hand as that thumb**. With a bare
+`#define CHORDAL_HOLD`, chording the thumb with its own hand's mods within `TAPPING_TERM` settles
+the layer-tap as a tap:
+
+| Thumb | Same-hand keys on the target layer | Broken result |
+|---|---|---|
+| `LT(L_MEDIA, KC_ESC)` `HDP-defs.h:77` | `KC_LCTL/LALT/LGUI/LSFT` `MED-defs.h:37-40`, `KC_ALGR :55` | types `Esc` |
+| `LT(L_NAV, KC_SPC)` `:78` | left mods `NAV-defs.h:37-40` **+ both base-switch tap dances** `:23-24` | types `Space` |
+| `LT(L_MOUSE, KC_TAB)` `:79` | left mods `MOS-defs.h:36-39` | types `Tab` |
+| `LT(L_SYM, KC_R)` `:80` | `KC_RSFT/RGUI/RALT/RCTL` `SYM-defs.h:46-49` | types `r` |
+| `LT(L_NUM, KC_BSPC)` `:81` | right mirror mods `NUM-defs.h:46-49` | types `Backspace` |
+| `LT(L_FUN, KC_ENT)` `:82` | right mirror mods `FUN-defs.h:46-49` | types `Enter` |
+
+Scope, stated honestly: "Chordal Hold has no effect after the tapping term", so this only bites
+when the second key lands within 200 ms of the thumb press — which is precisely normal fast layer
+use. **Prerequisite: define `chordal_hold_layout` over `LAYOUT_split_3x6_3_ex2` with `'*'` on all
+six thumbs** — the docs recommend exactly this for thumb keys, and it fixes (a) at the same time
+by covering all 46 positions.
+
+**(c) The 17 combos are safe.** `get_chordal_hold_default` returns `true` for non-`KEY_EVENT`
+records (`action_tapping.c:941-943`), so combos are exempt from the opposite-hands rule.
+
+**Pair it with `HOLD_ON_OTHER_KEY_PRESS`** — the docs state Chordal Hold is *intended* to be used
+with that or Permissive Hold; alone, the cross-hand case is unchanged.
+
+**If adopted, verify:** `tt verify` on the same-hand adaptives most at risk — `KH` (same-finger
+index), `KG` (inner-column stretch), and the 3-key `WMG` — plus confirm cross-hand `⌘C`/`⌘V` and
+`⇧`+letter still hold. Expect adaptives to get *more* reliable, not less: same-hand taps now
+settle at the next keydown instead of at release/timeout, which shortens the very delay documented
+at `ramo_adaptive.c:12-18`.
+
+### 3.2 Repeat Key — and it answers the four dead keys
+`REPEAT_KEY_ENABLE = yes` · `QK_REP` / `QK_AREP` · `docs/features/repeat_key.md`
+
+Same problem domain as the adaptive engine — eliminating same-finger bigrams — but with **zero
+collision risk**, because it never rewrites already-typed text and so can't fight combos the way
+`W M`/`V J` did (`HDPM-parity.md:223-225`). moutis' own HD layouts lean on it.
+
+**Placement is already decided by the keymap:** all four `ex2` positions carry
+`// TODO: should be a real key once a use is found` — `KC_NO` on HDP (`HDP-defs.h:33,35,49,51`)
+and a pointless duplicate `LCTL_T(KC_A)` on Colemak (`CMK-defs.h:77,79,93,95`). `QK_REP` and
+`QK_AREP` are the answer to that TODO, and doing so also erases finding §3.1(a).
+Alt-Repeat's context-dependent inverse (`←`→`→`, PgDn→PgUp) is a real NAV-layer win.
+
+**Caveat to check, not assume:** `process_last_key` and `process_repeat_key` run at
+`../qmk/quantum/quantum.c:343`, **upstream of `process_record_kb`/`process_record_user` (line
+355)**. So the adaptive engine's `prior_keycode`/`preprior_keycode` history will see the
+*repeated* keycode rather than `QK_REP`. That's probably what you want — a repeat should count as
+the letter for adaptive purposes — but it needs a `tt verify` pass to confirm the interaction with
+the del-then-retype rules.
+
+### 3.3 Key Overrides — strictly less code than what's there
+`KEY_OVERRIDE_ENABLE = yes` · `docs/features/key_overrides.md` · already tracked `[ ]` at
+`HDPM-parity.md:180`
+
+`ramo_process_record.c:35-42` hand-rolls Shift+Bspc→Del with manual `clear_mods()`/`set_mods()`.
+`ko_make_with_layers_negmods_and_options` (`../qmk/quantum/process_keycode/process_key_override.h:135`)
+expresses it declaratively, and `negative_mod_mask` (`:69`) is the exact equivalent of the current
+`!(get_mods() & ~MOD_MASK_SHIFT)` guard — so ⌘⇧Bspc keeps *not* becoming Del. Also gives a proper
+home for future shifted-punctuation variants.
+
+**Behavioural difference to record:** `process_key_override` runs at `quantum/quantum.c:387`,
+*after* `process_record_user`. Today's early `return false` means the adaptive dispatch never sees
+the keypress; with an override it would run first and set `prior_keycode = KC_BSPC`. Harmless as
+far as the current 24 rules go (none trigger on `KC_BSPC`), but it is a real change in state.
+
+### 3.4 `oled_task_user` — the display is currently lying
+No `oled_task_user` exists anywhere in the userspace, so stock `crkbd.c` renders: it prints
+**"Lower" for Hands Down and "Undef" for every layer ≥ 4**, against an 8-layer keymap.
+
+The DRY fix reuses `RAMO_FOR_EACH_LAYER` (`ramo_layers.h:16-24`) — already the single source for
+both `keymaps[]` (`keymap.c:49-57`) and `ledmap[]` (`ramo_ledmap.c:6-13`) — to generate a
+layer-name table from the same list, so a layer can't have a keymap without a name. Worth showing
+alongside: active base (CMK vs HDP, from `default_layer_state`), Caps Word state, and WPM (§4.4).
+
+`elpekenin/ui` is already in the `modules/` submodule if you'd rather not hand-roll the drawing.
+
+---
+
+## 4. Tier 2 — good fits, lower stakes
+
+### 4.1 Flow Tap — `#define FLOW_TAP_TERM 150` · `docs/tap_hold.md:485`
+Complement to Chordal Hold: suppresses the hold when a tap-hold key is pressed shortly after
+another key. Same mental model as `RAMO_ADAPTIVE_TERM 175` (`config.h:35-49`) — a rolling
+typing-speed window.
+
+**⚠️ It would break the thumbs as shipped.** The default `is_flow_tap_key()`
+(`docs/tap_hold.md:504`) matches `KC_A`–`KC_Z`, `KC_SPC`, `KC_COMM`, `KC_DOT`, `KC_SCLN`, `KC_SLSH`
+— applied to `get_tap_keycode()`, so it captures `LT(L_NAV, KC_SPC)` (`HDP-defs.h:78`) and HDP's
+`LT(L_SYM, KC_R)` (`:80`). A letter followed within `FLOW_TAP_TERM` by a reach for thumb-NAV or
+thumb-SYM would emit a literal space or `r`. **Overriding `is_flow_tap_key()` to exclude thumb
+layer-taps is mandatory, not optional** (the docs even call out removing `case KC_SPC`).
+
+**Upside for the adaptives:** Flow Tap forces early tap resolution, which directly mitigates the
+late-`event.time` problem documented at `ramo_adaptive.c:12-18`.
+
+### 4.2 Layer Lock — `LAYER_LOCK_ENABLE = yes` · `QK_LLCK` · `docs/features/layer_lock.md`
+Long numeric entry on NUM and extended mouse work on MOS without holding a thumb the whole time.
+Both layers have plenty of `*_NO` slots to host it.
+
+### 4.3 Dynamic Tapping Term — `DYNAMIC_TAPPING_TERM_ENABLE = yes` · `docs/tap_hold.md:79`
+`DT_UP`/`DT_DOWN`/`DT_PRNT` tune the tapping term live and type out the current value. This is the
+same empirical loop already used for `RAMO_ADAPTIVE_TERM` (50 → 100 → 175, `config.h:35-49`), but
+without a reflash per iteration. FUN has free slots.
+
+**Caveat:** it switches the *runtime* value to `g_tapping_term` while the `TAPPING_TERM` **macro**
+stays 200 — so `RAMO_COMBO_HOLD`, `RAMO_LINGER_TERM` and `RAMO_ADAPT_SHIFT_TERM`
+(`config.h:32,33,52`) would stay pinned at 200 while the real tapping term moved. Use
+`GET_TAPPING_TERM(keycode, record)` if they should track it.
+
+### 4.4 WPM — `WPM_ENABLE = yes` + `#define SPLIT_WPM_ENABLE`
+Direct progress feedback during the learning phase, surfaced on the OLED from §3.4. Cheap, and it
+is the one addition that measures whether the layout is actually being learned.
+
+### 4.5 Caps Word activation extras — two defines, no code
+Caps Word is used heavily already (`CW_TOGG` on `NAV_RM0`, plus the `Y+B` combo
+`ramo_combos.c:76`).
+- `BOTH_SHIFTS_TURNS_ON_CAPS_WORD` — both index home-row shifts (`LSFT_T(KC_H)` +
+ `RSFT_T(KC_A)`), which is a **cross-hand** chord and so composes cleanly with §3.1.
+- `CAPS_WORD_INVERT_ON_SHIFT` — shift inverts instead of breaking the word.
+- Also worth knowing: `CAPS_WORD_IDLE_TIMEOUT` defaults to 5 s; moutis uses ~1.8 s
+ (`HDPM-parity.md:175`).
+
+### 4.6 `QK_BOOT` / `EE_CLR` — currently on no layer at all
+Flashing relies entirely on the RP2040 double-tap-reset. Cheap insurance on FUN, which has free
+slots. (Both appear in the `HDPM-layers.html` mock-up but exist on no real layer.)
+
+### 4.7 Encoders — **check the hardware before spending any effort**
+`info.json` declares 2 encoders per side, but there is no `encoder_map`, no `ENCODER_MAP_ENABLE`
+and no `encoder_update_user` — so they're compiled in and inert.
+
+**Verified pin conflict:** encoder pins are GP5/GP6/GP7 (left) and GP24/GP6/GP7 (right)
+(`../qmk/keyboards/crkbd/rev4_1/info.json:36-37,65-66`) against `I2C1_SDA_PIN GP6` /
+`I2C1_SCL_PIN GP7` (`../qmk/keyboards/crkbd/rev4_1/config.h:10-11`). **Encoders and the OLED
+share the same pins on rev4.1 and are mutually exclusive.** Decide which you want before writing
+an encoder map or an `oled_task_user`.
+
+---
+
+## 5. Examined and rejected
+
+- **✗ Autocorrect** — collides with the adaptive engine conceptually: both are armed on every
+ keystroke and both rewrite via backspace. Worse, during a learning phase it **masks the layout
+ errors you need to feel**. Revisit once HDP is automatic.
+- **✗ Leader Key** — redundant against 17 combos; a second chord vocabulary to memorise for no new
+ capability.
+- **✗ Auto Shift / Retro Shift** — fights home-row mods and the comma-leader shift
+ (`ramo_process_record.c:48-60`).
+- **✗ Deferred executors** — polling via `matrix_scan_user` was chosen *deliberately* to dodge
+ roll-over ordering bugs; the reasoning is at `ramo_linger.c:5-8`. Leave it.
+- **✗ Unicode / Unicode Map / UCIS / `send_unicode_string()`** — the whole diacritic set is Mac ⌥
+ dead keys by design (`ramo_combos.c`, `HDPM-parity.md:181`). Only relevant if a non-Mac host
+ ever appears, and then **OS Detection** (`docs/features/os_detection.md`) is the companion.
+- **✗ Grave Escape** — Esc is a thumb layer-tap (`HDP-defs.h:77`), so `QK_GESC` has nowhere to go.
+- **✗ Speculative Hold** (`docs/tap_hold.md:819`) — genuine latency win, but very new and it
+ perturbs the exact tap-hold path the adaptives depend on. Not while also learning the layout.
+- **✗ One-shot mods** — the comma-leader shift is better suited to this layout than `OSM(MOD_LSFT)`.
+- **✗ Space Cadet · Swap Hands · Dynamic Macros · Secure · Key Lock · Tri Layer · Programmable
+ Buttons · Digitizer · Joystick · MIDI · Sequencer · Audio · Haptic · Stenography · Quantum
+ Painter · VIA · Raw HID** — no problem in this firmware that they solve.
+
+---
+
+## 6. Defects noticed during the survey (documented, not fixed)
+
+1. **`saved_modifiers` is dead code.** Declared `raphaelmor.c:10` and read at `ramo_combos.c:108`
+ as the "don't fire combos while ⌥/⌘/⌃ is held" guard — but **never assigned anywhere**. As a
+ zero-initialised global it is permanently 0, so the guard never fires. Intent was presumably
+ `get_mods()`.
+2. **The HD Gold path does not compile.** Un-defining `RAMO_USE_HD_PROMETHIUM` (`config.h:34`)
+ breaks the build: `HDG-defs.h:186,189` define `HD_Q_keys`/`HD_Z_keys` in terms of `HDP_*`
+ positions, and `ramo_combos.c:13-34` references `HDP_*` unconditionally, but `ramo_layers.h:2-6`
+ includes one defs file or the other, never both.
+3. **`rules.mk:5`** is `// TEMP: for debugging adaptive`. `//` is not a Make comment, and the line
+ contains a `:`, so Make parses it as a stray rule target with prerequisites. Harmless in
+ practice (QMK sets its default goal first) — but it is not a comment.
+4. **`HDPM-parity.md:138`** marks Caps Word `Y+B` as not done; it *is* implemented
+ (`ramo_combos.c:34,76`).
+
+---
+
+## 7. Toolchain blocker
+
+The `qmk` CLI is currently broken on this machine — `/opt/homebrew/bin/qmk` has a dangling
+interpreter (`/opt/homebrew/Cellar/qmk/1.1.8/libexec/bin/python`). The userspace `Makefile`
+resolves `QMK_FIRMWARE_ROOT` via `qmk config -ro user.qmk_home`, so **nothing here can be built
+until that's repaired** (`brew reinstall qmk`). Unrelated to this audit, but it gates acting on
+any of it.
+
+---
+
+## 8. Summary
+
+| Priority | Feature | Effort | Note |
+|---|---|---|---|
+| **T1** | Chordal Hold + `HOLD_ON_OTHER_KEY_PRESS` | 2 defines **+ a 46-key handedness table with `'*'` thumbs** | biggest win; do not ship without the table |
+| **T1** | Repeat / Alt-Repeat on the four `ex2` keys | 1 flag + 4 keycodes | answers an existing TODO; also erases §3.1(a) |
+| **T1** | Key Overrides | 1 flag + 1 table; **deletes** `ramo_process_record.c:35-42` | net less code |
+| **T1** | `oled_task_user` | ~30 lines via `RAMO_FOR_EACH_LAYER` | the OLED is currently wrong |
+| **T2** | Flow Tap | 1 define + `is_flow_tap_key()` override | override is mandatory |
+| **T2** | Layer Lock · Dynamic Tapping Term · WPM · Caps Word defines · `QK_BOOT`/`EE_CLR` | flags + keycodes | cheap |
+| **T2** | Encoder map | medium | **blocked on the OLED-vs-encoder pin decision** |
diff --git a/users/raphaelmor/README.md b/users/raphaelmor/README.md
index d9467bc6..eebbf210 100644
--- a/users/raphaelmor/README.md
+++ b/users/raphaelmor/README.md
@@ -1,4 +1,15 @@
-# HANDS DOWN GOLD and COLEMAK MIRYOKU CUSTOM SETUP FOR CORNE
+# Corne v4.1 · QMK
+
+The Corne implementation of my [Colemak + Hands Down Promethium
+layout](https://github.com/raphaelmor/keyboard-layout).
+
+**The layout itself is documented there**, not here — the alphas, the six layers,
+the combos, the adaptive keys and the always-on behaviors are the same whichever
+board runs them. This file covers only what is true of this board and this
+firmware.
+
+Everything below the marker is generated from the C sources by
+`tools/kle_docs.py`. Edit around it, not inside it.
## TODO
@@ -27,3 +38,105 @@ SPRING : Colemak
ORANGE : HD-p or g
CHARTREUSE : Core Keys of the layer (arrows on NAV, numbers on NUM etc...)
WHITE : Other Keys for the layer (home,end, copy cut on NAV, etc..)
+
+
+
+
+
+## The board
+
+Corne v4.1, `LAYOUT_split_3x6_3_ex2` (`keymap.c`): 46 switches — 3x6 per
+half, three thumbs, plus the four ex2 inner keys `LTA LMA RTA RMA`.
+
+Only 36 of them carry anything, so the pictures in the layout
+repo draw a 3x5 + 3. See *Not drawn* below for the rest.
+
+## Not drawn
+
+The board has 46 switches. These 10 are left off:
+
+- Outer pinky column `LT5 LM5 LB5 RT5 RM5 RB5` — `KC_NO` on every layer.
+- Inner ex2 keys `LTA LMA RTA RMA`.
+
+Careful: these hidden positions do have something bound.
+
+- `CMK_LTA` is `LCTL_T(KC_A)`
+- `CMK_RTA` is `LCTL_T(KC_A)`
+- `CMK_LMA` is `LCTL_T(KC_A)`
+- `CMK_RMA` is `LCTL_T(KC_A)`
+
+## Build flags and timings
+
+All from `config.h`, read at generation time.
+
+```
+ define value effect
+ -------------------------------------------------------------------------------
+ RAMO_USE_HD_PROMETHIUM set Hands Down Promethium
+ TAPPING_TERM 200 hold to get the mod, the layer, the bracket pair
+ RAMO_ADAPTIVE_TERM 175 keydown to keydown, for an adaptive to fire
+```
+
+## Where each feature lives
+
+All paths are relative to `users/raphaelmor/`.
+
+```
+ feature file
+ -------------------------------------------------------------------------------------------------
+ layer list and order ramo_layers.h
+ key and colour per position layers/{CMK,HDP,HDG,MED,NAV,MOS,SYM,NUM,FUN}-defs.h
+ the keymap itself ../../keyboards/crkbd/rev4_1/standard/keymaps/raphaelmor/keymap.c
+ combos ramo_combos.c
+ adaptive keys ramo_adaptive.c
+ linger brackets, Caps Word ramo_linger.c
+ Shift+Bksp, comma-leader shift ramo_process_record.c
+ base switch (tap dance) ramo_tap_dance.c
+ per-key RGB ramo_ledmap.c
+```
+
+## Regenerating the docs
+
+```
+python3 tools/kle_docs.py # this file and the layout repo
+python3 tools/kle_export.py # corne-v4-1-ramo.json, for KLE
+```
+
+`kle_export.py` runs the C preprocessor over `ramo_layers.h`, so it reads
+the same keymap the firmware builds. It aborts instead of writing a wrong
+file: an unknown keycode, a combo whose keys do not resolve, or a legend
+too long for a cap all stop the export.
+
+To open it in [keyboard-layout-editor.com](http://www.keyboard-layout-editor.com),
+use the **Upload** button and pick `corne-v4-1-ramo.json`. Do not paste that
+file into the **Raw data** tab — that tab wraps whatever you type in `[ ]` of
+its own, so a whole `.json` file ends up double-wrapped and draws one empty
+key. Paste this instead:
+
+```
+python3 tools/kle_export.py --raw -o /tmp/kle-raw.txt && pbcopy < /tmp/kle-raw.txt
+```
+
+
+
+## License
+
+**GPL-3.0-or-later.** See [LICENSE](LICENSE).
+
+This directory is not under the GPL-2.0 licence at the root of `qmk_userspace`,
+which covers the userspace template. The design here follows [Hands Down
+Promethium by moutis](https://github.com/moutis/HandsDown), whose QMK
+implementation is GPL-3.0. QMK itself is GPL-2.0-**or-later**, and that "or
+later" is what lets the two combine: the result is GPL-3.0.
+
+A keyboard layout — which letter sits on which position — is a system rather than
+an expressive work, so no licence is owed for the arrangement itself. The
+attribution is given because it is deserved, not because it is required.
+
+Layer structure follows [Miryoku by
+manna-harbour](https://github.com/manna-harbour/miryoku). That project publishes
+no licence, which is another reason nothing of its is copied here.
+
+`tools/` is MIT, not GPL. Those scripts are original and share nothing with
+upstream, and they generate the [layout
+repo](https://github.com/raphaelmor/keyboard-layout), which is MIT.
diff --git a/users/raphaelmor/config.h b/users/raphaelmor/config.h
index 7cfcaede..42b0c9c5 100644
--- a/users/raphaelmor/config.h
+++ b/users/raphaelmor/config.h
@@ -1,3 +1,13 @@
+/*
+ * config.h
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
#pragma once
// default but used in macros
@@ -7,6 +17,12 @@
// Enable rapid switch from tap to hold, disables double tap hold auto-repeat.
#define QUICK_TAP_TERM 0
+// Gap between register/unregister in tap_code(). At the default 0, back-to-back
+// taps in the adaptives (del-then-retype, e.g. MG -> LG) can land in the same USB
+// frame and go missing or out of sequence. 20 is moutis's value.
+#undef TAP_CODE_DELAY
+#define TAP_CODE_DELAY 20
+
// Mouse key speed and acceleration.
#undef MOUSEKEY_DELAY
#define MOUSEKEY_DELAY 0
@@ -24,8 +40,26 @@
// RAMO CUSTOM CONFIG
#define RAMO_COMBO_HOLD (TAPPING_TERM) // time to hold to trigger delayed combo
+#define RAMO_LINGER_TERM (TAPPING_TERM) // hold time to auto-pair brackets
#define RAMO_USE_HD_PROMETHIUM // (Gold is the default)
-#define RAMO_ADAPTIVE_TERM (TAPPING_TERM/4) // rolling threshold
+// Adaptive rolling window: how long after a keydown the next key can still trigger
+// an adaptive. Measured keydown-to-keydown (see prior_keydown_timer / event.time).
+// 50ms (TAPPING_TERM/4) = ~240 WPM — roll-only; missed most rules in practice
+// 100ms = ~120 WPM — fine for fast rolls (PD, WG), but SFB and
+// cross-row rules (KH same-finger, KG inner-column
+// stretch) are inherently slower and never fired
+// 175ms = ~69 WPM — current. Adaptives exist to fix SFBs and
+// scissors, which are *slow by definition*, so the
+// window has to clear those motions.
+// 229ms (moutis, COMBO_HOLD*1.35) = ~52 WPM
+// Collisions to watch at 175ms (same-row rolls in real words): GM in
+// "pragma"/"progmem", PD in "update", YB in "keyboard", KG in "background".
+// #define RAMO_ADAPTIVE_TERM (TAPPING_TERM/4) // original: 50ms
+// #define RAMO_ADAPTIVE_TERM 100 // too tight for SFB rules
+#define RAMO_ADAPTIVE_TERM 175
+
+
+#define RAMO_ADAPT_SHIFT_TERM (TAPPING_TERM) // comma-leader one-shot-shift window (wider than adaptives)
// Disable led stuff
diff --git a/users/raphaelmor/corne-v4-1-ramo.json b/users/raphaelmor/corne-v4-1-ramo.json
new file mode 100644
index 00000000..7a1d194a
--- /dev/null
+++ b/users/raphaelmor/corne-v4-1-ramo.json
@@ -0,0 +1,1604 @@
+[
+ {
+ "backcolor": "#626262",
+ "name": "Corne v4.1 · raphaelmor Colemak + Hands Down Promethium",
+ "notes": "# Colemak + HandsDown · Corne v4.1\n\nGenerated by `users/raphaelmor/tools/kle_export.py` from the QMK sources.\nDo not hand-edit — regenerate instead.\n\n## Reaching a layer\n\nEach layer's picture marks the key you **hold** to get there in red, and\nghosts it, because it is not something that layer binds. Where the two\nbases differ the column reads Colemak / HandsDown.\n\n```\n layer hold position\n ----------------------\n Media Esc LH2\n Nav ␣ LH1\n Mouse Tab LH0\n Sym ⏎ / R RH0\n Num ⌫ RH1\n Fun ⌦ / ⏎ RH2\n```\n\n## Combos — 18\n\nA combo is a set of key *positions*, not of letters. Most are limited to\nthe HandsDown base, but 3 are not, so those also fire on\nColemak — at whatever letters sit under those positions.\n\n```\n Colemak HandsDown sends positions\n --------------------------------------------------------------------------------\n S+T T+H th LM2+LM1\n W+P W+M qu — hold past 200 ms to drop the u and leave q LT3+LT1\n R+T N+H Z LM3+LM1\n\n HandsDown base only\n A+E acute dead key (⌥E) — é RM1+RM2\n E+I grave dead key (⌥`) — è RM2+RM3\n A+I circumflex dead key (⌥I) — ê RM1+RM3\n U+O umlaut dead key (⌥U) — ü RB1+RB2\n V+J tilde dead key (⌥N) — ñ LT4+LT0\n S+T ç (⌥C) LM4+LM2\n E+O œ (⌥Q) RM2+RB2\n A+U æ (⌥') RM1+RB1\n V+W+G Shift-Tab LT4+LT3+LT2\n M+J App menu LT1+LT0\n ,+A ; RM0+RM1\n .+\" : RT1+RT3\n /+\" ? RT2+RT3\n .+/ ! RT1+RT2\n Y+B Caps Word on or off RB3+RB4\n```\n\n`key_combos[]` declares 19 entries. One of them repeats a chord\nalready listed above — same positions, same output.\n\n## Adaptive keys — 22\n\nOnly on the HandsDown base. Roll two keys inside 175 ms and the\nsecond one rewrites the pair. Rules that name three letters need the\nthird-from-last key too.\n\n```\n P B D -> PWD (fires on D after P+B)\n PD -> PWD (insert W, D continues) (fires on D after P)\n PF -> PS (fires on F after P)\n KG -> KL (fires on G after K)\n W M G -> WML (fires on G after W+M)\n MG -> LG (del M, L, G continues) (fires on G after M)\n JG -> JPG (insert P, G continues) (fires on G after J)\n WG -> WD (fires on G after W)\n KH -> KN (fires on H after K)\n GJ -> ...g + \"th\" (e.g. \"length\") (fires on J after G)\n WJ -> WL (fires on J after W)\n MK -> LK (del M, L, K continues) (fires on K after M)\n HK -> NK (del H, N, K continues) (fires on K after H)\n GM -> GL (fires on M after G)\n VM -> VL (fires on M after V)\n FP -> SP (del F, S, P continues) (fires on P after F)\n GV -> GT (fires on V after G)\n MV -> LV (del M, L, V continues) (fires on V after M)\n GW -> GD (fires on W after G)\n MW -> MP (fires on W after M)\n YB -> IB (del Y, I, B continues) (fires on B after Y)\n AE -> AU (keeps home row) (fires on E after A)\n```\n\n## Always on\n\n- **Linger brackets** — Tap ( or { for one bracket. Hold past 200 ms for the pair, caret inside: (|) `ramo_linger.c:20`\n- **Caps Word** — Letters get ⇧. Digits, -, _, ⌫ keep it on. Space → _, double space → real space + off. `ramo_linger.c:55`\n- **Shift + ⌫ = ⌦** — Keeps Delete reachable on Hands Down, where it left the thumb row. `ramo_process_record.c:45`\n- **Comma-leader shift** — Hands Down only. Type , then a letter within 200 ms → the comma goes, the letter gets ⇧. `ramo_process_record.c:60`\n- **Combo hold on qu** — Hold the W+M combo past 200 ms and the u is deleted, leaving q. `ramo_combos.c:164`\n- **Base switch** — NAV layer, two left keys. Double-tap only, so a stray press cannot change your base. `ramo_tap_dance.c:11`\n\n## Not drawn\n\nThe board has 46 switches. These 10 are left off:\n\n- Outer pinky column `LT5 LM5 LB5 RT5 RM5 RB5` — `KC_NO` on every layer.\n- Inner ex2 keys `LTA LMA RTA RMA`.\n\nCareful: these hidden positions do have something bound.\n\n- `CMK_LTA` is `LCTL_T(KC_A)`\n- `CMK_RTA` is `LCTL_T(KC_A)`\n- `CMK_LMA` is `LCTL_T(KC_A)`\n- `CMK_RMA` is `LCTL_T(KC_A)`",
+ "plate": true
+ },
+ [
+ {
+ "x": 2.0,
+ "c": "#424242",
+ "t": "#ffffff",
+ "a": 7,
+ "f": 3,
+ "g": false
+ },
+ "F\n\n\n\nHyper",
+ {
+ "x": 7.0
+ },
+ "U\n\n\n\nHyper"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0
+ },
+ "W",
+ {
+ "x": 1.0
+ },
+ "P\n\n\n\nMeh",
+ {
+ "x": 5.0
+ },
+ "L\n\n\n\nMeh",
+ {
+ "x": 1.0
+ },
+ "Y"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0
+ },
+ "G",
+ {
+ "x": 3.0
+ },
+ "J"
+ ],
+ [
+ {
+ "y": -0.9
+ },
+ "Q",
+ {
+ "x": 11.0,
+ "a": 5
+ },
+ "\"\n'"
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0,
+ "a": 7
+ },
+ "S\n\n\n\n⌘",
+ {
+ "x": 7.0
+ },
+ "E\n\n\n\n⌘"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0
+ },
+ "R\n\n\n\n⌥",
+ {
+ "x": 1.0
+ },
+ "T\n\n\n\n⇧",
+ {
+ "x": 5.0
+ },
+ "N\n\n\n\n⇧",
+ {
+ "x": 1.0
+ },
+ "I\n\n\n\n⌥"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0
+ },
+ "D",
+ {
+ "x": 3.0
+ },
+ "H"
+ ],
+ [
+ {
+ "y": -0.9
+ },
+ "A\n\n\n\n⌃",
+ {
+ "x": 11.0
+ },
+ "O\n\n\n\n⌃"
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0
+ },
+ "C",
+ {
+ "x": 7.0,
+ "a": 5
+ },
+ "<\n,"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "a": 7
+ },
+ "X",
+ {
+ "x": 1.0
+ },
+ "V",
+ {
+ "x": 5.0
+ },
+ "M",
+ {
+ "x": 1.0,
+ "a": 5
+ },
+ ">\n."
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "a": 7
+ },
+ "B",
+ {
+ "x": 3.0
+ },
+ "K"
+ ],
+ [
+ {
+ "y": -0.9
+ },
+ "Z",
+ {
+ "x": 11.0,
+ "a": 5
+ },
+ "?\n/"
+ ],
+ [
+ {
+ "y": -0.7,
+ "x": 5.0,
+ "a": 7,
+ "w": 3.0,
+ "d": true
+ },
+ "Colemak\n\n\n\nbase · CMK"
+ ],
+ [
+ {
+ "y": -0.25,
+ "x": 2.5
+ },
+ "Esc\n\n\n\nMedia",
+ "␣\n\n\n\nNav",
+ "Tab\n\n\n\nMouse",
+ {
+ "x": 2.0
+ },
+ "⏎\n\n\n\nSym",
+ "⌫\n\n\n\nNum",
+ "⌦\n\n\n\nFun"
+ ],
+ [
+ {
+ "y": 2.25,
+ "x": 2.0
+ },
+ "G\n\n\n\nHyper",
+ {
+ "x": 7.0,
+ "a": 5
+ },
+ "?\n/\n\n\nHyper"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "a": 7
+ },
+ "W",
+ {
+ "x": 1.0
+ },
+ "M\n\n\n\nMeh",
+ {
+ "x": 5.0,
+ "a": 5
+ },
+ ">\n.\n\n\nMeh",
+ {
+ "x": 1.0,
+ "a": 7
+ },
+ "\""
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0
+ },
+ "J",
+ {
+ "x": 3.0
+ },
+ "#"
+ ],
+ [
+ {
+ "y": -0.9
+ },
+ "V",
+ {
+ "x": 11.0,
+ "a": 5
+ },
+ "\"\n'"
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0,
+ "a": 7
+ },
+ "T\n\n\n\n⌘",
+ {
+ "x": 7.0
+ },
+ "E\n\n\n\n⌘"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0
+ },
+ "N\n\n\n\n⌥",
+ {
+ "x": 1.0
+ },
+ "H\n\n\n\n⇧",
+ {
+ "x": 5.0
+ },
+ "A\n\n\n\n⇧",
+ {
+ "x": 1.0
+ },
+ "I\n\n\n\n⌥"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0
+ },
+ "K",
+ {
+ "x": 3.0,
+ "a": 5
+ },
+ "<\n,"
+ ],
+ [
+ {
+ "y": -0.9,
+ "a": 7
+ },
+ "S\n\n\n\n⌃",
+ {
+ "x": 11.0
+ },
+ "C\n\n\n\n⌃"
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0
+ },
+ "D",
+ {
+ "x": 7.0
+ },
+ "O"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0
+ },
+ "P",
+ {
+ "x": 1.0
+ },
+ "L",
+ {
+ "x": 5.0
+ },
+ "U",
+ {
+ "x": 1.0
+ },
+ "Y"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0
+ },
+ "X",
+ {
+ "x": 3.0,
+ "a": 5
+ },
+ "_\n-"
+ ],
+ [
+ {
+ "y": -0.9,
+ "a": 7
+ },
+ "F",
+ {
+ "x": 11.0
+ },
+ "B"
+ ],
+ [
+ {
+ "y": -0.7,
+ "x": 5.0,
+ "w": 3.0,
+ "d": true
+ },
+ "HandsDown\n\n\n\nbase · HDP"
+ ],
+ [
+ {
+ "y": -0.25,
+ "x": 2.5
+ },
+ "Esc\n\n\n\nMedia",
+ "␣\n\n\n\nNav",
+ "Tab\n\n\n\nMouse",
+ {
+ "x": 2.0
+ },
+ "R\n\n\n\nSym",
+ "⌫\n\n\n\nNum",
+ "⏎\n\n\n\nFun"
+ ],
+ [
+ {
+ "y": 2.25,
+ "x": 2.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 7.0,
+ "g": false
+ },
+ "RGB hue"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 1.0
+ },
+ "",
+ {
+ "x": 5.0,
+ "g": false
+ },
+ "RGB mode",
+ {
+ "x": 1.0
+ },
+ "RGB sat"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 3.0,
+ "g": false
+ },
+ "RGB on"
+ ],
+ [
+ {
+ "y": -0.9,
+ "g": true
+ },
+ "",
+ {
+ "x": 11.0,
+ "g": false
+ },
+ "RGB val"
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0
+ },
+ "⌘",
+ {
+ "x": 7.0
+ },
+ "Vol−"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0
+ },
+ "⌥",
+ {
+ "x": 1.0
+ },
+ "⇧",
+ {
+ "x": 5.0
+ },
+ "Prev",
+ {
+ "x": 1.0
+ },
+ "Vol+"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 3.0
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "g": false
+ },
+ "⌃",
+ {
+ "x": 11.0
+ },
+ "Next"
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0
+ },
+ "AltGr",
+ {
+ "x": 7.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0
+ },
+ "",
+ {
+ "x": 1.0
+ },
+ "",
+ {
+ "x": 5.0
+ },
+ "",
+ {
+ "x": 1.0
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0
+ },
+ "",
+ {
+ "x": 3.0
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9
+ },
+ "",
+ {
+ "x": 11.0
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.7,
+ "x": 5.0,
+ "g": false,
+ "w": 3.0,
+ "d": true
+ },
+ "Media\n\n\n\nlayer · MED"
+ ],
+ [
+ {
+ "y": -0.25,
+ "x": 2.5,
+ "c": "#8c5656",
+ "g": true
+ },
+ "",
+ {
+ "c": "#424242"
+ },
+ "",
+ "",
+ {
+ "x": 2.0,
+ "g": false
+ },
+ "Stop",
+ "Play",
+ "Mute"
+ ],
+ [
+ {
+ "y": 2.25,
+ "x": 2.0
+ },
+ "2× CMK",
+ {
+ "x": 7.0
+ },
+ "⌘C"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 1.0,
+ "g": false
+ },
+ "2× HDP",
+ {
+ "x": 5.0
+ },
+ "⌘V",
+ {
+ "x": 1.0
+ },
+ "⌘X"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 3.0,
+ "g": false
+ },
+ "⌘Z"
+ ],
+ [
+ {
+ "y": -0.9,
+ "g": true
+ },
+ "",
+ {
+ "x": 11.0,
+ "g": false
+ },
+ "⇧⌘Z"
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0
+ },
+ "⌘",
+ {
+ "x": 7.0
+ },
+ "↓"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0
+ },
+ "⌥",
+ {
+ "x": 1.0
+ },
+ "⇧",
+ {
+ "x": 5.0
+ },
+ "←",
+ {
+ "x": 1.0
+ },
+ "↑"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 3.0,
+ "g": false
+ },
+ "Caps Word"
+ ],
+ [
+ {
+ "y": -0.9
+ },
+ "⌃",
+ {
+ "x": 11.0
+ },
+ "→"
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0
+ },
+ "AltGr",
+ {
+ "x": 7.0
+ },
+ "PgDn"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 1.0
+ },
+ "",
+ {
+ "x": 5.0,
+ "g": false
+ },
+ "Home",
+ {
+ "x": 1.0
+ },
+ "PgUp"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 3.0,
+ "g": false
+ },
+ "Ins"
+ ],
+ [
+ {
+ "y": -0.9,
+ "g": true
+ },
+ "",
+ {
+ "x": 11.0,
+ "g": false
+ },
+ "End"
+ ],
+ [
+ {
+ "y": -0.7,
+ "x": 5.0,
+ "w": 3.0,
+ "d": true
+ },
+ "Nav\n\n\n\nlayer · NAV"
+ ],
+ [
+ {
+ "y": -0.25,
+ "x": 2.5,
+ "g": true
+ },
+ "",
+ {
+ "c": "#8c5656"
+ },
+ "",
+ {
+ "c": "#424242"
+ },
+ "",
+ {
+ "x": 2.0,
+ "g": false
+ },
+ "⏎",
+ "⌫",
+ "⌦"
+ ],
+ [
+ {
+ "y": 2.25,
+ "x": 2.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 7.0,
+ "g": false
+ },
+ "⌘C"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 1.0
+ },
+ "",
+ {
+ "x": 5.0,
+ "g": false
+ },
+ "⌘V",
+ {
+ "x": 1.0
+ },
+ "⌘X"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 3.0,
+ "g": false
+ },
+ "⇧⌘Z"
+ ],
+ [
+ {
+ "y": -0.9,
+ "g": true
+ },
+ "",
+ {
+ "x": 11.0,
+ "g": false
+ },
+ "⌘Z"
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0
+ },
+ "⌘",
+ {
+ "x": 7.0
+ },
+ "Ms ↓"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0
+ },
+ "⌥",
+ {
+ "x": 1.0
+ },
+ "⇧",
+ {
+ "x": 5.0
+ },
+ "Ms ←",
+ {
+ "x": 1.0
+ },
+ "Ms ↑"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 3.0
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "g": false
+ },
+ "⌃",
+ {
+ "x": 11.0
+ },
+ "Ms →"
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0
+ },
+ "AltGr",
+ {
+ "x": 7.0
+ },
+ "Wh ↓"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 1.0
+ },
+ "",
+ {
+ "x": 5.0,
+ "g": false
+ },
+ "Wh ←",
+ {
+ "x": 1.0
+ },
+ "Wh ↑"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 3.0
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9
+ },
+ "",
+ {
+ "x": 11.0,
+ "g": false
+ },
+ "Wh →"
+ ],
+ [
+ {
+ "y": -0.7,
+ "x": 5.0,
+ "w": 3.0,
+ "d": true
+ },
+ "Mouse\n\n\n\nlayer · MOS"
+ ],
+ [
+ {
+ "y": -0.25,
+ "x": 2.5,
+ "g": true
+ },
+ "",
+ "",
+ {
+ "c": "#8c5656"
+ },
+ "",
+ {
+ "x": 2.0,
+ "c": "#424242",
+ "g": false
+ },
+ "RMB",
+ "LMB",
+ "MMB"
+ ],
+ [
+ {
+ "y": 2.25,
+ "x": 2.0
+ },
+ "*",
+ {
+ "x": 7.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "g": false
+ },
+ "&",
+ {
+ "x": 1.0
+ },
+ "(",
+ {
+ "x": 5.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 1.0
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "g": false
+ },
+ "}",
+ {
+ "x": 3.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "g": false
+ },
+ "{",
+ {
+ "x": 11.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0,
+ "g": false
+ },
+ "%",
+ {
+ "x": 7.0
+ },
+ "⌘"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0
+ },
+ "$",
+ {
+ "x": 1.0
+ },
+ "^",
+ {
+ "x": 5.0
+ },
+ "⇧",
+ {
+ "x": 1.0
+ },
+ "⌥"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0
+ },
+ "+",
+ {
+ "x": 3.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "g": false
+ },
+ ":",
+ {
+ "x": 11.0
+ },
+ "⌃"
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0
+ },
+ "@",
+ {
+ "x": 7.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "g": false
+ },
+ "!",
+ {
+ "x": 1.0
+ },
+ "#",
+ {
+ "x": 5.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 1.0,
+ "g": false
+ },
+ "AltGr"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0
+ },
+ "|",
+ {
+ "x": 3.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "g": false
+ },
+ "~",
+ {
+ "x": 11.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.7,
+ "x": 5.0,
+ "g": false,
+ "w": 3.0,
+ "d": true
+ },
+ "Sym\n\n\n\nlayer · SYM"
+ ],
+ [
+ {
+ "y": -0.25,
+ "x": 2.5
+ },
+ "(",
+ ")",
+ "_",
+ {
+ "x": 2.0,
+ "c": "#8c5656",
+ "g": true
+ },
+ "",
+ {
+ "c": "#424242"
+ },
+ "",
+ ""
+ ],
+ [
+ {
+ "y": 2.25,
+ "x": 2.0,
+ "a": 5,
+ "g": false
+ },
+ "*\n8",
+ {
+ "x": 7.0,
+ "a": 7,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "a": 5,
+ "g": false
+ },
+ "&\n7",
+ {
+ "x": 1.0
+ },
+ "(\n9",
+ {
+ "x": 5.0,
+ "a": 7,
+ "g": true
+ },
+ "",
+ {
+ "x": 1.0
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "a": 5,
+ "g": false
+ },
+ "}\n]",
+ {
+ "x": 3.0,
+ "a": 7,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "a": 5,
+ "g": false
+ },
+ "{\n[",
+ {
+ "x": 11.0,
+ "a": 7,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0,
+ "a": 5,
+ "g": false
+ },
+ "%\n5",
+ {
+ "x": 7.0,
+ "a": 7
+ },
+ "⌘"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "a": 5
+ },
+ "$\n4",
+ {
+ "x": 1.0
+ },
+ "^\n6",
+ {
+ "x": 5.0,
+ "a": 7
+ },
+ "⇧",
+ {
+ "x": 1.0
+ },
+ "⌥"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "a": 5
+ },
+ "+\n=",
+ {
+ "x": 3.0,
+ "a": 7,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "a": 5,
+ "g": false
+ },
+ ":\n;",
+ {
+ "x": 11.0,
+ "a": 7
+ },
+ "⌃"
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0,
+ "a": 5
+ },
+ "@\n2",
+ {
+ "x": 7.0,
+ "a": 7,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "a": 5,
+ "g": false
+ },
+ "!\n1",
+ {
+ "x": 1.0
+ },
+ "#\n3",
+ {
+ "x": 5.0,
+ "a": 7,
+ "g": true
+ },
+ "",
+ {
+ "x": 1.0,
+ "g": false
+ },
+ "AltGr"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "a": 5
+ },
+ "|\n\\",
+ {
+ "x": 3.0,
+ "a": 7,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "a": 5,
+ "g": false
+ },
+ "~\n`",
+ {
+ "x": 11.0,
+ "a": 7,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.7,
+ "x": 5.0,
+ "g": false,
+ "w": 3.0,
+ "d": true
+ },
+ "Num\n\n\n\nlayer · NUM"
+ ],
+ [
+ {
+ "y": -0.25,
+ "x": 2.5,
+ "a": 5
+ },
+ ">\n.",
+ ")\n0",
+ "_\n-",
+ {
+ "x": 2.0,
+ "a": 7,
+ "g": true
+ },
+ "",
+ {
+ "c": "#8c5656"
+ },
+ "",
+ {
+ "c": "#424242"
+ },
+ ""
+ ],
+ [
+ {
+ "y": 2.25,
+ "x": 2.0,
+ "g": false
+ },
+ "F8",
+ {
+ "x": 7.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "g": false
+ },
+ "F7",
+ {
+ "x": 1.0
+ },
+ "F9",
+ {
+ "x": 5.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 1.0
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0,
+ "g": false
+ },
+ "PrtSc",
+ {
+ "x": 3.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "g": false
+ },
+ "F12",
+ {
+ "x": 11.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0,
+ "g": false
+ },
+ "F5",
+ {
+ "x": 7.0
+ },
+ "⌘"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0
+ },
+ "F4",
+ {
+ "x": 1.0
+ },
+ "F6",
+ {
+ "x": 5.0
+ },
+ "⇧",
+ {
+ "x": 1.0
+ },
+ "⌥"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0
+ },
+ "ScrLk",
+ {
+ "x": 3.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "g": false
+ },
+ "F11",
+ {
+ "x": 11.0
+ },
+ "⌃"
+ ],
+ [
+ {
+ "y": -0.3,
+ "x": 2.0
+ },
+ "F2",
+ {
+ "x": 7.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 1.0,
+ "g": false
+ },
+ "F1",
+ {
+ "x": 1.0
+ },
+ "F3",
+ {
+ "x": 5.0,
+ "g": true
+ },
+ "",
+ {
+ "x": 1.0,
+ "g": false
+ },
+ "AltGr"
+ ],
+ [
+ {
+ "y": -0.9,
+ "x": 4.0
+ },
+ "Pause",
+ {
+ "x": 3.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.9,
+ "g": false
+ },
+ "F10",
+ {
+ "x": 11.0,
+ "g": true
+ },
+ ""
+ ],
+ [
+ {
+ "y": -0.7,
+ "x": 5.0,
+ "g": false,
+ "w": 3.0,
+ "d": true
+ },
+ "Fun\n\n\n\nlayer · FUN"
+ ],
+ [
+ {
+ "y": -0.25,
+ "x": 2.5
+ },
+ "▤",
+ "␣",
+ "Tab",
+ {
+ "x": 2.0,
+ "g": true
+ },
+ "",
+ "",
+ {
+ "c": "#8c5656"
+ },
+ ""
+ ]
+]
diff --git a/users/raphaelmor/hd-colemak.html b/users/raphaelmor/hd-colemak.html
new file mode 100644
index 00000000..02cb550c
--- /dev/null
+++ b/users/raphaelmor/hd-colemak.html
@@ -0,0 +1,358 @@
+
+
+
+
+
+Corne layers — current · full HD · proposed hybrid
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Proposed hybrid — Miryoku chassis + HD engine
+
+
+
+ alpha
+ mod (hold)
+ layer / switch
+ semantic / editing
+ symbol
+ number
+ mouse
+ media
+ RGB/cfg
+ NEW
+ CHG
+ +hold
+ removed
+
+
+
+
+
+
Engine features — code, not layout (✅ implemented)
+
+
✅ Adaptives — Hands Down base only
Full PM set (~20 rules): PF→PS, KG→KL, KH→KN, GJ→"th", MK→LK, GV→GT, AE→AU… Gated so they run only on the HD base.
ramo_adaptive.c (implemented)
+
✅ Combos
qu (P+L), z (N+H), and now th (T+H) live. More digraphs/symbols easy to add.
ramo_combos.c (implemented)
+
✅ Linger brackets — SYM
Hold ( or { → auto-pair, caret between: (|). Tap = single. ([ and " not yet — need a SYM home.)
ramo_linger.c · RAMO_LINGER_TERM
+
✅ Smart caps-word
Shifts letters, keeps digits/-/_, turns space→_, auto-exits on punctuation.
ramo_linger.c (implemented)
+
+
+
+
+
+
+
+
diff --git a/users/raphaelmor/layers/CMK-defs.h b/users/raphaelmor/layers/CMK-defs.h
index 1c6e16f5..ee36e572 100644
--- a/users/raphaelmor/layers/CMK-defs.h
+++ b/users/raphaelmor/layers/CMK-defs.h
@@ -1,3 +1,13 @@
+/*
+ * CMK-defs.h
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
#pragma once
// Override keys with HRM and Layer keys
diff --git a/users/raphaelmor/layers/FUN-defs.h b/users/raphaelmor/layers/FUN-defs.h
index 8a1b9dbf..ae3e56c0 100644
--- a/users/raphaelmor/layers/FUN-defs.h
+++ b/users/raphaelmor/layers/FUN-defs.h
@@ -1,3 +1,13 @@
+/*
+ * FUN-defs.h
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
#pragma once
// Override keys with HRM and Layer keys
diff --git a/users/raphaelmor/layers/HDG-defs.h b/users/raphaelmor/layers/HDG-defs.h
index 69388d57..1db5bd14 100644
--- a/users/raphaelmor/layers/HDG-defs.h
+++ b/users/raphaelmor/layers/HDG-defs.h
@@ -1,3 +1,13 @@
+/*
+ * HDG-defs.h
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
#pragma once
// Override keys with HRM and Layer keys
diff --git a/users/raphaelmor/layers/HDP-defs.h b/users/raphaelmor/layers/HDP-defs.h
index 1778b173..f6b3ab33 100644
--- a/users/raphaelmor/layers/HDP-defs.h
+++ b/users/raphaelmor/layers/HDP-defs.h
@@ -1,3 +1,13 @@
+/*
+ * HDP-defs.h
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
#pragma once
// Let's place these HD keycodes on the keymap
@@ -13,32 +23,34 @@
// ╰─────────────╯ ╰─────────────╯
//
//
-// Base (alpha) Layer Hands Down Promethium (HRMs /+ thumb mods)
+// Base (alpha) Layer Hands Down Promethium — canonical arrangement (moutis)
// ╭───────────────────────────────╮ ╭───────────────────────────────╮
-// │ ESC F P D L X LTA │ │ RTA ; U O Y B Z │
-// │ TAB S N T H K LMA │ │ RMA , A E I C Q │
-// │ \ V W G M J ╭─────╯ ╰─────╮ - . ' = / ENT │
-// ╰───────────╮ ESC TAB R │ │ SPC ENT BSP ╭───────────╯
+// │ · V W G M J LTA │ │ RTA # . / " ' · │
+// │ · S N T H K LMA │ │ RMA , A E I C · │
+// │ · F P D L X ╭─────╯ ╰─────╮ - U O Y B · │
+// ╰───────────╮ ESC SPC TAB │ │ R BSP ENT ╭───────────╯
// ╰─────────────╯ ╰─────────────╯
+// Home-row mods (my GACS): pinky ⌃ · ring ⌥ · middle ⌘ · index ⇧
+// Kept extras: Hyper on D/O, Meh on L/U · outer columns blank · Option-A thumbs
//
-#define HDP_LT5 KC_TAB
-#define HDP_LT4 KC_F
-#define HDP_LT3 KC_P
-#define HDP_LT2 HYPR_T(KC_D)
-#define HDP_LT1 MEH_T(KC_L)
-#define HDP_LT0 KC_X
+#define HDP_LT5 KC_NO // outer column unused
+#define HDP_LT4 KC_V
+#define HDP_LT3 KC_W
+#define HDP_LT2 HYPR_T(KC_G)
+#define HDP_LT1 MEH_T(KC_M)
+#define HDP_LT0 KC_J
#define HDP_LTA KC_NO // TODO: should be a real key once a use is found
#define HDP_RTA KC_NO // TODO: should be a real key once a use is found
-#define HDP_RT0 KC_SCLN
-#define HDP_RT1 MEH_T(KC_U)
-#define HDP_RT2 HYPR_T(KC_O)
-#define HDP_RT3 KC_Y
-#define HDP_RT4 KC_B
-#define HDP_RT5 KC_Z
+#define HDP_RT0 KC_HASH
+#define HDP_RT1 MEH_T(KC_DOT)
+#define HDP_RT2 HYPR_T(KC_SLSH)
+#define HDP_RT3 KC_DQUO
+#define HDP_RT4 KC_QUOT
+#define HDP_RT5 KC_NO // outer column unused (Z via N+H combo)
-#define HDP_LM5 KC_TAB
+#define HDP_LM5 KC_NO // outer column unused (Tab is on thumb)
#define HDP_LM4 LCTL_T(KC_S)
#define HDP_LM3 LALT_T(KC_N)
#define HDP_LM2 LGUI_T(KC_T)
@@ -52,29 +64,32 @@
#define HDP_RM2 RGUI_T(KC_E)
#define HDP_RM3 RALT_T(KC_I)
#define HDP_RM4 RCTL_T(KC_C)
-#define HDP_RM5 KC_Q
+#define HDP_RM5 KC_NO // outer column unused (Q via W+M combo)
-#define HDP_LB5 KC_BSLS
-#define HDP_LB4 KC_V
-#define HDP_LB3 KC_W
-#define HDP_LB2 KC_G
-#define HDP_LB1 KC_M
-#define HDP_LB0 KC_J
+#define HDP_LB5 KC_NO // outer column unused (\ is on NUM layer)
+#define HDP_LB4 KC_F
+#define HDP_LB3 KC_P
+#define HDP_LB2 KC_D
+#define HDP_LB1 KC_L
+#define HDP_LB0 KC_X
#define HDP_RB0 KC_MINS
-#define HDP_RB1 KC_DOT
-#define HDP_RB2 KC_QUOT
-#define HDP_RB3 KC_EQL
-#define HDP_RB4 KC_SLSH
-#define HDP_RB5 KC_ENT
+#define HDP_RB1 KC_U
+#define HDP_RB2 KC_O
+#define HDP_RB3 KC_Y
+#define HDP_RB4 KC_B
+#define HDP_RB5 KC_NO // outer column unused (Enter is on thumb)
//Primary Thumbs 1-3 (others are unique to the board)
-#define HDP_LH2 LT(L_MEDIA, KC_ESC)
-#define HDP_LH1 LT(L_NAV, KC_TAB)
-#define HDP_LH0 LT(L_MOUSE, KC_R)
-#define HDP_RH0 LT(L_SYM, KC_SPC)
-#define HDP_RH1 LT(L_NUM, KC_ENT)
-#define HDP_RH2 LT(L_FUN, KC_BSPC)
+// Option A: thumbs re-aligned to Colemak. Left hand now identical to Colemak;
+// only R (inner-right, Promethium's signature) and Enter (outer-right) differ.
+// Colemak's Del (outer-right) is not on a thumb here → use Shift+Bksp (see ramo_process_record.c).
+#define HDP_LH2 LT(L_MEDIA, KC_ESC) // Esc (= Colemak)
+#define HDP_LH1 LT(L_NAV, KC_SPC) // Space (= Colemak) [was Tab]
+#define HDP_LH0 LT(L_MOUSE, KC_TAB) // Tab (= Colemak) [was R]
+#define HDP_RH0 LT(L_SYM, KC_R) // R (Promethium R-on-thumb) [was Space]
+#define HDP_RH1 LT(L_NUM, KC_BSPC) // Bksp (= Colemak) [was Enter]
+#define HDP_RH2 LT(L_FUN, KC_ENT) // Enter (Colemak has Del here) [was Bksp]
#define HDP_RGB_LT5 BLACK
#define HDP_RGB_LT4 ORANGE
@@ -131,9 +146,13 @@
#define HDP_RGB_RH2 MAGENTA
-// type p + l to type q
+// type w + m to type qu (canonical PM position)
#define HD_Q_keys HDP_LT3, HDP_LT1
// type n + h to type z
#define HD_Z_keys HDP_LM3, HDP_LM1
+// Enable the H-digraph roll-combo on Promethium
+#define USE_HD_H_DIGRAPH_COMBO
+#define HD_Th_keys HDP_LM2, HDP_LM1 // T + H -> "th"
+
diff --git a/users/raphaelmor/layers/MED-defs.h b/users/raphaelmor/layers/MED-defs.h
index f03dbb07..fd4e08c5 100644
--- a/users/raphaelmor/layers/MED-defs.h
+++ b/users/raphaelmor/layers/MED-defs.h
@@ -1,3 +1,13 @@
+/*
+ * MED-defs.h
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
#pragma once
// Override keys with HRM and Layer keys
@@ -26,11 +36,11 @@
#define MED_LTA KC_NO // TODO: should be a real key once a use is found
#define MED_RTA KC_NO // TODO: should be a real key once a use is found
-#define MED_RT0 RGB_TOG
-#define MED_RT1 RGB_MOD
-#define MED_RT2 RGB_HUI
-#define MED_RT3 RGB_SAI
-#define MED_RT4 RGB_VAI
+#define MED_RT0 RM_TOGG
+#define MED_RT1 RM_NEXT
+#define MED_RT2 RM_HUEU
+#define MED_RT3 RM_SATU
+#define MED_RT4 RM_VALU
#define MED_RT5 MED_NO
#define MED_LM5 NAV_NO
diff --git a/users/raphaelmor/layers/MOS-defs.h b/users/raphaelmor/layers/MOS-defs.h
index 7c02dc4b..0e4976c5 100644
--- a/users/raphaelmor/layers/MOS-defs.h
+++ b/users/raphaelmor/layers/MOS-defs.h
@@ -1,3 +1,13 @@
+/*
+ * MOS-defs.h
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
#pragma once
// Override keys with HRM and Layer keys
@@ -42,10 +52,10 @@
#define MOS_RMA KC_NO // TODO: should be a real key once a use is found
#define MOS_RM0 MOS_NO
-#define MOS_RM1 KC_MS_L
-#define MOS_RM2 KC_MS_D
-#define MOS_RM3 KC_MS_U
-#define MOS_RM4 KC_MS_R
+#define MOS_RM1 MS_LEFT
+#define MOS_RM2 MS_DOWN
+#define MOS_RM3 MS_UP
+#define MOS_RM4 MS_RGHT
#define MOS_RM5 MOS_NO
#define MOS_LB5 MOS_NO
@@ -56,10 +66,10 @@
#define MOS_LB0 MOS_NO
#define MOS_RB0 MOS_NO
-#define MOS_RB1 KC_WH_L
-#define MOS_RB2 KC_WH_D
-#define MOS_RB3 KC_WH_U
-#define MOS_RB4 KC_WH_R
+#define MOS_RB1 MS_WHLL
+#define MOS_RB2 MS_WHLD
+#define MOS_RB3 MS_WHLU
+#define MOS_RB4 MS_WHLR
#define MOS_RB5 MOS_NO
//Primary Thumbs 1-3 (others are unique to the board)
@@ -67,9 +77,9 @@
#define MOS_LH1 MOS_NO
#define MOS_LH0 MOS_NO
-#define MOS_RH0 KC_BTN2
-#define MOS_RH1 KC_BTN1
-#define MOS_RH2 KC_BTN3
+#define MOS_RH0 MS_BTN2
+#define MOS_RH1 MS_BTN1
+#define MOS_RH2 MS_BTN3
diff --git a/users/raphaelmor/layers/NAV-defs.h b/users/raphaelmor/layers/NAV-defs.h
index 835cffdf..16a82ce9 100644
--- a/users/raphaelmor/layers/NAV-defs.h
+++ b/users/raphaelmor/layers/NAV-defs.h
@@ -1,3 +1,13 @@
+/*
+ * NAV-defs.h
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
#pragma once
// Override keys with HRM and Layer keys
diff --git a/users/raphaelmor/layers/NUM-defs.h b/users/raphaelmor/layers/NUM-defs.h
index e6146258..fdaddb37 100644
--- a/users/raphaelmor/layers/NUM-defs.h
+++ b/users/raphaelmor/layers/NUM-defs.h
@@ -1,3 +1,13 @@
+/*
+ * NUM-defs.h
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
#pragma once
// Override keys with HRM and Layer keys
diff --git a/users/raphaelmor/layers/SYM-defs.h b/users/raphaelmor/layers/SYM-defs.h
index affa19ed..2ad0c23e 100644
--- a/users/raphaelmor/layers/SYM-defs.h
+++ b/users/raphaelmor/layers/SYM-defs.h
@@ -1,3 +1,13 @@
+/*
+ * SYM-defs.h
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
#pragma once
// Override keys with HRM and Layer keys
diff --git a/users/raphaelmor/ramo_adaptive.c b/users/raphaelmor/ramo_adaptive.c
index 84804530..e94c8083 100644
--- a/users/raphaelmor/ramo_adaptive.c
+++ b/users/raphaelmor/ramo_adaptive.c
@@ -1,61 +1,139 @@
+/*
+ * ramo_adaptive.c
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
bool process_adaptive_key(uint16_t keycode, const keyrecord_t *record) {
bool result = true;
- if (timer_elapsed(prior_keydown_timer) > RAMO_ADAPTIVE_TERM) { // outside adaptive threshhold
- prior_keycode = prior_keydown_timer = 0; // turn off Adaptives.
- return true; // no adaptive conditions, so return.
+ // Adaptives are tuned to Hands Down Promethium's physical rolls,
+ // so only run them while the Hands Down base is the active default layer.
+ // (On Colemak they'd "fix" bigrams that aren't even neighbors.)
+ if (get_highest_layer(default_layer_state) != L_HANDSDOWN) {
+ return true;
}
- // We're within Adaptive time window
+
+ // Window is measured keydown-to-keydown using record->event.time, NOT
+ // timer_read(). For a mod-tap/layer-tap, process_record_user only sees the
+ // record once the tap resolves (on release, or after TAPPING_TERM), so
+ // timer_read() here would measure prior-keydown → *this key's release* and
+ // charge the hold time against the window. event.time is stamped when the
+ // key was physically pressed, so 12 mod-tap keys (G M . / + both home rows)
+ // get the same window as plain keys.
+ if (TIMER_DIFF_16(record->event.time, prior_keydown_timer) > RAMO_ADAPTIVE_TERM) { // outside adaptive window
+ prior_keycode = preprior_keycode = prior_keydown_timer = 0; // turn off adaptives, reset
+ return true; // no adaptive conditions, continue processing.
+ }
+ // We're within the adaptive rolling window.
+
switch (keycode) {
+
+ // ---------------- left hand ----------------
+ case KC_D:
+ switch (prior_keycode) {
+ case KC_B:
+ if (preprior_keycode == KC_P) { // P B D -> PWD
+ RAMO_TAP(KC_BSPC); RAMO_TAP(KC_W); // del B, W, D continues
+ }
+ break;
+ case KC_P: RAMO_TAP(KC_W); break; // PD -> PWD (insert W, D continues)
+ }
+ break;
+ case KC_F:
+ switch (prior_keycode) {
+ case KC_P: RAMO_TAP(KC_S); result = false; break; // PF -> PS
+ }
+ break;
+ case KC_G:
+ switch (prior_keycode) {
+ case KC_K: RAMO_TAP(KC_L); result = false; break; // KG -> KL
+ case KC_M:
+ if (preprior_keycode == KC_W) { // W M G -> WML
+ RAMO_TAP(KC_L); result = false; // G becomes L
+ break;
+ }
+ RAMO_TAP(KC_BSPC); RAMO_TAP(KC_L); break; // MG -> LG (del M, L, G continues)
+ case KC_J: RAMO_TAP(KC_P); break; // JG -> JPG (insert P, G continues)
+ case KC_W: RAMO_TAP(KC_D); result = false; break; // WG -> WD
+ }
+ break;
+ case KC_H:
+ switch (prior_keycode) {
+ case KC_K: RAMO_TAP(KC_N); result = false; break; // KH -> KN
+ }
+ break;
+ case KC_J:
+ switch (prior_keycode) {
+ case KC_G: RAMO_SEND_STRING("th"); result = false; break; // GJ -> ...g + "th" (e.g. "length")
+ // V J removed: V+J is the ˜ tilde combo (HD_tilde_combo), same
+ // unreachability as W M above. W J kept — no combo on those keys.
+ case KC_W: RAMO_TAP(KC_L); result = false; break; // WJ -> WL
+ }
+ break;
+ case KC_K:
+ switch (prior_keycode) {
+ case KC_M: RAMO_TAP(KC_BSPC); RAMO_TAP(KC_L); break; // MK -> LK (del M, L, K continues)
+ case KC_H: RAMO_TAP(KC_BSPC); RAMO_TAP(KC_N); break; // HK -> NK (del H, N, K continues)
+ }
+ break;
+ case KC_M:
+ switch (prior_keycode) {
+ case KC_G: // GM -> GL
+ case KC_V: RAMO_TAP(KC_L); result = false; break; // VM -> VL
+ // W M (and the 3-key M W M) removed: W+M is the `qu` combo
+ // (HD_Q_keys). Fast enough to roll for the adaptive is also fast
+ // enough to land inside COMBO_TERM, so `qu` won and the adaptive
+ // was unreachable. Kept the combo — Q needs a home.
+ }
+ break;
+ case KC_P:
+ switch (prior_keycode) {
+ case KC_F: RAMO_TAP(KC_BSPC); RAMO_TAP(KC_S); break; // FP -> SP (del F, S, P continues)
+ }
+ break;
+ case KC_V:
+ switch (prior_keycode) {
+ case KC_G: RAMO_TAP(KC_T); result = false; break; // GV -> GT
+ case KC_M: RAMO_TAP(KC_BSPC); RAMO_TAP(KC_L); break; // MV -> LV (del M, L, V continues)
+ }
+ break;
+ case KC_W:
+ switch (prior_keycode) {
+ case KC_G: RAMO_TAP(KC_D); result = false; break; // GW -> GD
+ case KC_M: RAMO_TAP(KC_P); result = false; break; // MW -> MP
+ }
+ break;
+
+ // ---------------- right hand ----------------
+ case KC_B:
+ switch (prior_keycode) {
+ case KC_Y: RAMO_TAP(KC_BSPC); RAMO_TAP(KC_I); break; // YB -> IB (del Y, I, B continues)
+ }
+ break;
case KC_E:
switch (prior_keycode) {
- case KC_A: // "AE" yields "AU" (8x more common) keeping it on home row
- tap_code(KC_U);
- result = false; // done.
- break;
+ case KC_A: RAMO_TAP(KC_U); result = false; break; // AE -> AU (keeps home row)
}
break;
}
- // true: did not trigger adaptive. Continue processing.
- // false: did trigger adaptive. Do not continue.
+ // true: no adaptive triggered, continue processing.
+ // false: adaptive replaced the key, stop processing.
return result;
}
+
/*
-PBD -> PWD
+ Skipped by choice (low value on this HDP arrangement):
+ P/B/S then L -> unshifted L
-PF -> PS
-
-KG -> KL
-WMG -> WML
-MG -> LG
-JG -> JPG
-WG -> WD
-
-KH -> KN
-
-GJ -> GTH
-VJ -> VL
-WJ -> WL
-
-MK -> LK
-HK -> NK
-
-GM -> GL
-VM -> VL
-MWM -> MWL
-(.)WM -> (.)LM
-
-FP -> SP
-
-GV -> GT
-MV -> LV
-
-GW -> GD
-MW -> MP
-
-YB -> IB
-AE -> AU
+ Removed — unreachable, the same two keys are a combo (combos resolve in
+ pre_process_record_quantum, before this engine ever runs):
+ W M -> lm and M W M -> MWL (W+M is the `qu` combo)
+ V J -> VL (V+J is the ˜ tilde combo)
*/
diff --git a/users/raphaelmor/ramo_combos.c b/users/raphaelmor/ramo_combos.c
index 252fa6c7..52a8cbb6 100644
--- a/users/raphaelmor/ramo_combos.c
+++ b/users/raphaelmor/ramo_combos.c
@@ -1,3 +1,13 @@
+/*
+ * ramo_combos.c
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
// combos
#ifdef USE_HD_H_DIGRAPH_COMBO
const uint16_t PROGMEM HD_Th_combo[] = {HD_Th_keys, COMBO_END}; // TYPE "th" in hands down
@@ -6,6 +16,33 @@ const uint16_t PROGMEM HD_Qu_combo[] = {HD_Q_keys, COMBO_END}; // TYPE "qu" (Lin
const uint16_t PROGMEM HD_Z_combo[] = {HD_Z_keys, COMBO_END}; // TYPE "z"
const uint16_t PROGMEM CMK_Th_combo[] = {CMK_Th_keys, COMBO_END}; // TYPE "th" in colemak
+// --- Hands Down base only (gated in combo_should_trigger) -------------------
+// All at moutis' CANONICAL PM positions (layout now matches). Diacritics/ligatures
+// assume the stock US Mac layout (⌥-dead-keys).
+// 2g diacritics — tap the combo (dead key / glyph), then type the letter:
+const uint16_t PROGMEM HD_acut_combo[] = {HDP_RM1, HDP_RM2, COMBO_END}; // A+E → ´ (⌥E) é
+const uint16_t PROGMEM HD_grv_combo[] = {HDP_RM2, HDP_RM3, COMBO_END}; // E+I → ` (⌥`) è
+const uint16_t PROGMEM HD_circ_combo[] = {HDP_RM1, HDP_RM3, COMBO_END}; // A+I → ˆ (⌥I) ê
+const uint16_t PROGMEM HD_umla_combo[] = {HDP_RB1, HDP_RB2, COMBO_END}; // U+O → ¨ (⌥U) ü
+const uint16_t PROGMEM HD_tilde_combo[] = {HDP_LT4, HDP_LT0, COMBO_END}; // V+J → ˜ (⌥N) ñ
+const uint16_t PROGMEM HD_cedil_combo[] = {HDP_LM4, HDP_LM2, COMBO_END}; // S+T → ç (⌥C direct)
+// 2f ligatures:
+const uint16_t PROGMEM HD_oe_combo[] = {HDP_RM2, HDP_RB2, COMBO_END}; // E+O → œ (⌥Q)
+const uint16_t PROGMEM HD_ae_combo[] = {HDP_RM1, HDP_RB1, COMBO_END}; // A+U → æ (⌥')
+// 2c whitespace (Tab/Enter already on thumbs → only these):
+const uint16_t PROGMEM HD_stab_combo[] = {HDP_LT4, HDP_LT3, HDP_LT2, COMBO_END}; // V+W+G → Shift-Tab
+const uint16_t PROGMEM HD_app_combo[] = {HDP_LT1, HDP_LT0, COMBO_END}; // M+J → App menu
+// 2e symbols — canonical top-right punctuation cluster (# . / " '):
+const uint16_t PROGMEM HD_scln_combo[] = {HDP_RM0, HDP_RM1, COMBO_END}; // ,+A → ;
+const uint16_t PROGMEM HD_coln_combo[] = {HDP_RT1, HDP_RT3, COMBO_END}; // .+" → :
+const uint16_t PROGMEM HD_exlm_combo[] = {HDP_RT1, HDP_RT2, COMBO_END}; // .+/ → !
+const uint16_t PROGMEM HD_ques_combo[] = {HDP_RT2, HDP_RT3, COMBO_END}; // /+" → ?
+// 2h caps word — canonical position (moutis: H_CAPW_combo on HD_RB3+HD_RB4).
+// Same two keys as the YB → IB adaptive, but combos are resolved in
+// pre_process_record_quantum before process_record_user ever runs: a
+// simultaneous press is a combo, a sequential roll is the adaptive.
+const uint16_t PROGMEM HD_capw_combo[] = {HDP_RB3, HDP_RB4, COMBO_END}; // Y+B → Caps Word
+
enum ramo_combos {
NO_COMBO = 0, // Sentinel value (0) to signify no combo is currently pressed
#ifdef USE_HD_H_DIGRAPH_COMBO
@@ -14,6 +51,12 @@ enum ramo_combos {
HD_COMBO_QU,
HD_COMBO_Z,
CMK_COMBO_Th,
+ // Hands Down base only (contiguous — see combo_should_trigger)
+ HD_COMBO_ACUT, HD_COMBO_GRV, HD_COMBO_CIRC, HD_COMBO_UMLA, HD_COMBO_TILDE, HD_COMBO_CEDIL,
+ HD_COMBO_OE, HD_COMBO_AE,
+ HD_COMBO_STAB, HD_COMBO_APP,
+ HD_COMBO_SCLN, HD_COMBO_COLN, HD_COMBO_QUES, HD_COMBO_EXLM,
+ HD_COMBO_CAPW, // keep last: combo_should_trigger gates ACUT ... CAPW
};
combo_t key_combos[] = {
@@ -25,8 +68,34 @@ combo_t key_combos[] = {
[HD_COMBO_Z] = COMBO(HD_Z_combo, KC_Z),
// Colemak combos
[CMK_COMBO_Th] = COMBO_ACTION(CMK_Th_combo),
+ // Hands Down base only (gated in combo_should_trigger)
+ [HD_COMBO_ACUT] = COMBO(HD_acut_combo, A(KC_E)),
+ [HD_COMBO_GRV] = COMBO(HD_grv_combo, A(KC_GRV)),
+ [HD_COMBO_CIRC] = COMBO(HD_circ_combo, A(KC_I)),
+ [HD_COMBO_UMLA] = COMBO(HD_umla_combo, A(KC_U)),
+ [HD_COMBO_TILDE] = COMBO(HD_tilde_combo, A(KC_N)),
+ [HD_COMBO_CEDIL] = COMBO(HD_cedil_combo, A(KC_C)),
+ [HD_COMBO_OE] = COMBO(HD_oe_combo, A(KC_Q)),
+ [HD_COMBO_AE] = COMBO(HD_ae_combo, A(KC_QUOT)),
+ [HD_COMBO_STAB] = COMBO(HD_stab_combo, S(KC_TAB)),
+ [HD_COMBO_APP] = COMBO(HD_app_combo, KC_APP),
+ [HD_COMBO_SCLN] = COMBO(HD_scln_combo, KC_SCLN),
+ [HD_COMBO_COLN] = COMBO(HD_coln_combo, KC_COLN),
+ [HD_COMBO_QUES] = COMBO(HD_ques_combo, KC_QUES),
+ [HD_COMBO_EXLM] = COMBO(HD_exlm_combo, KC_EXLM),
+ [HD_COMBO_CAPW] = COMBO(HD_capw_combo, CW_TOGG),
};
+// Restrict the added diacritic/ligature/symbol/whitespace combos to the Hands
+// Down base; qu/z/th keep their existing (both-base) behavior.
+bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record) {
+ switch (combo_index) {
+ case HD_COMBO_ACUT ... HD_COMBO_CAPW:
+ return get_highest_layer(default_layer_state) == L_HANDSDOWN;
+ }
+ return true;
+}
+
uint8_t current_combo = 0; // for combo actions to hold before triggering
uint8_t ramo_process_combo_pressed(uint16_t combo_index);
@@ -57,23 +126,23 @@ uint8_t ramo_process_combo_pressed(uint16_t combo_index) {
#endif
case CMK_COMBO_Th:
if (is_caps_word_on()) {
- tap_code16(S(KC_T)); // send "T"
- tap_code16(S(KC_H)); // send "H"
+ RAMO_TAP16(S(KC_T)); // send "T"
+ RAMO_TAP16(S(KC_H)); // send "H"
} else {
- tap_code(KC_T); // send "T" honoring caps
+ RAMO_TAP(KC_T); // send "T" honoring caps
unregister_mods(MOD_MASK_SHIFT);
- tap_code(KC_H); // send "h" honoring CAPSLK state
+ RAMO_TAP(KC_H); // send "h" honoring CAPSLK state
}
break;
case HD_COMBO_QU:
- println("RAMO: Pressed N + H to type qu");
+ println("RAMO: W + M -> qu");
if (is_caps_word_on()) {
- tap_code16(S(KC_Q));
- tap_code16(S(KC_U));
+ RAMO_TAP16(S(KC_Q));
+ RAMO_TAP16(S(KC_U));
} else {
- tap_code16(KC_Q);
+ RAMO_TAP16(KC_Q);
unregister_mods(MOD_MASK_SHIFT);
- tap_code16(KC_U);
+ RAMO_TAP16(KC_U);
}
return combo_index; // if held, delete the 'u' in matrix_scan_user_process_combo
}
@@ -88,7 +157,7 @@ void ramo_process_combo_released(uint16_t combo_index) {
case CMK_COMBO_Th:
break;
case HD_COMBO_QU:
- println("RAMO: Released P + L to type qu");
+ println("RAMO: released W + M (qu)");
}
}
@@ -99,7 +168,7 @@ void combo_check_for_linger(void) {
clear_mods();
switch (current_combo) {
case HD_COMBO_QU:
- tap_code16(KC_BSPC); // held, so delete u
+ RAMO_TAP16(KC_BSPC); // held, so delete u
break;
}
current_combo = NO_COMBO;
diff --git a/users/raphaelmor/ramo_layers.h b/users/raphaelmor/ramo_layers.h
index 97307d15..fb3f6f8d 100644
--- a/users/raphaelmor/ramo_layers.h
+++ b/users/raphaelmor/ramo_layers.h
@@ -1,3 +1,13 @@
+/*
+ * ramo_layers.h
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
#include "layers/CMK-defs.h"
#ifdef RAMO_USE_HD_PROMETHIUM
#include "layers/HDP-defs.h"
diff --git a/users/raphaelmor/ramo_ledmap.c b/users/raphaelmor/ramo_ledmap.c
index 2f0bf1e7..c0135bd2 100644
--- a/users/raphaelmor/ramo_ledmap.c
+++ b/users/raphaelmor/ramo_ledmap.c
@@ -1,3 +1,12 @@
+/*
+ * ramo_ledmap.c
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
// Macro that defines a ledmap per layer (e.g. CMK_RGB_LT5, NAV_RGB_LB1)
// Then each layer simply has to define the derired color per key
diff --git a/users/raphaelmor/ramo_linger.c b/users/raphaelmor/ramo_linger.c
new file mode 100644
index 00000000..4c590061
--- /dev/null
+++ b/users/raphaelmor/ramo_linger.c
@@ -0,0 +1,107 @@
+/*
+ * ramo_linger.c
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
+// Linger brackets + smart Caps Word (casemods)
+//
+// Linger: tap ( or { for a normal single bracket; HOLD it past RAMO_LINGER_TERM
+// and it auto-closes with the caret between the pair: (|) {|} .
+// We consume the press and TAP the open bracket immediately (so the key is never
+// held in the HID report → no OS auto-repeat), then add the closing bracket + Left
+// once the hold threshold is reached. No event deferral, no roll-over ordering bugs.
+
+// returns true if it consumed the key press (caller should stop processing).
+bool linger_track(uint16_t keycode, keyrecord_t *record) {
+ if (record->event.pressed) {
+ uint16_t close = 0;
+ switch (keycode) {
+ case KC_LPRN: close = KC_RPRN; break;
+ case KC_LCBR: close = KC_RCBR; break;
+ }
+ if (close) {
+ RAMO_TAP16(keycode); // type the open bracket now (tap → no auto-repeat)
+ linger_key = keycode;
+ linger_close = close;
+ linger_ktimer = timer_read();
+ linger_fired = false;
+ return true; // consumed
+ }
+ linger_key = 0; // any other key press cancels a pending linger
+ } else if (keycode == linger_key) {
+ linger_key = 0; // released before/after threshold
+ }
+ return false;
+}
+
+void linger_scan(void) {
+ if (linger_key && !linger_fired && timer_elapsed(linger_ktimer) > RAMO_LINGER_TERM) {
+ RAMO_TAP16(linger_close); // add the closing bracket
+ RAMO_TAP16(KC_LEFT); // move caret between the pair
+ linger_fired = true;
+ }
+}
+
+// Smart Caps Word: letters get shifted; digits / _ / - / bspc keep it on;
+// anything else ends it. Space is handled by caps_word_space_track() below
+// (space -> _, double space -> real space + exit), so KC_SPC deliberately does
+// NOT appear here: if a space ever reaches this hook the intercept missed it,
+// and ending the word is the right fallback.
+bool caps_word_press_user(uint16_t keycode) {
+ switch (keycode) {
+ case KC_A ... KC_Z:
+ add_weak_mods(MOD_BIT(KC_LSFT)); // shift the letter
+ return true;
+ case KC_1 ... KC_0:
+ case KC_BSPC:
+ case KC_DEL:
+ case KC_MINS:
+ case KC_UNDS:
+ return true; // keep Caps Word active, don't shift
+ default:
+ return false; // turn Caps Word off
+ }
+}
+
+// --- Caps Word space handling (SCREAMING_SNAKE) ------------------------------
+// space -> `_`, Caps Word stays on
+// space space -> deletes the `_`, sends a real space, Caps Word turns off
+//
+// This lives in process_record_user rather than caps_word_press_user because
+// that hook can't suppress a keypress — it can only allow it or end the word.
+//
+// NOTE: the space key is LT(L_NAV, KC_SPC) on both bases, so this only sees
+// KC_SPC because process_record_user strips mod-taps/layer-taps to their tap
+// keycode first. Keep this call *after* that strip.
+static bool cw_space_pending = false; // did the last press emit a `_`?
+
+// QMK calls this whenever Caps Word turns on or off (including idle timeout),
+// so the pending-space state can never outlive the word.
+void caps_word_set_user(bool active) {
+ cw_space_pending = false;
+}
+
+// returns true if it consumed the key press (caller should stop processing).
+bool caps_word_space_track(uint16_t keycode, keyrecord_t *record) {
+ if (!record->event.pressed || !is_caps_word_on()) {
+ return false;
+ }
+ if (keycode != KC_SPC) {
+ cw_space_pending = false; // any other key ends the double-space window
+ return false;
+ }
+ if (cw_space_pending) { // second space in a row: end the word
+ RAMO_TAP(KC_BSPC); // remove the `_` we just typed
+ caps_word_off(); // clears cw_space_pending via caps_word_set_user
+ RAMO_TAP(KC_SPC); // ...and leave a real space behind
+ return true;
+ }
+ RAMO_TAP16(KC_UNDS); // first space: SCREAMING_SNAKE separator
+ cw_space_pending = true;
+ return true;
+}
diff --git a/users/raphaelmor/ramo_matrix.c b/users/raphaelmor/ramo_matrix.c
index 65a18ca6..e378290f 100644
--- a/users/raphaelmor/ramo_matrix.c
+++ b/users/raphaelmor/ramo_matrix.c
@@ -1,4 +1,14 @@
+/*
+ * ramo_matrix.c
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
void matrix_scan_user(void) {
combo_check_for_linger();
+ linger_scan();
}
diff --git a/users/raphaelmor/ramo_process_record.c b/users/raphaelmor/ramo_process_record.c
index 0ad834ca..81fdf108 100644
--- a/users/raphaelmor/ramo_process_record.c
+++ b/users/raphaelmor/ramo_process_record.c
@@ -1,10 +1,24 @@
+/*
+ * ramo_process_record.c
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
// This is called everytime a key is pressed or released
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
- bool result = true;
- // uint8_t saved_modifiers;
- // saved_modifiers = get_mods(); // share modifiers
-
// Do we need to filter multi-function keys?
+
+
+ if (record->event.pressed) {
+ uprintf("RAMO: USER: Pressed kc: %s\n", get_keycode_string(keycode));
+ } else {
+ uprintf("RAMO: USER: Released kc: %s\n", get_keycode_string(keycode));
+ }
+
switch (keycode) {
case QK_MOD_TAP ... QK_MOD_TAP_MAX:
case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
@@ -12,20 +26,66 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
if (!record->tap.count) // if not tapped yet…
return true; // let QMK do that first
keycode &= QK_BASIC_MAX; // mods & taps have been handled.
+ uprintf("RAMO: USER: Key is complex -> Keycode &= QK_BASIC_MAX: %s\n", get_keycode_string(keycode));
+ }
+
+ // Linger brackets: track ( and { presses for hold-to-autopair (see ramo_linger.c)
+ if (linger_track(keycode, record)) {
+ return false; // consumed: open bracket already tapped
+ }
+
+ // Caps Word spaces: space -> _, double space -> real space + exit.
+ // (see caps_word_space_track in ramo_linger.c)
+ if (caps_word_space_track(keycode, record)) {
+ return false;
+ }
+
+ // Shift + Backspace = Delete. Keeps Del available on the Hands Down base,
+ // where it's no longer on a thumb (Option A). Only fires with shift alone.
+ if (record->event.pressed && keycode == KC_BSPC
+ && (get_mods() & MOD_MASK_SHIFT) && !(get_mods() & ~MOD_MASK_SHIFT)) {
+ uint8_t saved = get_mods();
+ clear_mods();
+ RAMO_TAP(KC_DEL);
+ set_mods(saved);
+ return false;
+ }
+
+ // ADAPT_SHIFT (Hands Down base only): comma-leader one-shot Shift.
+ // Type `,` then a letter within RAMO_ADAPT_SHIFT_TERM → delete the comma and
+ // capitalize the letter (", the" -> "The"). Skipped while Caps Word is active.
+ // Gated to HDP because `,` without a trailing space is common in code (arg lists).
+ if (record->event.pressed
+ && get_highest_layer(default_layer_state) == L_HANDSDOWN
+ && prior_keycode == KC_COMM
+ && !is_caps_word_on()
+ && TIMER_DIFF_16(record->event.time, prior_keydown_timer) <= RAMO_ADAPT_SHIFT_TERM
+ && keycode >= KC_A && keycode <= KC_Z) {
+ RAMO_TAP(KC_BSPC); // remove the leader comma
+ RAMO_TAP16(S(keycode)); // send the capitalized letter
+ preprior_keycode = prior_keycode; // keep history consistent for 3-key rules
+ prior_keycode = keycode; // record for the next adaptive check
+ prior_keydown_timer = record->event.time;
+ return false; // handled
}
if (record->event.pressed) {
if (!process_adaptive_key(keycode, record)) { // false means we handled the key press
- prior_keycode = keycode &= QK_BASIC_MAX; // this keycode is stripped of mods+taps
- prior_keydown_timer = timer_read(); // reset the adaptive timer
+ preprior_keycode = prior_keycode;
+ prior_keycode = keycode; // this keycode is stripped of mods+taps
+ // Store the PRESS time, not timer_read(): mod-taps arrive late (see
+ // the note in ramo_adaptive.c), and using read-time here would make
+ // the *next* key's window start from this key's release instead.
+ prior_keydown_timer = record->event.time;
+
+ uprintf("RAMO: USER: Adaptive succeed. Setting prior to %s and reset timer\n", get_keycode_string(prior_keycode));
return false; // took care of that key
}
- // RAMO: is this needed ?
- // prior_keycode = keycode &= QK_BASIC_MAX; // this keycode is stripped of mods+taps
- // prior_keydown_timer = timer_read(); // reset the adaptive timer
- } else {
-
+ preprior_keycode = prior_keycode;
+ prior_keycode = keycode; // this keycode is stripped of mods+taps
+ prior_keydown_timer = record->event.time; // press time (see above)
+ uprintf("RAMO: USER: Adaptive failed. setting prior to %s and reset timer\n", get_keycode_string(prior_keycode));
}
- return result;
+ return true;
}
diff --git a/users/raphaelmor/ramo_tap_dance.c b/users/raphaelmor/ramo_tap_dance.c
index a8512806..6727604d 100644
--- a/users/raphaelmor/ramo_tap_dance.c
+++ b/users/raphaelmor/ramo_tap_dance.c
@@ -1,3 +1,13 @@
+/*
+ * ramo_tap_dance.c
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
void ramo_TD_make_colemak_default(tap_dance_state_t *state, void *user_data) {
if (state->count == 2) {
set_single_default_layer(L_COLEMAK);
diff --git a/users/raphaelmor/ramo_tap_dance.h b/users/raphaelmor/ramo_tap_dance.h
index 6d26f516..9f371e62 100644
--- a/users/raphaelmor/ramo_tap_dance.h
+++ b/users/raphaelmor/ramo_tap_dance.h
@@ -1,3 +1,13 @@
+/*
+ * ramo_tap_dance.h
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
// Tap Dance definitions
enum {
RAMO_TD_CMK,
diff --git a/users/raphaelmor/raphaelmor.c b/users/raphaelmor/raphaelmor.c
index 24706cda..a5270bba 100644
--- a/users/raphaelmor/raphaelmor.c
+++ b/users/raphaelmor/raphaelmor.c
@@ -1,3 +1,13 @@
+/*
+ * raphaelmor.c
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
#include QMK_KEYBOARD_H
#include "raphaelmor.h"
@@ -11,7 +21,13 @@ uint8_t saved_modifiers;
uint32_t linger_timer = 0; // time elapsed since combo was pressed
// Adaptive
uint16_t prior_keycode = KC_NO;
+uint16_t preprior_keycode = KC_NO; // key before prior (for 3-key adaptives)
uint16_t prior_keydown_timer = 0; // timer of keydown for adaptive threshhold.
+// Linger brackets
+uint16_t linger_key = 0; // currently-lingering open bracket (0 = none)
+uint16_t linger_close = 0; // its matching close bracket
+uint16_t linger_ktimer = 0; // keydown timer for the linger hold
+bool linger_fired = false; // has the pair already been inserted?
void keyboard_post_init_user(void) {
// Customise these values to desired behaviour
@@ -27,6 +43,9 @@ void keyboard_post_init_user(void) {
// Include combo processing code
#include "ramo_combos.c"
+// Include linger-bracket + casemods code (must precede matrix + process_record)
+#include "ramo_linger.c"
+
// Include matrix processing code
#include "ramo_matrix.c"
diff --git a/users/raphaelmor/raphaelmor.h b/users/raphaelmor/raphaelmor.h
index bb604a60..4a7abb91 100644
--- a/users/raphaelmor/raphaelmor.h
+++ b/users/raphaelmor/raphaelmor.h
@@ -1,5 +1,39 @@
+/*
+ * raphaelmor.h
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright (c) 2026 Raphaël Mor
+ *
+ * Part of a QMK userspace whose design follows Hands Down Promethium by moutis
+ * (https://github.com/moutis/HandsDown, GPL-3.0). See ./LICENSE.
+ */
+
#pragma once
#include "ramo_tap_dance.h"
#include "ramo_layers.h"
+
+// --- Firmware-verification logging ------------------------------------------
+// Log the ACTUAL keycodes/strings sent to the host, as distinct from the
+// "Pressed kc" logged in process_record_user (which is the keymap keycode —
+// the intent — before combos/adaptives/linger transform it). This lets the
+// touchtutor `tt verify` tool cross-check "what was pressed" vs "what was sent"
+// and catch transformation bugs, including for keys that emit no character.
+//
+// Wrap tap_code/tap_code16/send_string at the userspace emission sites (combos,
+// adaptives, linger, and the process_record transforms). Plain keys are sent by
+// core QMK unchanged, so their output already equals the logged "Pressed kc".
+//
+// Uses uprintf (CONSOLE_ENABLE) + get_keycode_string (KEYCODE_STRING_ENABLE),
+// both already enabled in rules.mk. Define RAMO_NO_SENT_LOG for a quiet build.
+#ifdef RAMO_NO_SENT_LOG
+# define RAMO_TAP(kc) tap_code(kc)
+# define RAMO_TAP16(kc) tap_code16(kc)
+# define RAMO_SEND_STRING(s) send_string(s)
+#else
+# define RAMO_TAP(kc) do { uprintf("RAMO: SENT: %s\n", get_keycode_string(kc)); tap_code(kc); } while (0)
+# define RAMO_TAP16(kc) do { uprintf("RAMO: SENT: %s\n", get_keycode_string(kc)); tap_code16(kc); } while (0)
+# define RAMO_SEND_STRING(s) do { uprintf("RAMO: SENT_STR: %s\n", s); send_string(s); } while (0)
+#endif
+
diff --git a/users/raphaelmor/rules.mk b/users/raphaelmor/rules.mk
index b0b077f0..ddb1a095 100644
--- a/users/raphaelmor/rules.mk
+++ b/users/raphaelmor/rules.mk
@@ -2,6 +2,10 @@ CAPS_WORD_ENABLE = yes
COMBO_ENABLE = yes
TAP_DANCE_ENABLE = yes
+// TEMP: for debugging adaptive
+KEYCODE_STRING_ENABLE = yes
+CONSOLE_ENABLE = yes
+
INTROSPECTION_KEYMAP_C += raphaelmor.c
diff --git a/users/raphaelmor/tools/kle_docs.py b/users/raphaelmor/tools/kle_docs.py
new file mode 100644
index 00000000..1eba61cf
--- /dev/null
+++ b/users/raphaelmor/tools/kle_docs.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MIT
+# Copyright (c) 2026 Raphaël Mor
+"""Write both layout documents from the QMK sources.
+
+Two targets, one command, because they must not disagree:
+
+ ../../../keyboard-layout/README.md the layout, no board in it, plus docs/*.svg
+ users/raphaelmor/README.md what is true of this Corne and this firmware
+
+Each file keeps everything outside
+
+
+
+
+so the prose you write around the generated block survives a regeneration.
+
+ python3 tools/kle_docs.py # write both
+ python3 tools/kle_docs.py --check # exit 1 and name anything stale
+"""
+
+from __future__ import annotations
+
+import argparse
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+import kle_export as E # noqa: E402
+import kle_render as R # noqa: E402
+
+QMK_README = E.USERSPACE / "README.md"
+LAYOUT_REPO = E.USERSPACE.parents[2] / "keyboard-layout"
+BEGIN = ""
+END = ""
+GENERATED = ("")
+
+LAYOUT_PREAMBLE = """# Colemak + Hands Down Promethium
+
+A 36-key layout, on two alpha bases that share one set of layers.
+
+**[Hands Down Promethium](https://github.com/moutis/HandsDown)** is the base I
+type on. **Colemak** is kept alongside it because it is what everything else in
+the world assumes, and because switching back costs one double tap.
+
+The layers follow **[Miryoku](https://github.com/manna-harbour/miryoku)**: hold a
+thumb, get a layer, and the payload sits on the hand opposite the thumb that
+activated it. On top of that sit combos for the letters and glyphs that have no
+home on 36 keys, and adaptive keys that rewrite same-finger bigrams as you roll
+them.
+
+Nothing below names a switch. Everything is described by position, so it holds
+for any 3x5 + 3 split.
+
+
+"""
+
+QMK_PREAMBLE_MARKER = "# HANDS DOWN GOLD and COLEMAK MIRYOKU CUSTOM SETUP FOR CORNE"
+
+
+def render_layers(source: dict) -> dict[str, str]:
+ """Layer prefix -> SVG text, one board per file."""
+ return {prefix: R.render(E.one_board(source, enum, prefix))
+ for enum, prefix in source["layers"]}
+
+
+def section(body: str) -> str:
+ return "\n".join([BEGIN, "", GENERATED, "", body, "", END])
+
+
+def splice(existing: str, body: str, fallback_header: str = "") -> str:
+ """Replace the marked region, or create the file around it."""
+ start, end = existing.find(BEGIN), existing.find(END)
+ if start == -1 or end == -1:
+ head = existing.rstrip() or fallback_header.rstrip()
+ return head + "\n\n" + section(body) + "\n"
+ return existing[:start] + section(body) + existing[end + len(END):]
+
+
+def read_or_empty(path: Path) -> str:
+ return path.read_text() if path.exists() else ""
+
+
+def planned_files(source: dict, layout_repo: Path | None) -> dict[Path, str]:
+ """Every path this run would write, and what it would put there."""
+ files: dict[Path, str] = {
+ QMK_README: splice(read_or_empty(QMK_README),
+ E.qmk_doc(source),
+ QMK_PREAMBLE_MARKER),
+ }
+ if layout_repo is None:
+ return files
+
+ body = E.layout_doc(source, "docs/layer-{prefix}.svg")
+ files[layout_repo / "README.md"] = splice(
+ read_or_empty(layout_repo / "README.md"), body, LAYOUT_PREAMBLE)
+ for prefix, svg in render_layers(source).items():
+ files[layout_repo / "docs" / f"layer-{prefix.lower()}.svg"] = svg
+ return files
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser(description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ ap.add_argument("--layout-repo", type=Path, default=LAYOUT_REPO,
+ help=f"default {LAYOUT_REPO}")
+ ap.add_argument("--check", action="store_true",
+ help="report what is stale and change nothing")
+ ap.add_argument("--gold", action="store_true",
+ help="render Hands Down Gold instead of Promethium")
+ args = ap.parse_args()
+
+ repo = args.layout_repo if args.layout_repo.is_dir() else None
+ if repo is None:
+ print(f"kle_docs: {args.layout_repo} is not there — writing the QMK "
+ f"README only", file=sys.stderr)
+
+ try:
+ source = E.read_all(promethium=not args.gold)
+ files = planned_files(source, repo)
+ except E.ExportError as err:
+ print(f"kle_docs: {err}", file=sys.stderr)
+ return 1
+
+ stale = [p for p, text in files.items()
+ if not p.exists() or p.read_text() != text]
+ if args.check:
+ for path in stale:
+ print(f"out of date: {path}")
+ print("up to date" if not stale
+ else f"{len(stale)} file(s) need `python3 tools/kle_docs.py`")
+ return 1 if stale else 0
+
+ for path, text in files.items():
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(text)
+ print(f"wrote {len(files)} file(s), {len(stale)} changed")
+ for path in stale:
+ print(f" {path}")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/users/raphaelmor/tools/kle_export.py b/users/raphaelmor/tools/kle_export.py
new file mode 100644
index 00000000..bb4a2da1
--- /dev/null
+++ b/users/raphaelmor/tools/kle_export.py
@@ -0,0 +1,1073 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MIT
+# Copyright (c) 2026 Raphaël Mor
+"""Render the QMK keymap in users/raphaelmor as a keyboard-layout-editor document.
+
+The keymap is built out of X-macros, so the only honest way to read it is to run
+the C preprocessor over it exactly as the firmware build does. This tool does
+that, then lays the result out as one KLE document: a board per layer, plus
+boards for the combos, the adaptive rules, and the always-on behaviors.
+
+ python3 tools/kle_export.py # writes corne-v4-1-ramo.json
+ python3 tools/kle_export.py -o /tmp/x.json # somewhere else
+ python3 tools/kle_export.py --gold # render Hands Down Gold instead
+
+Paste the output into keyboard-layout-editor.com under Raw data.
+
+There are no unit tests. Instead every structural assumption is a check() call
+that aborts the export, so a wrong file never reaches disk.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import subprocess
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+import kle_legends as L # noqa: E402
+
+USERSPACE = Path(__file__).resolve().parent.parent
+KEYMAP_C = (USERSPACE.parent.parent / "keyboards/crkbd/rev4_1/standard"
+ / "keymaps/raphaelmor/keymap.c")
+
+
+class ExportError(Exception):
+ """A structural assumption about the C sources no longer holds."""
+
+
+def check(condition: bool, message: str) -> None:
+ if not condition:
+ raise ExportError(message)
+
+
+# --- Geometry ----------------------------------------------------------------
+# Positions are named by the diagram every layer header carries:
+#
+# ╭───────────────────────────────╮ ╭───────────────────────────────╮
+# │ LT5 LT4 LT3 LT2 LT1 LT0 LTA │ │ RTA RT0 RT1 RT2 RT3 RT4 RT5 │
+# │ LM5 LM4 LM3 LM2 LM1 LM0 LMA │ │ RMA RM0 RM1 RM2 RM3 RM4 RM5 │
+# │ LB5 LB4 LB3 LB2 LB1 LB0 ╭─────╯ ╰─────╮ RB0 RB1 RB2 RB3 RB4 RB5 │
+# ╰───────────╮ LH2 LH1 LH0 │ │ RH0 RH1 RH2 ╭───────────╯
+# ╰─────────────╯ ╰─────────────╯
+
+ROWS = ["T", "M", "B"]
+LEFT_COLS = ["5", "4", "3", "2", "1", "0"]
+RIGHT_COLS = ["0", "1", "2", "3", "4", "5"]
+
+# The board has 46 switches, but 10 of them carry nothing on any layer: the outer
+# pinky column (LT5 LM5 LB5 RT5 RM5 RB5) and the four ex2 inner keys (LTA LMA RTA
+# RMA). They are not drawn. What is left is a plain 3x5 + 3 per half.
+SKIP_COLS = {"5"}
+SKIP_POSITIONS = {"LTA", "LMA", "RTA", "RMA"}
+DRAWN_LEFT_COLS = [c for c in LEFT_COLS if c not in SKIP_COLS]
+DRAWN_RIGHT_COLS = [c for c in RIGHT_COLS if c not in SKIP_COLS]
+
+# Column stagger, decoded from the hand-made corne-v4-1-miryoku-adaptation.json.
+# Index matches DRAWN_LEFT_COLS: ring first, index inner last.
+COL_Y = [0.3, 0.1, 0.0, 0.1, 0.2]
+ROW_PITCH = 1.0
+HALF_GAP = 3.0 # blank units between the two halves
+RIGHT_X0 = len(DRAWN_LEFT_COLS) + HALF_GAP
+THUMB_DX = 0.5 # thumbs sit half a unit inboard of the columns above them
+THUMB_Y = 3.35
+BOARD_PITCH = 6.6 # vertical distance between two stacked boards
+TITLE_Y = 2.6 # the wide title decal sits in the gap, bottom-row height
+
+
+def build_geometry() -> dict[str, tuple[float, float]]:
+ """Position name -> (x, y) in key units, relative to the board origin."""
+ geo: dict[str, tuple[float, float]] = {}
+ for r_i, row in enumerate(ROWS):
+ for c_i, col in enumerate(DRAWN_LEFT_COLS):
+ geo[f"L{row}{col}"] = (float(c_i), COL_Y[c_i] + r_i * ROW_PITCH)
+ for c_i, col in enumerate(DRAWN_RIGHT_COLS):
+ # The right half mirrors the left, so its stagger reads backwards.
+ geo[f"R{row}{col}"] = (RIGHT_X0 + c_i, COL_Y[-1 - c_i] + r_i * ROW_PITCH)
+ left_thumb0 = len(DRAWN_LEFT_COLS) - 3 + THUMB_DX
+ for i, name in enumerate(("LH2", "LH1", "LH0")):
+ geo[name] = (left_thumb0 + i, THUMB_Y)
+ for i, name in enumerate(("RH0", "RH1", "RH2")):
+ geo[name] = (RIGHT_X0 - THUMB_DX + i, THUMB_Y)
+ return geo
+
+
+GEOMETRY = build_geometry()
+
+# The order the LAYOUT macro takes its arguments in. Checked against keymap.c.
+POSITIONS = (
+ [f"LT{c}" for c in LEFT_COLS] + ["LTA", "RTA"] + [f"RT{c}" for c in RIGHT_COLS]
+ + [f"LM{c}" for c in LEFT_COLS] + ["LMA", "RMA"] + [f"RM{c}" for c in RIGHT_COLS]
+ + [f"LB{c}" for c in LEFT_COLS] + [f"RB{c}" for c in RIGHT_COLS]
+ + ["LH2", "LH1", "LH0", "RH0", "RH1", "RH2"]
+)
+
+# The subset that gets drawn.
+DRAWN = [p for p in POSITIONS
+ if p not in SKIP_POSITIONS and p[-1] not in SKIP_COLS]
+
+
+def check_positions_match_keymap() -> None:
+ """The LAYOUT call in keymap.c is the real argument order. Follow it."""
+ if not KEYMAP_C.exists(): # a bare checkout of just the userspace
+ return
+ source = KEYMAP_C.read_text()
+ body = source.split("LAYOUT_split_3x6_3_ex2(", 1)
+ check(len(body) > 1, f"no LAYOUT_split_3x6_3_ex2 call in {KEYMAP_C}")
+ found = re.findall(r"STR##_([LR][TMBH][0-5A])\b", body[1])
+ check(found == POSITIONS,
+ "POSITIONS no longer matches the LAYOUT call in keymap.c.\n"
+ f" keymap.c: {found}\n here: {POSITIONS}")
+
+
+# --- Preprocessor ------------------------------------------------------------
+
+def expand(lines: list[str], *, promethium: bool, preamble: str = "") -> str:
+ """Run the C preprocessor over `lines` with the layer headers in scope."""
+ document = '#include "ramo_layers.h"\n' + preamble + "\n".join(lines) + "\n"
+ cmd = ["clang", "-E", "-P", "-x", "c", "-I", str(USERSPACE)]
+ if promethium:
+ cmd.append("-DRAMO_USE_HD_PROMETHIUM")
+ cmd.append("-")
+ done = subprocess.run(cmd, input=document, capture_output=True, text=True)
+ check(done.returncode == 0,
+ f"the preprocessor failed:\n{done.stderr.strip()}")
+ return done.stdout
+
+
+def read_layers(promethium: bool) -> list[tuple[str, str]]:
+ """[(enum name, prefix)], in layer-index order, straight from ramo_layers.h.
+
+ ramo_layers.h undefines RAMO_DO after building the enum, so expanding
+ RAMO_FOR_EACH_LAYER on its own yields literal RAMO_DO(NAME, PREFIX) calls.
+ """
+ out = expand(["RAMO_FOR_EACH_LAYER"], promethium=promethium)
+ layers = re.findall(r"RAMO_DO\(\s*(\w+)\s*,\s*(\w+)\s*\)", out)
+ check(len(layers) >= 2, f"could not read the layer list, got: {out!r}")
+ return layers
+
+
+def read_keys(layers, promethium: bool) -> dict[str, dict[str, str]]:
+ """Keycode per layer prefix and position."""
+ lines = []
+ for _, prefix in layers:
+ for pos in POSITIONS:
+ lines.append(f"KEY({prefix},{pos},{prefix}_{pos})")
+ out = expand(lines, promethium=promethium)
+
+ keys: dict[str, dict[str, str]] = {p: {} for _, p in layers}
+ for prefix, pos, value in re.findall(
+ r"\bKEY\((\w+),(\w+),(.*?)\)\s*$", out, re.MULTILINE):
+ keys[prefix][pos] = value.strip()
+
+ for _, prefix in layers:
+ check(len(keys[prefix]) == len(POSITIONS),
+ f"{prefix}: expanded {len(keys[prefix])} keys, want {len(POSITIONS)}")
+ return keys
+
+
+def read_combo_gates(source: str, promethium: bool) -> dict[str, str]:
+ """Combo enum name -> the base layer it is limited to, from combo_should_trigger.
+
+ The gate is written as a range (`case HD_COMBO_ACUT ... HD_COMBO_CAPW:`), so
+ the enum order decides which combos it covers. The enum has an #ifdef in it,
+ so let the preprocessor resolve it rather than guessing.
+ """
+ block = re.search(r"enum ramo_combos \{(.*?)\n\};", source, re.S)
+ check(block is not None, "no `enum ramo_combos` found in ramo_combos.c")
+ expanded = expand(["enum ramo_combos {" + block.group(1) + "\n};"],
+ promethium=promethium)
+ tail = expanded.split("enum ramo_combos {", 1)[1].split("};", 1)[0]
+ order = [n for n in re.findall(r"\b(\w+)\b(?:\s*=\s*\d+)?\s*,", tail)
+ if not n.isdigit()]
+ check("NO_COMBO" in order, f"could not read the combo enum, got {order}")
+
+ gates: dict[str, str] = {}
+ for first, last, layer in re.findall(
+ r"case (\w+)\s*\.\.\.\s*(\w+):\s*\n\s*return get_highest_layer\("
+ r"default_layer_state\) == (L_\w+);", source):
+ check(first in order and last in order,
+ f"gate range {first}..{last} names a combo that is not in the enum")
+ lo, hi = order.index(first), order.index(last)
+ check(lo <= hi, f"gate range {first}..{last} runs backwards")
+ for name in order[lo:hi + 1]:
+ gates[name] = layer
+ return gates
+
+
+def read_combos(layers, promethium: bool) -> list[dict]:
+ """Combos from ramo_combos.c, with each member resolved to a key position.
+
+ Position macros are redefined to markers before the combo arrays expand, so an
+ alias like HD_Q_keys resolves to @@LT3@@, @@LT1@@ instead of to keycodes.
+ """
+ source = (USERSPACE / "ramo_combos.c").read_text()
+ gates = read_combo_gates(source, promethium)
+
+ arrays = dict(re.findall(
+ r"const uint16_t PROGMEM (\w+)\[\]\s*=\s*\{(.*?),\s*COMBO_END\s*\}",
+ source, re.S))
+ check(arrays, "no combo key arrays found in ramo_combos.c")
+
+ # Redefine every position macro to a marker that survives expansion.
+ redef = []
+ for _, prefix in layers:
+ for pos in POSITIONS:
+ redef.append(f"#undef {prefix}_{pos}")
+ redef.append(f"#define {prefix}_{pos} @@{prefix}_{pos}@@")
+ preamble = "\n".join(redef) + "\n"
+
+ names = list(arrays)
+ out = expand([f"MEMBERS({n},{arrays[n]})" for n in names],
+ promethium=promethium, preamble=preamble)
+ members: dict[str, list[str]] = {}
+ raw: dict[str, str] = {}
+ for name, body in re.findall(r"MEMBERS\((\w+),(.*?)\)\s*$", out, re.MULTILINE):
+ members[name] = re.findall(r"@@(\w+)_([LR][TMBH][0-5A])@@", body)
+ raw[name] = body.strip()
+ check(set(members) == set(names),
+ f"combo members did not resolve: {set(names) - set(members)}")
+
+ # key_combos[] gives the enum name, the array, and the output.
+ combos = []
+ for enum, kind, array, arg in re.findall(
+ r"\[(\w+)\]\s*=\s*(COMBO_ACTION|COMBO)\(\s*(\w+)\s*(?:,\s*([^)]*?))?\s*\)",
+ source):
+ check(array in members, f"combo {enum} uses unknown array {array}")
+ resolved = members[array]
+ check(len(resolved) >= 2,
+ f"combo {enum}: {array} resolved to {len(resolved)} key positions, "
+ f"want 2 or more. The preprocessor made it {raw[array]!r}, so it "
+ f"names a position macro that this build does not define.")
+ prefixes = {p for p, _ in resolved}
+ check(len(prefixes) == 1,
+ f"combo {enum} mixes layers: {prefixes}")
+ combos.append({
+ "enum": enum,
+ "prefix": prefixes.pop(),
+ "positions": [pos for _, pos in resolved],
+ "output": (L.COMBO_ACTION_NOTES.get(enum) if kind == "COMBO_ACTION"
+ else L.COMBO_RESULT_NOTES.get(enum) or legend_text(arg)),
+ "action": kind == "COMBO_ACTION",
+ "gate": gates.get(enum),
+ })
+ check(combos[-1]["output"] is not None,
+ f"combo {enum} is a COMBO_ACTION with no note in kle_legends.py")
+
+ declared = len(re.findall(r"^\s*\[(\w+)\]\s*=\s*COMBO", source, re.MULTILINE))
+ check(len(combos) == declared,
+ f"parsed {len(combos)} combos but key_combos[] declares {declared}")
+ return combos
+
+
+def read_adaptives() -> list[dict]:
+ """Adaptive rules from ramo_adaptive.c.
+
+ The nested switch gives the trigger key and the prior key. The effect comes
+ from the trailing // comment every rule already carries, which is both exact
+ and already maintained. A comment describes a rule when it contains "->";
+ the other comments in the file explain a step or record a removal.
+
+ One inner case can hold two rules: a three-key rule inside an
+ `if (preprior_keycode == …)` guard, then the two-key rule after it.
+ """
+ source = (USERSPACE / "ramo_adaptive.c").read_text()
+ body = source.split("switch (keycode) {", 1)
+ check(len(body) > 1, "no outer switch found in ramo_adaptive.c")
+ body = body[1].split("\n }\n", 1)[0]
+
+ rules: list[dict] = []
+ described = 0
+ trigger = prior = None
+ for line in body.splitlines():
+ outer = re.match(r"\s{8}case (KC_\w+):", line)
+ if outer:
+ trigger, prior = outer.group(1), None
+ continue
+ inner = re.match(r"\s{16}case (KC_\w+):(.*)", line)
+ if inner:
+ prior = inner.group(1)
+ line = inner.group(2)
+ comment = re.search(r"//\s*(.+?)\s*$", line)
+ if not comment or "->" not in comment.group(1):
+ continue
+ described += 1
+ check(trigger is not None and prior is not None,
+ f"rule comment outside any case: {comment.group(1)!r}")
+ guard = re.search(r"preprior_keycode == (KC_\w+)", line)
+ rules.append({"trigger": trigger, "prior": prior,
+ "note": comment.group(1),
+ "preprior": guard.group(1) if guard else None})
+
+ # A fallthrough case (`case KC_G:` with the body on the next `case`) carries
+ # its own comment, so every "->" comment must have produced exactly one rule.
+ check(len(rules) == described,
+ f"parsed {len(rules)} rules from {described} rule comments")
+ check(rules, "no adaptive rules parsed from ramo_adaptive.c")
+ return rules
+
+
+# --- Keycode to legend -------------------------------------------------------
+
+def split_args(text: str) -> list[str]:
+ """Split a macro argument list on commas that are not inside parentheses."""
+ args, depth, current = [], 0, ""
+ for ch in text:
+ if ch == "(":
+ depth += 1
+ elif ch == ")":
+ depth -= 1
+ if ch == "," and depth == 0:
+ args.append(current.strip())
+ current = ""
+ else:
+ current += ch
+ args.append(current.strip())
+ return args
+
+
+def parse_keycode(keycode: str) -> tuple[object, str | None]:
+ """(tap legend, hold legend). The hold legend goes on the front face."""
+ keycode = keycode.strip()
+ if keycode in L.BASIC:
+ return L.BASIC[keycode], None
+
+ call = re.fullmatch(r"(\w+)\((.*)\)", keycode, re.S)
+ if not call:
+ raise ExportError(f"no legend for keycode {keycode!r} — add it to kle_legends.py")
+ name, args = call.group(1), split_args(call.group(2))
+
+ if name in L.MOD_TAPS:
+ tap, _ = parse_keycode(args[0])
+ return tap, L.MOD_TAPS[name]
+ if name == "LT":
+ tap, _ = parse_keycode(args[1])
+ layer = L.LAYER_NAMES.get(args[0])
+ check(layer is not None, f"unknown layer {args[0]!r} in {keycode!r}")
+ return tap, layer
+ if name == "DF":
+ layer = L.LAYER_NAMES.get(args[0])
+ check(layer is not None, f"unknown layer {args[0]!r} in {keycode!r}")
+ return f"Base: {layer}", None
+ if name == "TD":
+ note = L.TAP_DANCES.get(args[0])
+ check(note is not None, f"unknown tap dance {args[0]!r} — add it to kle_legends.py")
+ return note, None
+ if name in L.CHORDS:
+ inner, _ = parse_keycode(args[0])
+ if isinstance(inner, tuple):
+ inner = inner[1]
+ return L.CHORDS[name] + inner, None
+
+ raise ExportError(f"no legend for keycode {keycode!r} — add it to kle_legends.py")
+
+
+def legend_text(keycode: str) -> str:
+ """One flat string for a keycode. Used where a key has no room for two lines."""
+ tap, hold = parse_keycode(keycode)
+ if isinstance(tap, tuple):
+ tap = tap[1]
+ return f"{tap} ({hold})" if hold else tap
+
+
+# --- KLE serialization -------------------------------------------------------
+# KLE stores a label list of 12 slots whose meaning depends on the key's align
+# flag. labelMap[align][slot] is the visual position that slot renders at:
+#
+# 0 1 2 top row of the keycap
+# 3 4 5 middle
+# 6 7 8 bottom
+# 9 10 11 the sloped front face
+#
+# Taken from the keyboard-layout-editor deserializer.
+
+LABEL_MAP = [
+ [0, 6, 2, 8, 9, 11, 3, 5, 1, 4, 7, 10], # 0 = nothing centred
+ [1, 7, -1, -1, 9, 11, 4, -1, -1, -1, -1, 10],
+ [3, -1, 5, -1, 9, 11, -1, -1, 4, -1, -1, 10],
+ [4, -1, -1, -1, 9, 11, -1, -1, -1, -1, -1, 10],
+ [0, 6, 2, 8, 10, -1, 3, 5, 1, 4, 7, -1],
+ [1, 7, -1, -1, 10, -1, 4, -1, -1, -1, -1, -1], # 5 = centre x + front
+ [3, -1, 5, -1, 10, -1, -1, -1, 4, -1, -1, -1],
+ [4, -1, -1, -1, 10, -1, -1, -1, -1, -1, -1, -1], # 7 = centre x, y + front
+]
+
+TOP, MIDDLE, BOTTOM, FRONT = 1, 4, 7, 10
+TOP_LEFT, CENTER = 0, 4
+# A 1u cap has a 3x3 label grid on its top face plus a 3-slot front face. These
+# are the stacks that do not collide: one centred column of four, and two columns
+# of three for short strings.
+CENTER_STACK = (1, 4, 7, 10)
+TWO_COLUMNS = (0, 2, 3, 5, 6, 8)
+FRONT_CENTER = 10
+
+
+def labels(align: int, placed: dict[int, str]) -> str:
+ """Build the \\n-joined label string putting each text at a visual position."""
+ slot_of = {}
+ for slot, visual in enumerate(LABEL_MAP[align]):
+ if visual >= 0 and visual not in slot_of:
+ slot_of[visual] = slot
+ out = [""] * 12
+ for visual, text in placed.items():
+ check(visual in slot_of,
+ f"visual position {visual} is unreachable with align {align}")
+ out[slot_of[visual]] = text
+ while out and not out[-1]:
+ out.pop()
+ return "\n".join(out)
+
+
+class Sheet:
+ """Collects absolutely-positioned keys and serializes them as KLE rows."""
+
+ def __init__(self) -> None:
+ self.keys: list[dict] = []
+
+ def add(self, x, y, text, *, color, text_color, width=1.0, align=7,
+ font=3, decal=False, ghost=False) -> None:
+ self.keys.append(dict(x=x, y=y, text=text, color=color, tcolor=text_color,
+ width=width, align=align, font=font,
+ decal=decal, ghost=ghost))
+
+ def rows(self) -> list:
+ """KLE rows. Keys are grouped by y; x and y are emitted as deltas.
+
+ KLE keeps colour, text colour, align, font and ghost until they change,
+ but resets width and decal to their defaults after every key. So the
+ first group is emitted on change and the second on every key that needs it.
+ """
+ sticky = (("c", "color"), ("t", "tcolor"), ("a", "align"),
+ ("f", "font"), ("g", "ghost"))
+ per_key = (("w", "width", 1.0), ("d", "decal", False))
+ out = []
+ # Start every sticky property as unknown, not as its default. KLE carries
+ # them across rows, and these rows get concatenated after another sheet's,
+ # so the first key must state all of them.
+ state = dict(color=None, tcolor=None, align=None, font=None, ghost=None)
+ y_cursor = 0.0
+ for y in sorted({round(k["y"], 4) for k in self.keys}):
+ row, x_cursor = [], 0.0
+ pending = {}
+ if abs(y - y_cursor) > 1e-9:
+ pending["y"] = round(y - y_cursor, 4)
+ for key in sorted((k for k in self.keys if round(k["y"], 4) == y),
+ key=lambda k: k["x"]):
+ if abs(key["x"] - x_cursor) > 1e-9:
+ pending["x"] = round(key["x"] - x_cursor, 4)
+ for prop, field in sticky:
+ if key[field] != state[field]:
+ pending[prop] = key[field]
+ state[field] = key[field]
+ for prop, field, default in per_key:
+ if key[field] != default:
+ pending[prop] = key[field]
+ if pending:
+ row.append(pending)
+ pending = {}
+ row.append(key["text"])
+ x_cursor = key["x"] + key["width"]
+ out.append(row)
+ y_cursor = y + 1.0
+ return out
+
+
+def deserialize(rows: list) -> list[tuple[float, float, float]]:
+ """Minimal KLE reader, used to check that Sheet.rows() round-trips.
+
+ Mirrors KLE: x and y accumulate, and width resets to 1 after every key.
+ """
+ found, y, width = [], 0.0, 1.0
+ for row in rows:
+ if isinstance(row, dict):
+ continue
+ x = 0.0
+ for item in row:
+ if isinstance(item, dict):
+ x += item.get("x", 0)
+ y += item.get("y", 0)
+ width = item.get("w", 1.0)
+ else:
+ found.append((round(x, 4), round(y, 4), round(width, 4)))
+ x += width
+ width = 1.0
+ y += 1.0
+ return found
+
+
+# --- Boards ------------------------------------------------------------------
+
+def title(sheet: Sheet, y0: float, text: str, sub: str = "") -> None:
+ sheet.add(len(DRAWN_LEFT_COLS), y0 + TITLE_Y,
+ labels(7, {CENTER: text, FRONT: sub} if sub else {CENTER: text}),
+ color=L.TITLE_COLOR, text_color=L.TITLE_TEXT, width=HALF_GAP,
+ align=7, decal=True)
+
+
+def display_name(layer_enum: str) -> str:
+ name = L.LAYER_NAMES.get("L_" + layer_enum)
+ check(name is not None,
+ f"no display name for layer {layer_enum} — add it to kle_legends.py")
+ return name
+
+
+def read_activators(layers, keys: dict) -> dict[str, dict[str, list[str]]]:
+ """Layer enum -> {position: the tap legend that position has on each base}.
+
+ A layer-tap on a base layer is how every layer here is reached, so each
+ layer's own board can mark the key you hold to get there.
+ """
+ found: dict[str, dict[str, list[str]]] = {}
+ for _, prefix in layers[:2]:
+ for pos in DRAWN:
+ call = re.fullmatch(r"LT\(\s*(L_\w+)\s*,(.*)\)",
+ keys[prefix][pos].strip(), re.S)
+ if not call:
+ continue
+ layer = call.group(1).removeprefix("L_")
+ tap, _ = parse_keycode(call.group(2).strip())
+ if isinstance(tap, tuple):
+ tap = tap[1]
+ found.setdefault(layer, {}).setdefault(pos, []).append(tap)
+ return found
+
+
+def layer_board(sheet: Sheet, y0: float, keys: dict, activators: set[str]) -> None:
+ for pos in DRAWN:
+ x, y = GEOMETRY[pos]
+ tap, hold = parse_keycode(keys[pos])
+ if isinstance(tap, tuple):
+ placed = {TOP: tap[0], BOTTOM: tap[1]}
+ align = 5
+ else:
+ placed = {CENTER: tap}
+ align = 7
+ if hold:
+ placed[FRONT] = hold
+ reaches_here = pos in activators
+ sheet.add(x, y0 + y, labels(align, placed),
+ color=L.ACTIVATOR_COLOR if reaches_here else L.KEY_COLOR,
+ text_color=L.KEY_TEXT, align=align,
+ ghost=reaches_here or L.is_unbound(keys[pos]))
+
+
+def alpha_of(keycode: str) -> str:
+ """The bare tap legend, for the combo and adaptive boards."""
+ tap, _ = parse_keycode(keycode)
+ return tap[1] if isinstance(tap, tuple) else tap
+
+
+def combos_on(combos: list[dict], layer_enum: str) -> list[dict]:
+ """The combos that fire while `layer_enum` is the default layer.
+
+ A combo is a set of key positions, not of letters, so an ungated combo fires
+ on both bases — at whatever letters that base puts under those positions.
+ Two entries on the same positions with the same output are one chord to the
+ typist (HD_COMBO_Th and CMK_COMBO_Th are literally the same two keys), so
+ they collapse.
+ """
+ seen, out = set(), []
+ for combo in combos:
+ if combo["gate"] not in (None, "L_" + layer_enum):
+ continue
+ signature = (tuple(sorted(combo["positions"])), combo["output"])
+ if signature in seen:
+ continue
+ seen.add(signature)
+ out.append(combo)
+ return out
+
+
+def read_terms() -> dict[str, int]:
+ """The timing constants, from config.h. They appear in the notes."""
+ text = (USERSPACE / "config.h").read_text()
+ out = {}
+ for name in ("TAPPING_TERM", "RAMO_ADAPTIVE_TERM"):
+ found = re.findall(rf"^#define {name}\s+(\d+)", text, re.M)
+ check(found, f"could not read {name} from config.h")
+ out[name] = int(found[-1]) # the last definition is the live one
+ return out
+
+
+# --- Documentation sections --------------------------------------------------
+# Each returns a list of lines. Three composers below pick the ones their target
+# needs, so the KLE Notes field, the layout repo and this userspace README all
+# come from one description and cannot disagree.
+
+def table(head: list[str], rows: list[list[str]]) -> list[str]:
+ """A fixed-width table inside a code fence. Columns sized from the content."""
+ width = [max(len(h), *(len(r[i]) for r in rows)) if rows else len(h)
+ for i, h in enumerate(head)]
+
+ def line(values: list[str]) -> str:
+ return " " + " ".join(v.ljust(w) for v, w in zip(values, width)).rstrip()
+
+ return ["```", line(head),
+ " " + "-" * (sum(width) + 2 * (len(width) - 1))] + \
+ [line(r) for r in rows] + ["```"]
+
+
+def section_grid() -> list[str]:
+ """The position names. This is the vocabulary both implementations share."""
+ return [
+ "## Key positions",
+ "",
+ "Every layer below is described by position, never by what a particular",
+ "board calls a switch. Both implementations use these same names —",
+ "`users/raphaelmor/layers/*-defs.h` in QMK, `config/keynames_totem.h` in ZMK.",
+ "",
+ "```",
+ " ╭─────────────────────────╮ ╭─────────────────────────╮",
+ " │ LT4 LT3 LT2 LT1 LT0 │ │ RT0 RT1 RT2 RT3 RT4 │",
+ " │ LM4 LM3 LM2 LM1 LM0 │ │ RM0 RM1 RM2 RM3 RM4 │",
+ " │ LB4 LB3 LB2 LB1 LB0 │ │ RB0 RB1 RB2 RB3 RB4 │",
+ " ╰───────────╮ LH2 LH1 LH0 │ │ RH0 RH1 RH2 ╭───────────╯",
+ " ╰─────────────╯ ╰─────────────╯",
+ "```",
+ "",
+ "T/M/B are the top, middle and bottom rows; H is the thumb cluster. The",
+ "digit counts inward from the pinky, so 0 is the index column.",
+ ]
+
+
+def section_reaching(layers, activators) -> list[str]:
+ bases = layers[:2]
+ rows = []
+ for enum, _ in layers:
+ if enum not in activators:
+ continue
+ # activators[enum][pos] lists the tap legend per base, in base order.
+ taps = [" / ".join(dict.fromkeys(activators[enum][pos]))
+ for pos in sorted(activators[enum])]
+ rows.append([display_name(enum), ", ".join(taps),
+ " ".join(sorted(activators[enum]))])
+
+ out = [
+ "## Reaching a layer",
+ "",
+ "Each layer's picture marks the key you **hold** to get there in red, and",
+ "ghosts it, because it is not something that layer binds. Where the two",
+ f"bases differ the column reads {display_name(bases[0][0])} / "
+ f"{display_name(bases[1][0])}.",
+ "",
+ ] + table(["layer", "hold", "position"], rows)
+
+ unreachable = [display_name(e) for e, _ in layers[2:] if e not in activators]
+ if unreachable:
+ out += ["",
+ "No layer-tap reaches " + ", ".join(unreachable)
+ + " — check how you get there."]
+ return out
+
+
+def section_combos(layers, keys, combos) -> list[str]:
+ bases = layers[:2]
+ # One row per distinct chord. key_combos[] can register the same two
+ # positions twice — HD_COMBO_Th and CMK_COMBO_Th are literally the same
+ # chord — and combos_on() already collapses those, so count what is shown.
+ both = [c for c in combos
+ if all(c in combos_on(combos, enum) for enum, _ in bases)]
+ per_base: list[tuple[str, list[dict]]] = []
+ shown = list(both)
+ for enum, _ in bases:
+ rest = [c for c in combos_on(combos, enum)
+ if id(c) not in {id(b) for b in both}]
+ if rest:
+ per_base.append((enum, rest))
+ shown += rest
+
+ def row(combo: dict) -> list[str]:
+ cells = ["+".join(alpha_of(keys[prefix][p]) for p in combo["positions"])
+ if combo in combos_on(combos, enum) else ""
+ for enum, prefix in bases]
+ return cells + [L.COMBO_LONG_NOTES.get(combo["enum"], combo["output"]),
+ "+".join(combo["positions"])]
+
+ head = [display_name(e) for e, _ in bases] + ["sends", "positions"]
+ rendered = table(head, [row(c) for c in shown])
+ # table() emits one block; re-open it to head the per-base groups.
+ body = rendered[:-1][:3 + len(both)]
+ index = len(both)
+ for enum, rest in per_base:
+ body += ["", f" {display_name(enum)} base only"]
+ body += rendered[3 + index:3 + index + len(rest)]
+ index += len(rest)
+ body.append("```")
+
+ out = [
+ f"## Combos — {len(shown)}",
+ "",
+ "A combo is a set of key *positions*, not of letters. Most are limited to",
+ f"the {display_name(bases[1][0])} base, but {len(both)} are not, so those "
+ f"also fire on",
+ f"{display_name(bases[0][0])} — at whatever letters sit under those positions.",
+ "",
+ ] + body
+
+ duplicates = len(combos) - len(shown)
+ if duplicates:
+ out += ["",
+ f"`key_combos[]` declares {len(combos)} entries. "
+ + (f"{duplicates} of them repeat a chord"
+ if duplicates > 1 else "One of them repeats a chord"),
+ "already listed above — same positions, same output."]
+ return out
+
+
+def section_adaptives(layers, adaptives, terms) -> list[str]:
+ out = [
+ f"## Adaptive keys — {len(adaptives)}",
+ "",
+ f"Only on the {display_name(layers[1][0])} base. Roll two keys inside "
+ f"{terms['RAMO_ADAPTIVE_TERM']} ms and the",
+ "second one rewrites the pair. Rules that name three letters need the",
+ "third-from-last key too.",
+ "",
+ "```",
+ ]
+ for rule in adaptives:
+ after = (f"{rule['preprior'][3:]}+{rule['prior'][3:]}" if rule["preprior"]
+ else rule["prior"][3:])
+ out.append(f" {rule['note']:<52} (fires on {rule['trigger'][3:]}"
+ f" after {after})")
+ out.append("```")
+ return out
+
+
+def anchor(filename: str, needle: str) -> str:
+ """`file:line` for the line that holds `needle`. Never a stale number."""
+ path = USERSPACE / filename
+ check(path.exists(), f"{filename} is gone — fix the anchor in kle_legends.py")
+ for number, line in enumerate(path.read_text().splitlines(), 1):
+ if needle in line:
+ return f"{filename}:{number}"
+ raise ExportError(
+ f"{filename} no longer contains {needle!r} — the behaviour moved or was "
+ f"renamed. Fix the anchor in kle_legends.py.")
+
+
+def section_always_on(terms) -> list[str]:
+ return ["## Always on", ""] + [
+ f"- **{name}** — {body.format(**terms)} `{anchor(filename, needle)}`"
+ for name, body, filename, needle in L.ALWAYS_ON]
+
+
+def section_not_drawn(hidden) -> list[str]:
+ out = [
+ "## Not drawn",
+ "",
+ "The board has 46 switches. These "
+ f"{len(SKIP_POSITIONS) + len(SKIP_COLS) * len(ROWS) * 2} are left off:",
+ "",
+ "- Outer pinky column `LT5 LM5 LB5 RT5 RM5 RB5` — `KC_NO` on every layer.",
+ "- Inner ex2 keys `LTA LMA RTA RMA`.",
+ ]
+ if hidden:
+ out += ["", "Careful: these hidden positions do have something bound.", ""]
+ out += [f"- {h}" for h in hidden]
+ return out
+
+
+def build_notes(layers, keys: dict, combos: list[dict], adaptives: list[dict],
+ activators: dict[str, dict[str, list[str]]],
+ hidden: list[str]) -> str:
+ """The keyboard's Notes field in the KLE document.
+
+ Combos and adaptives used to sit on the caps. They never fit: a key can take
+ four short lines at best, and the text has to be cut down until it stops
+ meaning anything. Notes has no such limit and KLE shows it under the board.
+ """
+ terms = read_terms()
+ out = [f"# {display_name(layers[0][0])} + {display_name(layers[1][0])} "
+ "· Corne v4.1",
+ "",
+ "Generated by `users/raphaelmor/tools/kle_export.py` from the QMK sources.",
+ "Do not hand-edit — regenerate instead.",
+ ""]
+ for block in (section_reaching(layers, activators),
+ section_combos(layers, keys, combos),
+ section_adaptives(layers, adaptives, terms),
+ section_always_on(terms),
+ section_not_drawn(hidden)):
+ out += block + [""]
+ return "\n".join(out[:-1])
+
+
+# --- Export ------------------------------------------------------------------
+
+def round_trip(sheet: Sheet, rows: list, what: str) -> None:
+ """The x/y/width deltas are the easiest thing to get wrong. Read them back."""
+ want = [(round(k["x"], 4), round(k["y"], 4), round(k["width"], 4))
+ for k in sorted(sheet.keys, key=lambda k: (round(k["y"], 4), k["x"]))]
+ got = deserialize(rows)
+ check(got == want,
+ f"the {what} rows do not round-trip. First difference: "
+ + next((f"want {w}, got {g}" for w, g in zip(want, got) if w != g),
+ f"{len(want)} keys in, {len(got)} keys out"))
+
+
+def read_all(promethium: bool) -> dict:
+ """Everything the C sources have to say. Shared by every output format."""
+ check_positions_match_keymap()
+ layers = read_layers(promethium)
+ keys = read_keys(layers, promethium)
+ return {
+ "layers": layers,
+ "keys": keys,
+ "combos": read_combos(layers, promethium),
+ "adaptives": read_adaptives(),
+ "activators": read_activators(layers, keys),
+ # Nothing gets dropped quietly. If a position we do not draw has
+ # something bound on some layer, say so in the notes.
+ "hidden": [f"`{prefix}_{pos}` is `{keys[prefix][pos]}`"
+ for _, prefix in layers for pos in POSITIONS
+ if pos not in DRAWN and not L.is_unbound(keys[prefix][pos])],
+ "promethium": promethium,
+ }
+
+
+def section_alphas(layers, keys) -> list[str]:
+ """The two alpha bases as text grids, so the doc is useful without images."""
+ out = ["## The two bases", "",
+ "Switch between them with a double tap on the Nav layer. Everything",
+ "else — layers, combos, adaptives — is shared.", ""]
+ for enum, prefix in layers[:2]:
+ cells = {p: alpha_of(keys[prefix][p]) for p in DRAWN}
+ pad = max(len(v) for v in cells.values())
+ half = (pad + 1) * len(DRAWN_LEFT_COLS) + 1 # a half's inner width
+ thumb = (pad + 1) * 3 + 1 # the thumb block's width
+ shoulder = half - thumb # what the thumbs sit under
+
+ def cell_row(side: str, row: str, cols: list[str]) -> str:
+ return " ".join(cells[f"{side}{row}{c}"].center(pad) for c in cols)
+
+ out += [f"### {display_name(enum)}", "", "```",
+ " ╭" + "─" * half + "╮ ╭" + "─" * half + "╮"]
+ for row in ROWS:
+ out.append(f" │ {cell_row('L', row, DRAWN_LEFT_COLS)} │"
+ f" │ {cell_row('R', row, DRAWN_RIGHT_COLS)} │")
+ thumbs_l = " ".join(cells[p].center(pad) for p in ("LH2", "LH1", "LH0"))
+ thumbs_r = " ".join(cells[p].center(pad) for p in ("RH0", "RH1", "RH2"))
+ out += [" ╰" + "─" * shoulder + "╮ " + thumbs_l + " │"
+ " │ " + thumbs_r + " ╭" + "─" * shoulder + "╯",
+ " " + " " * (shoulder + 1) + "╰" + "─" * thumb + "╯"
+ " ╰" + "─" * thumb + "╯",
+ "```", ""]
+ return out[:-1]
+
+
+def section_board() -> list[str]:
+ return [
+ "## The board",
+ "",
+ f"Corne v4.1, `LAYOUT_split_3x6_3_ex2` (`keymap.c`): {len(POSITIONS)} "
+ "switches — 3x6 per",
+ "half, three thumbs, plus the four ex2 inner keys `LTA LMA RTA RMA`.",
+ "",
+ f"Only {len(DRAWN)} of them carry anything, so the pictures in the layout",
+ "repo draw a 3x5 + 3. See *Not drawn* below for the rest.",
+ ]
+
+
+def section_flags(promethium: bool, terms: dict) -> list[str]:
+ rows = [["RAMO_USE_HD_PROMETHIUM",
+ "set" if promethium else "unset",
+ "Hands Down Promethium" if promethium else "Hands Down Gold"]]
+ rows += [[name, str(value), note] for name, value, note in (
+ ("TAPPING_TERM", terms["TAPPING_TERM"],
+ "hold to get the mod, the layer, the bracket pair"),
+ ("RAMO_ADAPTIVE_TERM", terms["RAMO_ADAPTIVE_TERM"],
+ "keydown to keydown, for an adaptive to fire"))]
+ return ["## Build flags and timings", "",
+ "All from `config.h`, read at generation time.", ""] + \
+ table(["define", "value", "effect"], rows)
+
+
+def section_files() -> list[str]:
+ return ["## Where each feature lives", "",
+ "All paths are relative to `users/raphaelmor/`.", ""] + \
+ table(["feature", "file"], [
+ ["layer list and order", "ramo_layers.h"],
+ ["key and colour per position", "layers/{CMK,HDP,HDG,MED,NAV,MOS,"
+ "SYM,NUM,FUN}-defs.h"],
+ ["the keymap itself", "../../keyboards/crkbd/rev4_1/standard/"
+ "keymaps/raphaelmor/keymap.c"],
+ ["combos", "ramo_combos.c"],
+ ["adaptive keys", "ramo_adaptive.c"],
+ ["linger brackets, Caps Word", "ramo_linger.c"],
+ ["Shift+Bksp, comma-leader shift", "ramo_process_record.c"],
+ ["base switch (tap dance)", "ramo_tap_dance.c"],
+ ["per-key RGB", "ramo_ledmap.c"],
+ ])
+
+
+def section_tooling() -> list[str]:
+ return [
+ "## Regenerating the docs",
+ "",
+ "```",
+ "python3 tools/kle_docs.py # this file and the layout repo",
+ "python3 tools/kle_export.py # corne-v4-1-ramo.json, for KLE",
+ "```",
+ "",
+ "`kle_export.py` runs the C preprocessor over `ramo_layers.h`, so it reads",
+ "the same keymap the firmware builds. It aborts instead of writing a wrong",
+ "file: an unknown keycode, a combo whose keys do not resolve, or a legend",
+ "too long for a cap all stop the export.",
+ "",
+ "To open it in [keyboard-layout-editor.com](http://www.keyboard-layout-editor.com),",
+ "use the **Upload** button and pick `corne-v4-1-ramo.json`. Do not paste that",
+ "file into the **Raw data** tab — that tab wraps whatever you type in `[ ]` of",
+ "its own, so a whole `.json` file ends up double-wrapped and draws one empty",
+ "key. Paste this instead:",
+ "",
+ "```",
+ "python3 tools/kle_export.py --raw -o /tmp/kle-raw.txt && pbcopy < /tmp/kle-raw.txt",
+ "```",
+ ]
+
+
+def notes_for(source: dict) -> str:
+ return build_notes(source["layers"], source["keys"], source["combos"],
+ source["adaptives"], source["activators"],
+ source["hidden"])
+
+
+def document_name(promethium: bool) -> str:
+ return ("Corne v4.1 · raphaelmor "
+ + ("Colemak + Hands Down Promethium" if promethium
+ else "Colemak + Hands Down Gold"))
+
+
+# GitHub URLs use blob/HEAD so they follow whatever the default branch is.
+QMK_REPO = "https://github.com/raphaelmor/qmk_userspace"
+ZMK_REPO = "https://github.com/raphaelmor/zmk-config"
+QMK_DOC = f"{QMK_REPO}/blob/HEAD/users/raphaelmor/README.md"
+
+
+def layout_doc(source: dict, image: str) -> str:
+ """The layout itself, with no board in it. For the keyboard-layout repo.
+
+ `image` is a format string taking the layer prefix, so the caller decides
+ where the pictures sit relative to the file it is writing.
+ """
+ layers, keys = source["layers"], source["keys"]
+ terms = read_terms()
+ base_prefixes = [p for _, p in layers[:2]]
+
+ out = section_grid() + [""] + section_alphas(layers, keys) + [""]
+ out += ["## Every layer", ""]
+ for enum, prefix in layers:
+ role = "base" if prefix in base_prefixes else "layer"
+ out += [f"### {display_name(enum)} — {role}", "",
+ f")})",
+ ""]
+ for block in (section_reaching(layers, source["activators"]),
+ section_combos(layers, keys, source["combos"]),
+ section_adaptives(layers, source["adaptives"], terms),
+ section_always_on(terms)):
+ out += block + [""]
+
+ out += [
+ "## Implementations",
+ "",
+ f"- **Corne v4.1 on QMK** — [{QMK_REPO.split('/')[-1]}]({QMK_DOC}). "
+ "The reference implementation; this page is generated from it.",
+ f"- **Totem on ZMK** — [{ZMK_REPO.split('/')[-1]}]({ZMK_REPO}). "
+ "38 keys, so it keeps `LB5` and `RB5`. Its own doc is still to write.",
+ ]
+ return "\n".join(out)
+
+
+def qmk_doc(source: dict) -> str:
+ """What is true of this board and this firmware, and nothing else."""
+ out: list[str] = []
+ for block in (section_board(),
+ section_not_drawn(source["hidden"]),
+ section_flags(source["promethium"], read_terms()),
+ section_files(),
+ section_tooling()):
+ out += block + [""]
+ return "\n".join(out[:-1])
+
+
+def one_board(source: dict, enum: str, prefix: str) -> list:
+ """A KLE document holding a single layer and no title decal.
+
+ The README wants one image per layer with a markdown heading above it, so
+ the decal that labels a board inside the stacked document is left out here.
+ """
+ sheet = Sheet()
+ layer_board(sheet, 0.0, source["keys"][prefix],
+ set(source["activators"].get(enum, {})))
+ rows = sheet.rows()
+ round_trip(sheet, rows, f"{prefix} board")
+ return [{"backcolor": L.BOARD_BG,
+ "name": f"{display_name(enum)} · {prefix}",
+ "plate": True}] + rows
+
+
+def build(promethium: bool) -> list:
+ source = read_all(promethium)
+ layers, keys = source["layers"], source["keys"]
+ base_prefixes = [p for _, p in layers[:2]]
+
+ sheet = Sheet()
+ y = 0.0
+ for enum, prefix in layers:
+ role = "base" if prefix in base_prefixes else "layer"
+ title(sheet, y, display_name(enum), f"{role} · {prefix}")
+ layer_board(sheet, y, keys[prefix],
+ set(source["activators"].get(enum, {})))
+ y += BOARD_PITCH
+
+ rows = sheet.rows()
+ round_trip(sheet, rows, "board")
+
+ head = {
+ "backcolor": L.BOARD_BG,
+ "name": document_name(promethium),
+ "notes": notes_for(source),
+ "plate": True,
+ }
+ return [head] + rows
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser(description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ ap.add_argument("-o", "--output", type=Path,
+ default=USERSPACE / "corne-v4-1-ramo.json")
+ ap.add_argument("--raw", action="store_true",
+ help="write the form KLE's Raw data tab wants: the same rows "
+ "with no enclosing brackets. That tab adds its own, so a "
+ "whole .json file pasted there parses as one empty key.")
+ ap.add_argument("--gold", action="store_true",
+ help="render Hands Down Gold instead of Promethium")
+ args = ap.parse_args()
+
+ try:
+ document = build(promethium=not args.gold)
+ except ExportError as err:
+ print(f"kle_export: {err}", file=sys.stderr)
+ return 1
+
+ if args.raw:
+ # One row per line, no outer [ ]. This is what the Raw data tab shows.
+ text = ",\n".join(json.dumps(row, ensure_ascii=False)
+ for row in document) + "\n"
+ else:
+ text = json.dumps(document, indent=2, ensure_ascii=False) + "\n"
+ args.output.write_text(text)
+
+ keys = sum(1 for row in document[1:] for item in row if isinstance(item, str))
+ print(f"wrote {args.output} — {keys} keys"
+ + (" (Raw data form)" if args.raw else ""))
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/users/raphaelmor/tools/kle_legends.py b/users/raphaelmor/tools/kle_legends.py
new file mode 100644
index 00000000..2893b27b
--- /dev/null
+++ b/users/raphaelmor/tools/kle_legends.py
@@ -0,0 +1,259 @@
+# SPDX-License-Identifier: MIT
+# Copyright (c) 2026 Raphaël Mor
+"""Display data for kle_export.py: keycode legends and hand-written notes.
+
+Everything here is presentation. The structure of the keymap comes from the C
+sources; this module only decides how a resolved keycode should read on a keycap.
+"""
+
+# --- Key appearance ----------------------------------------------------------
+# No colour. One neutral cap for everything, and a ghosted cap for a position
+# that has nothing bound to it, so the shape of each layer reads at a glance.
+# The greys are the ones from the original hand-made
+# corne-v4-1-miryoku-adaptation.json.
+
+KEY_COLOR = "#424242"
+KEY_TEXT = "#ffffff"
+# The key you hold to reach a layer, shown on that layer's own board. Ghosted,
+# because it is how you got here rather than something the layer binds.
+ACTIVATOR_COLOR = "#8c5656"
+
+UNBOUND = {"KC_NO", "KC_TRNS"}
+
+
+def is_unbound(keycode: str) -> bool:
+ return keycode.strip() in UNBOUND
+
+
+# --- Board chrome ------------------------------------------------------------
+
+BOARD_BG = "#626262"
+TITLE_COLOR = "#424242" # a decal draws no cap, so this only matters if d is off
+TITLE_TEXT = "#ffffff"
+
+
+# --- Keycode legends ---------------------------------------------------------
+# A legend is either a string (one centred label) or a (top, bottom) pair for a
+# key whose shifted glyph is worth showing.
+
+BASIC: dict[str, object] = {
+ "KC_NO": "",
+ "KC_TRNS": "▽",
+
+ # letters and digits fill themselves in below
+ "KC_QUOT": ('"', "'"),
+ "KC_COMM": ("<", ","),
+ "KC_DOT": (">", "."),
+ "KC_SLSH": ("?", "/"),
+ "KC_SCLN": (":", ";"),
+ "KC_MINS": ("_", "-"),
+ "KC_EQL": ("+", "="),
+ "KC_LBRC": ("{", "["),
+ "KC_RBRC": ("}", "]"),
+ "KC_BSLS": ("|", "\\"),
+ "KC_GRV": ("~", "`"),
+
+ "KC_DQUO": '"',
+ "KC_COLN": ":",
+ "KC_UNDS": "_",
+ "KC_PLUS": "+",
+ "KC_TILD": "~",
+ "KC_PIPE": "|",
+ "KC_EXLM": "!",
+ "KC_QUES": "?",
+ "KC_AT": "@",
+ "KC_HASH": "#",
+ "KC_DLR": "$",
+ "KC_PERC": "%",
+ "KC_CIRC": "^",
+ "KC_AMPR": "&",
+ "KC_ASTR": "*",
+ "KC_LPRN": "(",
+ "KC_RPRN": ")",
+ "KC_LCBR": "{",
+ "KC_RCBR": "}",
+
+ "KC_ESC": "Esc",
+ "KC_TAB": "Tab",
+ "KC_SPC": "␣",
+ "KC_ENT": "⏎",
+ "KC_BSPC": "⌫",
+ "KC_DEL": "⌦",
+ "KC_INS": "Ins",
+ "KC_APP": "▤",
+ "KC_PSCR": "PrtSc",
+ "KC_SCRL": "ScrLk",
+ "KC_PAUS": "Pause",
+
+ "KC_LEFT": "←",
+ "KC_DOWN": "↓",
+ "KC_UP": "↑",
+ "KC_RGHT": "→",
+ "KC_RIGHT": "→",
+ "KC_HOME": "Home",
+ "KC_END": "End",
+ "KC_PGUP": "PgUp",
+ "KC_PGDN": "PgDn",
+
+ "KC_LCTL": "⌃",
+ "KC_RCTL": "⌃",
+ "KC_LALT": "⌥",
+ "KC_RALT": "⌥",
+ "KC_ALGR": "AltGr",
+ "KC_LGUI": "⌘",
+ "KC_RGUI": "⌘",
+ "KC_LSFT": "⇧",
+ "KC_RSFT": "⇧",
+ "KC_HYPR": "Hyper",
+ "KC_MEH": "Meh",
+
+ "KC_MPRV": "Prev",
+ "KC_MNXT": "Next",
+ "KC_MPLY": "Play",
+ "KC_MSTP": "Stop",
+ "KC_MUTE": "Mute",
+ "KC_VOLD": "Vol−",
+ "KC_VOLU": "Vol+",
+
+ "MS_LEFT": "Ms ←",
+ "MS_DOWN": "Ms ↓",
+ "MS_UP": "Ms ↑",
+ "MS_RGHT": "Ms →",
+ "MS_WHLL": "Wh ←",
+ "MS_WHLD": "Wh ↓",
+ "MS_WHLU": "Wh ↑",
+ "MS_WHLR": "Wh →",
+ "MS_BTN1": "LMB",
+ "MS_BTN2": "RMB",
+ "MS_BTN3": "MMB",
+
+ "RM_TOGG": "RGB on",
+ "RM_NEXT": "RGB mode",
+ "RM_HUEU": "RGB hue",
+ "RM_SATU": "RGB sat",
+ "RM_VALU": "RGB val",
+
+ "CW_TOGG": "Caps Word",
+ "QK_BOOT": "Boot",
+}
+
+for _i in range(26):
+ BASIC["KC_" + chr(ord("A") + _i)] = chr(ord("A") + _i)
+for _d, _shifted in zip("1234567890", "!@#$%^&*()"):
+ BASIC["KC_" + _d] = (_shifted, _d)
+for _n in range(1, 25):
+ BASIC[f"KC_F{_n}"] = f"F{_n}"
+
+# Modifier wrappers: keycode prefix -> the symbol shown on the front face.
+MOD_TAPS = {
+ "LCTL_T": "⌃", "RCTL_T": "⌃",
+ "LALT_T": "⌥", "RALT_T": "⌥",
+ "LGUI_T": "⌘", "RGUI_T": "⌘",
+ "LSFT_T": "⇧", "RSFT_T": "⇧",
+ "HYPR_T": "Hyper", "MEH_T": "Meh",
+ "ALL_T": "Hyper",
+}
+
+# Chord wrappers: keycode prefix -> prefix string put in front of the inner legend.
+CHORDS = {
+ "LCTL": "⌃", "C": "⌃",
+ "LALT": "⌥", "A": "⌥",
+ "LGUI": "⌘", "G": "⌘", "LCMD": "⌘",
+ "LSFT": "⇧", "S": "⇧",
+ "SCMD": "⇧⌘", "SGUI": "⇧⌘",
+ "RCTL": "⌃", "RALT": "⌥", "RGUI": "⌘", "RSFT": "⇧",
+ "HYPR": "Hyper+", "MEH": "Meh+",
+}
+
+# Layer enum -> short name shown on a layer-tap front face.
+LAYER_NAMES = {
+ "L_COLEMAK": "Colemak",
+ "L_HANDSDOWN": "HandsDown",
+ "L_MEDIA": "Media",
+ "L_NAV": "Nav",
+ "L_MOUSE": "Mouse",
+ "L_SYM": "Sym",
+ "L_NUM": "Num",
+ "L_FUN": "Fun",
+}
+
+# Tap-dance enum -> what a double tap does. Short: this goes on a 1u cap.
+TAP_DANCES = {
+ "RAMO_TD_CMK": "2× CMK",
+ "RAMO_TD_HD": "2× HDP",
+}
+
+
+# --- Hand-written notes ------------------------------------------------------
+# COMBO_ACTION combos run C code, so their effect cannot be read off key_combos[].
+# Keyed by the enum name in ramo_combos.c.
+
+COMBO_ACTION_NOTES = {
+ "HD_COMBO_Th": "th",
+ "CMK_COMBO_Th": "th",
+ "HD_COMBO_QU": "qu",
+}
+
+# What each combo produces. Kept short — these go on a 1u keycap. The dead-key
+# combos show the dead key and the letter it makes: "´é" is ⌥E, then e.
+COMBO_RESULT_NOTES = {
+ "HD_COMBO_ACUT": "´é",
+ "HD_COMBO_GRV": "`è",
+ "HD_COMBO_CIRC": "ˆê",
+ "HD_COMBO_UMLA": "¨ü",
+ "HD_COMBO_TILDE": "˜ñ",
+ "HD_COMBO_CEDIL": "ç",
+ "HD_COMBO_OE": "œ",
+ "HD_COMBO_AE": "æ",
+ "HD_COMBO_STAB": "⇧Tab",
+ "HD_COMBO_APP": "▤",
+ "HD_COMBO_CAPW": "CapsW",
+}
+
+# The long form, for the list board. Anything absent falls back to the short form.
+COMBO_LONG_NOTES = {
+ "HD_COMBO_ACUT": "acute dead key (⌥E) — é",
+ "HD_COMBO_GRV": "grave dead key (⌥`) — è",
+ "HD_COMBO_CIRC": "circumflex dead key (⌥I) — ê",
+ "HD_COMBO_UMLA": "umlaut dead key (⌥U) — ü",
+ "HD_COMBO_TILDE": "tilde dead key (⌥N) — ñ",
+ "HD_COMBO_CEDIL": "ç (⌥C)",
+ "HD_COMBO_OE": "œ (⌥Q)",
+ "HD_COMBO_AE": "æ (⌥')",
+ "HD_COMBO_STAB": "Shift-Tab",
+ "HD_COMBO_APP": "App menu",
+ "HD_COMBO_QU": "qu — hold past 200 ms to drop the u and leave q",
+ "HD_COMBO_CAPW": "Caps Word on or off",
+}
+
+# Behaviors that are always on. These live in process_record / matrix_scan, not in
+# any table a parser could read, so they are transcribed here.
+#
+# The last two fields are a file and a string to find in it. The generator turns
+# them into a file:line anchor, so the line number cannot rot when the file moves
+# — and if the string ever disappears, the export stops rather than lying.
+# {TAPPING_TERM} is filled in from config.h.
+ALWAYS_ON = [
+ ("Linger brackets",
+ "Tap ( or {{ for one bracket. Hold past {TAPPING_TERM} ms for the pair, "
+ "caret inside: (|)",
+ "ramo_linger.c", "bool linger_track("),
+ ("Caps Word",
+ "Letters get ⇧. Digits, -, _, ⌫ keep it on. "
+ "Space → _, double space → real space + off.",
+ "ramo_linger.c", "bool caps_word_press_user("),
+ ("Shift + ⌫ = ⌦",
+ "Keeps Delete reachable on Hands Down, where it left the thumb row.",
+ "ramo_process_record.c", "keycode == KC_BSPC"),
+ ("Comma-leader shift",
+ "Hands Down only. Type , then a letter within {TAPPING_TERM} ms → "
+ "the comma goes, the letter gets ⇧.",
+ "ramo_process_record.c", "prior_keycode == KC_COMM"),
+ ("Combo hold on qu",
+ "Hold the W+M combo past {TAPPING_TERM} ms and the u is deleted, leaving q.",
+ "ramo_combos.c", "void combo_check_for_linger("),
+ ("Base switch",
+ "NAV layer, two left keys. Double-tap only, so a stray press cannot "
+ "change your base.",
+ "ramo_tap_dance.c", "ramo_TD_make_colemak_default"),
+]
diff --git a/users/raphaelmor/tools/kle_render.py b/users/raphaelmor/tools/kle_render.py
new file mode 100644
index 00000000..e26938f4
--- /dev/null
+++ b/users/raphaelmor/tools/kle_render.py
@@ -0,0 +1,247 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MIT
+# Copyright (c) 2026 Raphaël Mor
+"""Draw a KLE document as an SVG, in KLE's own style, with the legends included.
+
+KLE's "Download SVG" produces good-looking keycaps and **no text at all** — the
+export carries 296 keycap groups and zero nodes. So this module emits the
+same markup KLE does, copied from one of its exports, and then draws the legends
+on top.
+
+ python3 tools/kle_render.py corne-v4-1-ramo.json -o /tmp/board.svg
+
+Everything below is at KLE's native 54 px key unit, so the numbers match its
+output one for one:
+
+ shell x*54 + 1, y*54 + 1, w*54 - 2, h*54 - 2 rx 5
+ plate shell + (6, 3), shell - (12, 12) rx 5, lightness + 0.05
+ ghost shell only, no plate
+ decal nothing but its legends
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+from pathlib import Path
+from xml.sax.saxutils import escape
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from kle_export import LABEL_MAP # noqa: E402
+
+UNIT = 54 # KLE's key unit, in pixels
+PLATE_DX, PLATE_DY = 6, 3
+PLATE_DW, PLATE_DH = 12, 12
+OUTER_PAD = 10 # around the plate rectangle
+INNER_PAD = 5 # between the plate rectangle and the keys
+TEXT_PAD = 1.5
+FRONT_SCALE = 0.78 # the front lip is much shorter than the keytop
+DESCENDER = 0.22 # of an em; the tail of a comma has to stay on the keytop
+BOTTOM_LIFT = 0.45 # of an em, on top of the descender. The front lip carries
+ # visual weight below the plate, so a legend pinned to the
+ # plate's bottom edge reads lower than it measures.
+
+# Copied verbatim from a KLE export, so the caps render the way KLE draws them.
+STYLE = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+
+
+def deserialize(document: list) -> tuple[dict, list[dict]]:
+ """KLE document -> (metadata, keys with absolute x/y/w and their properties)."""
+ meta = document[0] if isinstance(document[0], dict) else {}
+ rows = document[1:] if isinstance(document[0], dict) else document
+
+ keys: list[dict] = []
+ state = {"c": "#cccccc", "t": "#000000", "a": 4, "w": 1.0, "f": 3,
+ "d": False, "g": False}
+ y = 0.0
+ for row in rows:
+ if isinstance(row, dict):
+ continue
+ x = 0.0
+ for item in row:
+ if isinstance(item, dict):
+ x += item.get("x", 0)
+ y += item.get("y", 0)
+ for prop in ("c", "t", "a", "d", "g", "w", "f"):
+ if prop in item:
+ state[prop] = item[prop]
+ continue
+ keys.append({"x": x, "y": y, "w": state["w"], "labels": item,
+ "c": state["c"], "t": state["t"], "a": state["a"],
+ "f": state["f"], "d": state["d"], "g": state["g"]})
+ x += state["w"]
+ state["w"] = 1.0 # KLE resets width and decal after every key
+ state["d"] = False
+ y += 1.0
+ return meta, keys
+
+
+def place(labels: str, align: int) -> dict[int, str]:
+ """Label string -> {visual position: text}, using KLE's label map."""
+ out = {}
+ for slot, text in enumerate(labels.split("\n")):
+ if not text or slot >= 12:
+ continue
+ visual = LABEL_MAP[align][slot]
+ if visual >= 0:
+ out[visual] = text
+ return out
+
+
+def keytop_color(color: str) -> str:
+ """KLE's keytop is the cap colour with 0.05 added to its HSL lightness.
+
+ Checked against its own export: #424242 becomes #4f4f4f.
+ """
+ if not (color.startswith("#") and len(color) == 7):
+ return color
+ rgb = [int(color[i:i + 2], 16) / 255 for i in (1, 3, 5)]
+ low, high = min(rgb), max(rgb)
+ lightness = (low + high) / 2
+ target = min(1.0, lightness + 0.05)
+ if high == low: # grey: no hue to preserve
+ return "#" + "%02x" % round(target * 255) * 3
+ delta = high - low
+ sat = delta / (2 - high - low if lightness > 0.5 else high + low)
+ hue = {high: 0.0}
+ if high == rgb[0]:
+ hue = ((rgb[1] - rgb[2]) / delta) % 6
+ elif high == rgb[1]:
+ hue = (rgb[2] - rgb[0]) / delta + 2
+ else:
+ hue = (rgb[0] - rgb[1]) / delta + 4
+ chroma = (1 - abs(2 * target - 1)) * sat
+ second = chroma * (1 - abs(hue % 2 - 1))
+ base = target - chroma / 2
+ parts = [(chroma, second, 0), (second, chroma, 0), (0, chroma, second),
+ (0, second, chroma), (second, 0, chroma), (chroma, 0, second)]
+ return "#" + "".join("%02x" % round((c + base) * 255)
+ for c in parts[int(hue)])
+
+
+def wrap(text: str, size: float, room: float) -> list[str]:
+ """Split on a space when a legend is too wide. SVG will not wrap for us."""
+ if len(text) * size * 0.55 <= room or " " not in text:
+ return [text]
+ words = text.split(" ")
+ best, score = 1, None
+ for cut in range(1, len(words)):
+ spread = abs(len(" ".join(words[:cut])) - len(" ".join(words[cut:])))
+ if score is None or spread < score:
+ best, score = cut, spread
+ return [" ".join(words[:best]), " ".join(words[best:])]
+
+
+def draw_text(x, y, anchor, text, size, color, room) -> list[str]:
+ lines = wrap(text, size, room)
+ step = size * 1.05
+ top = y - step * (len(lines) - 1) / 2
+ return [f'{escape(line)}'
+ for i, line in enumerate(lines)]
+
+
+def keycap(key: dict) -> list[str]:
+ """One keycap group, in the markup KLE's own SVG export uses."""
+ x, y = key["x"] * UNIT + 1, key["y"] * UNIT + 1
+ w, h = key["w"] * UNIT - 2, UNIT - 2
+ classes = ("ghosted keycap" if key["g"]
+ else " decal keycap" if key["d"] else " keycap")
+ out = [f""]
+ if not key["d"]:
+ shell = (f'x="{x:g}" y="{y:g}" width="{w:g}" height="{h:g}" '
+ f'rx="5" fill="{key["c"]}"')
+ out += [f' ',
+ f' ']
+ if not key["g"]:
+ top = (f'x="{x + PLATE_DX:g}" y="{y + PLATE_DY:g}" '
+ f'width="{w - PLATE_DW:g}" height="{h - PLATE_DH:g}" '
+ f'rx="5" fill="{keytop_color(key["c"])}"')
+ out += [f' ',
+ f' ']
+
+ # KLE stops here. Everything below is the text it does not export.
+ px, py = x + PLATE_DX, y + PLATE_DY
+ pw, ph = w - PLATE_DW, h - PLATE_DH
+ size = 6 + 2 * key["f"]
+ color = key["t"] if isinstance(key["t"], str) else "#000000"
+ color = color.split("\n")[0] or "#000000"
+ columns = [(px + TEXT_PAD, "start"), (px + pw / 2, "middle"),
+ (px + pw - TEXT_PAD, "end")]
+ # The front lip is only 9 px tall on a 52 px cap, so its legends are smaller
+ # than the ones on the keytop or they spill over the plate edge.
+ lip_top, lip_h = py + ph, h - PLATE_DY - ph
+ lip_size = size * FRONT_SCALE
+ rows = [(py + TEXT_PAD + size * 0.85, size), # top of the keytop
+ (py + ph / 2 + size * 0.35, size), # its middle
+ (py + ph - TEXT_PAD - size * (DESCENDER + BOTTOM_LIFT), size),
+ (lip_top + lip_h / 2 + lip_size * 0.35, lip_size)]
+ for visual, text in place(key["labels"], key["a"]).items():
+ cx, anchor = columns[visual % 3]
+ baseline, font = rows[visual // 3]
+ room = pw - 2 * TEXT_PAD if visual % 3 == 1 else pw
+ out += [" " + line for line in
+ draw_text(cx, baseline, anchor, text, font, color, room)]
+ out.append("")
+ return out
+
+
+def render(document: list) -> str:
+ meta, keys = deserialize(document)
+ content_w = max(k["x"] + k["w"] for k in keys) * UNIT
+ content_h = (max(k["y"] for k in keys) + 1) * UNIT
+ plate_w = content_w + 2 * INNER_PAD
+ plate_h = content_h + 2 * INNER_PAD
+ width = plate_w + 2 * OUTER_PAD
+ height = plate_h + 2 * OUTER_PAD
+
+ parts = [
+ f""]
+ return "\n".join(parts) + "\n"
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser(description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ ap.add_argument("input", type=Path)
+ ap.add_argument("-o", "--output", type=Path, default=Path("/tmp/kle.svg"))
+ args = ap.parse_args()
+
+ args.output.write_text(render(json.loads(args.input.read_text())))
+ print(f"wrote {args.output}")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())