Add layout doc tooling, linger brackets, and GPL-3.0 licensing

Generate the layout documentation from the C sources rather than by hand.
tools/kle_export.py runs the C preprocessor over ramo_layers.h, so it reads
the same keymap the firmware builds; kle_render.py draws each layer in KLE's
own keycap markup, adding the legends KLE's SVG export omits; kle_docs.py
writes this README and the keyboard-layout repo from one parse.

Add ramo_linger.c: hold-to-autopair brackets and the Caps Word space rule.

License users/raphaelmor as GPL-3.0-or-later. The design follows moutis'
Hands Down Promethium, whose QMK implementation is GPL-3.0; QMK is
GPL-2.0-or-later, so the combination is GPL-3.0. tools/ stays MIT, being
original and shared with the MIT layout repo.
This commit is contained in:
Raphaël Mor 2026-09-01 19:44:22 +02:00
commit 44804ef5d6
34 changed files with 5930 additions and 122 deletions

4
.gitignore vendored
View file

@ -14,3 +14,7 @@
compile_commands.json
.clangd/
.cache/
# Python caches
__pycache__/
*.pyc

View file

@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hands Down Promethium — full layer map</title>
<style>
:root{
--bg:#0f1117; --card:#171a22; --key:#222634; --key2:#1b1f2b;
--ink:#e7e9ee; --dim:#8b90a0; --line:#2c3142;
--alpha:#e7e9ee; --mod:#f6c177; --layer:#9ccfd8; --sem:#c4a7e7;
--sym:#7fd6a6; --media:#eb6f92; --rgb:#f28fad; --num:#a3be8c;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);
font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;padding:28px}
h1{font-size:22px;margin:0 0 4px}
.sub{color:var(--dim);margin:0 0 20px;max-width:900px}
.legend{display:flex;flex-wrap:wrap;gap:14px;margin:0 0 26px;font-size:12px}
.legend span{display:inline-flex;align-items:center;gap:6px;color:var(--dim)}
.legend i{width:12px;height:12px;border-radius:3px;display:inline-block}
.layer{background:var(--card);border:1px solid var(--line);border-radius:14px;
padding:18px 20px 22px;margin:0 0 22px}
.layer h2{margin:0 0 2px;font-size:17px;display:flex;align-items:baseline;gap:10px}
.layer h2 b{font-size:12px;font-weight:600;color:#0f1117;background:var(--layer);
padding:2px 8px;border-radius:20px}
.layer .how{color:var(--dim);font-size:12px;margin:0 0 16px}
.board{display:flex;gap:44px;flex-wrap:wrap;justify-content:center}
.half{display:grid;grid-template-columns:repeat(6,minmax(0,58px));gap:6px}
.row{display:contents}
.key{background:var(--key);border:1px solid var(--line);border-radius:8px;
height:52px;padding:4px 5px;position:relative;display:flex;flex-direction:column;
justify-content:center;align-items:center;text-align:center;overflow:hidden}
.key .m{font-size:13px;font-weight:600;line-height:1.15;word-break:break-word}
.key .h{position:absolute;top:2px;right:4px;font-size:9px;color:var(--dim);font-weight:700}
.key.trns{background:transparent;border-style:dashed;color:var(--dim)}
.key.trns .m{color:var(--dim);font-weight:400;font-size:15px}
.key.none{background:transparent;border-color:#20222c}
.key.none .m{color:#3a3f52}
.key.mod .m{color:var(--mod)} .key.layer .m{color:var(--layer)}
.key.sem .m{color:var(--sem);font-size:11px} .key.sym .m{color:var(--sym)}
.key.media .m{color:var(--media);font-size:11px} .key.rgb .m{color:var(--rgb);font-size:10px}
.key.num .m{color:var(--num)}
.thumbs{grid-column:1/7;display:grid;grid-template-columns:repeat(6,minmax(0,58px));gap:6px;margin-top:4px}
.thumbs .key{height:46px}
.foot{color:var(--dim);font-size:12px;margin-top:8px;max-width:900px}
code{background:#20222c;padding:1px 5px;border-radius:4px}
</style>
</head>
<body>
<h1>Hands Down&nbsp;·&nbsp;Promethium — full layer map</h1>
<p class="sub">Canonical moutis HD system (<code>github.com/moutis/HandsDown</code>), 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 = <b>hold</b> behavior (mod or layer). Layers are reached by <b>holding</b> a thumb/home key on the alpha layer.</p>
<div class="legend">
<span><i style="background:var(--alpha)"></i>alpha / letter</span>
<span><i style="background:var(--mod)"></i>mod (hold)</span>
<span><i style="background:var(--layer)"></i>layer-tap / switch</span>
<span><i style="background:var(--sem)"></i>semantic key (SK_*)</span>
<span><i style="background:var(--sym)"></i>symbol</span>
<span><i style="background:var(--num)"></i>number / nav</span>
<span><i style="background:var(--media)"></i>media</span>
<span><i style="background:var(--rgb)"></i>RGB / config</span>
<span><i style="border:1px dashed var(--dim)"></i>▽ transparent</span>
</div>
<div id="app"></div>
<p class="foot">How to reach each layer from the alpha layer (all momentary, via layer-tap holds):
<b>SYM</b> = hold Space · <b>NAV</b> = hold R (L-thumb) or Enter (R-thumb) · <b>FUN</b> = hold Bksp (L-thumb) or F · <b>NUM</b> = hold K · <b>CFG</b> = 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).</p>
<script>
// ---- raw keycodes per layer, in visual order ----
// each half: [ row of 6 (outer→inner) ]; thumbs: 3 inner keys
const L = {
HD:{purpose:"Base alpha — Hands Down Promethium", how:"letters + home-row mods + thumb layer-taps",
top:[["KC_LNG1","KC_V","KC_W","KC_G","KC_M","KC_J"],["KC_HASH","KC_DOT","KC_SLSH","KC_DQUO","KC_QUOT","KC_LNG2"]],
home:[["KC_ESC","RCTL_T(KC_S)","RALT_T(KC_N)","RGUI_T(KC_T)","RSFT_T(KC_H)","LT(L_NUM,KC_K)"],["KC_COMM","RSFT_T(KC_A)","RGUI_T(KC_E)","RALT_T(KC_I)","RCTL_T(KC_C)","KC_Z"]],
bot:[["KC_TAB","LT(L_FUN,KC_F)","KC_P","KC_D","KC_L","KC_X"],["KC_MINS","KC_U","KC_O","KC_Y","KC_B","KC_Q"]],
thumbs:[["LT(L_FUN,KC_BSPC)","LT(L_NAV,KC_R)","LT(L_CFG,KC_MUTE)"],["LT(L_CFG,KC_MPLY)","LT(L_SYM,KC_SPC)","LT(L_NAV,KC_ENT)"]]},
SYM:{purpose:"Symbols & punctuation",how:"hold Space",
top:[["KC_TRNS","KC_Z","KC_Q","SK_PARA","SK_SCRS","SK_DCRS"],["SK_CENT","KC_DLR","SK_EURO","SK_BPND","SK_JPY","KC_TRNS"]],
home:[["KC_TRNS","SK_SECT","KC_LBRC","KC_LPRN","KC_LCBR","SK_BBLT"],["KC_PSLS","RSFT_T(KC_PEQL)","RGUI_T(KC_PAST)","RALT_T(KC_PPLS)","RCTL_T(KC_PMNS)","KC_TRNS"]],
bot:[["KC_TRNS","SK_DEGR","KC_RBRC","KC_RPRN","KC_RCBR","SK_SBLT"],["KC_CIRC","KC_PIPE","KC_AMPR","KC_LT","KC_GT","KC_TRNS"]],
thumbs:[["SK_DELWDL","SK_DELWDR","KC_VOLD"],["KC_VOLU","KC_UNDS","SK_IEXC"]]},
FUN:{purpose:"Function keys (left) · number row (right)",how:"hold Bksp or F",
top:[["RGB_TOG","KC_F13","KC_F7","KC_F8","KC_F9","KC_F10"],["KC_SLSH","KC_7","KC_8","KC_9","KC_MINS","RGB_MOD"]],
home:[["RGB_VAI","LCTL_T(KC_F14)","LALT_T(KC_F1)","LGUI_T(KC_F2)","LSFT_T(KC_F3)","KC_F11"],["KC_ASTR","RSFT_T(KC_1)","RGUI_T(KC_2)","RALT_T(KC_3)","RCTL_T(KC_PPLS)","RGB_HUI"]],
bot:[["RGB_VAD","KC_F15","KC_F4","KC_F5","KC_F3","KC_F12"],["KC_COMM","KC_4","KC_5","KC_6","KC_EQL","RGB_HUD"]],
thumbs:[["KC_SCLN","TG(L_NUM)","KC_INS"],["KC_TRNS","KC_0","KC_DOT"]]},
NUM:{purpose:"Navigation (left) · numpad (right)",how:"hold K → right-hand numpad only (left index is busy holding K). For two-handed L-nav + R-numpad, TOGGLE it on: R+Space combo, or TG(NUM) from FUN.",
top:[["KC_TRNS","SK_DOCBEG","KC_HOME","KC_UP","KC_PGUP","SK_PARAPRV"],["KC_PSLS","KC_P7","KC_P8","KC_P9","KC_PMNS","KC_TRNS"]],
home:[["KC_TRNS","SK_WORDPRV","KC_LEFT","KC_DOWN","KC_RGHT","SK_WORDNXT"],["KC_PAST","RSFT_T(KC_P4)","RGUI_T(KC_P5)","RALT_T(KC_P6)","RCTL_T(KC_PPLS)","KC_TRNS"]],
bot:[["KC_TRNS","SK_DOCEND","KC_END","KC_SPC","KC_PGDN","SK_PARANXT"],["KC_PCMM","KC_P1","KC_P2","KC_P3","KC_PEQL","KC_TRNS"]],
thumbs:[["LGUI_T(KC_BSPC)","LSFT_T(KC_ENT)","SK_CLOZ"],["KC_C","KC_P0","KC_PDOT"]]},
NAV:{purpose:"Edit & mods (left) · arrows/nav (right)",how:"hold R (left thumb) or Enter (right thumb) — the held thumb frees the OTHER hand: navigate arrows with the right, hold ⌃⌥⌘⇧ + edit keys with the left.",
top:[["KC_TRNS","KC_TAB","SK_CLOZ","SK_SCAP","SK_SCLP","SK_PSTM"],["SK_DOCBEG","KC_HOME","KC_UP","KC_PGUP","SK_PARAPRV","KC_TRNS"]],
home:[["KC_VOLU","KC_LCTL","KC_LALT","KC_LGUI","KC_LSFT","SK_FIND"],["SK_WORDPRV","KC_LEFT","KC_DOWN","KC_RGHT","SK_WORDNXT","KC_TRNS"]],
bot:[["KC_VOLD","SK_UNDO","SK_CUT","SK_COPY","SK_PSTE","SK_FAGN"],["SK_DOCEND","KC_END","KC_DOWN","KC_PGDN","SK_PARANXT","KC_TRNS"]],
thumbs:[["KC_BSPC","KC_ENTER","SK_ZOOMRST"],["SK_ZOOMRST","SK_HISTPRV","SK_HISTNXT"]]},
CFG:{purpose:"Keyboard config (left) · media & consumer (right)",how:"hold Mute/Play, or CFG combo",
top:[["KC_NO","SK_Lux","SK_Win","SK_Mac","QK_RBT","SK_KILL"],["SK_ZOOMRST","KC_NUM","KC_EJCT","KC_LALT","LCA(KC_DEL)","RGB_MOD"]],
home:[["RGB_VAI","HD_AdaptKeyToggle","HD_L_QWERTY","HD_L_ALPHA","KC_VOLU","KC_BRIU"],["SK_ZOOMOUT","KC_MPRV","KC_MUTE","KC_MNXT","EE_CLR","RGB_SAI"]],
bot:[["RGB_HUI","QK_BOOT","AG_SWAP","AG_NORM","KC_VOLD","KC_BRID"],["SK_ZOOMIN","KC_MRWD","KC_MPLY","KC_MFFD","QK_BOOT","RGB_HUD"]],
thumbs:[["SK_ZOOMOUT","SK_ZOOMIN","SK_ZOOMRST"],["SK_ZOOMRST","SK_ZOOMIN","SK_ZOOMOUT"]]},
};
// ---- keycode → readable label ----
const SK={PARA:"¶",SCRS:"†",DCRS:"‡",CENT:"¢",EURO:"€",BPND:"£",JPY:"¥",SECT:"§",IEXC:"¡",BBLT:"•",SBLT:"·",DEGR:"°",IQUE:"¿",BLLT:"•",ELPS:"…",MDSH:"—",
DELWDL:"Del Wd◀",DELWDR:"Del Wd▶",WORDPRV:"Word◀",WORDNXT:"Word▶",DOCBEG:"Doc Top",DOCEND:"Doc End",
LINEBEG:"Ln Beg",LINEEND:"Ln End",PARAPRV:"¶ ◀",PARANXT:"¶ ▶",
UNDO:"Undo",REDO:"Redo",CUT:"Cut",COPY:"Copy",PSTE:"Paste",PSTM:"Paste=",SALL:"Sel All",SWRD:"Sel Wd",
FIND:"Find",FAGN:"Find→",NEW:"New",OPEN:"Open",CLOZ:"Close",QUIT:"Quit",
SCAP:"Shot▸file",SCLP:"Shot▸clip",SRCH:"Search",KILL:"Force-Quit",HENT:"Hard⏎",
ZOOMIN:"Zoom+",ZOOMOUT:"Zoom",ZOOMRST:"Zoom·0",HISTPRV:"◀ Back",HISTNXT:"Fwd ▶",
Mac:"OS·Mac",Win:"OS·Win",Lux:"OS·Linux",MHEN:"英数",HENK:"かな",DKT8:"Dictate",AIVC:"AI/Siri"};
const KC={SPC:"Space",BSPC:"⌫",ENT:"⏎",ENTER:"⏎",TAB:"⇥",ESC:"Esc",DEL:"⌦",INS:"Ins",
HOME:"Home",END:"End",PGUP:"PgUp",PGDN:"PgDn",UP:"↑",DOWN:"↓",LEFT:"←",RGHT:"→",RIGHT:"→",
MUTE:"🔇",VOLU:"Vol+",VOLD:"Vol",MPLY:"⏯",MPRV:"⏮",MNXT:"⏭",MSTP:"⏹",MRWD:"⏪",MFFD:"⏩",
EJCT:"⏏",BRIU:"☀+",BRID:"☀−",NUM:"NumLk",CAPS:"CapsLk",APP:"▤ Menu",
DLR:"$",HASH:"#",SLSH:"/",DQUO:'"',QUOT:"'",COMM:",",DOT:".",MINS:"",UNDS:"_",EQL:"=",
CIRC:"^",PIPE:"|",AMPR:"&",LT:"<",GT:">",LBRC:"[",RBRC:"]",LPRN:"(",RPRN:")",LCBR:"{",RCBR:"}",
ASTR:"*",PLUS:"+",SCLN:";",TILD:"~",GRAVE:"`",EXLM:"!",QUES:"?",
PSLS:"/",PAST:"*",PMNS:"",PPLS:"+",PEQL:"=",PDOT:".",PCMM:",",PENT:"⏎",
P0:"0",P1:"1",P2:"2",P3:"3",P4:"4",P5:"5",P6:"6",P7:"7",P8:"8",P9:"9",
LCTL:"⌃",LALT:"⌥",LGUI:"⌘",LSFT:"⇧",RCTL:"⌃",RALT:"⌥",RGUI:"⌘",RSFT:"⇧",
LNG1:"英/かな",LNG2:"かな",T:"T",C:"C"};
const MODSYM={SFT:"⇧",CTL:"⌃",ALT:"⌥",GUI:"⌘",HYPR:"✦",MEH:"◆"};
const LAYERNAME={L_SYM:"SYM",L_FUN:"FUN",L_NUM:"NUM",L_NAV:"NAV",L_CFG:"CFG",L_HD:"HD"};
const EXACT={ "LALT(KC_8)":{m:"•",h:"⌥8"}, "LSA(KC_8)":{m:"°",h:"⇧⌥8"}, "LSA(KC_9)":{m:"·",h:"⇧⌥9"},
"LALT(KC_9)":{m:"ª",h:"⌥9"}, "LCA(KC_DEL)":{m:"⌃⌥⌦",h:"kill"}, "G(KC_TAB)":{m:"⌘⇥"}, "G(KC_R)":{m:"⌘R"},
"HD_AdaptKeyToggle":{m:"Adaptive",h:"on/off",cls:"layer"}, "HD_L_QWERTY":{m:"▸QWERTY",cls:"layer"},
"HD_L_ALPHA":{m:"▸HD",cls:"layer"}, "QK_BOOT":{m:"Bootloader",cls:"rgb"}, "QK_RBT":{m:"Reboot",cls:"rgb"},
"EE_CLR":{m:"Clr EEPROM",cls:"rgb"}, "AG_SWAP":{m:"⌥/⌘ swap",cls:"rgb"}, "AG_NORM":{m:"⌥/⌘ norm",cls:"rgb"},
"KC_TRNS":{m:"▽",cls:"trns"}, "KC_NO":{m:"∅",cls:"none"} };
function fmt(kc){
if(EXACT[kc]) {const e=EXACT[kc];return {m:e.m,h:e.h||"",cls:e.cls||classify(kc,e.m)};}
let m;
// mod-tap RSFT_T(KC_A)
let mt=kc.match(/^[LR](SFT|CTL|ALT|GUI|HYPR|MEH)_T\((.+)\)$/);
if(mt){let inner=fmt(mt[2]);return {m:inner.m,h:MODSYM[mt[1]],cls:"mod"};}
// layer-tap LT(L_x,KC_y)
let lt=kc.match(/^LT\((\w+),\s*(.+)\)$/);
if(lt){let inner=fmt(lt[2]);return {m:inner.m,h:"▸"+(LAYERNAME[lt[1]]||lt[1].replace("L_","")),cls:"layer"};}
// MO/TG/TO/TT/DF(L_x)
let mo=kc.match(/^(MO|TG|TO|TT|DF)\((\w+)\)$/);
if(mo){return {m:"▸"+(LAYERNAME[mo[2]]||mo[2].replace("L_","")),h:mo[1].toLowerCase(),cls:"layer"};}
// SK_*
let sk=kc.match(/^SK_(\w+)$/);
if(sk){return {m:SK[sk[1]]||sk[1],h:"",cls:classify(kc,SK[sk[1]]||"")};}
// RGB_*
let rg=kc.match(/^RGB_(\w+)$/);
if(rg){const map={TOG:"RGB ⏻",MOD:"RGB ▸",VAI:"RGB ☀+",VAD:"RGB ☀−",HUI:"RGB hue+",HUD:"RGB hue",SAI:"RGB sat+",SAD:"RGB sat"};
return {m:map[rg[1]]||("RGB "+rg[1]),h:"",cls:"rgb"};}
// KC_*
let k=kc.match(/^KC_(\w+)$/);
if(k){m=KC[k[1]] !== undefined ? KC[k[1]] : (k[1].match(/^F\d+$/)?k[1]: k[1].length===1?k[1]:k[1]);
return {m:m,h:"",cls:classify(kc,m)};}
return {m:kc,h:"",cls:"base"};
}
function classify(kc,m){
if(/SK_/.test(kc))return "sem";
if(/RGB_|QK_|AG_|EE_/.test(kc))return "rgb";
if(/VOL|MPLY|MPRV|MNXT|MSTP|MRWD|MFFD|MUTE|BRIU|BRID|EJCT/.test(kc))return "media";
if(/KC_(DLR|HASH|SLSH|DQUO|QUOT|COMM|DOT|MINS|UNDS|EQL|CIRC|PIPE|AMPR|LT|GT|LBRC|RBRC|LPRN|RPRN|LCBR|RCBR|ASTR|PLUS|SCLN|TILD|GRAVE|EXLM|QUES|PSLS|PAST|PMNS|PPLS|PEQL)/.test(kc))return "sym";
if(/KC_(P?\d|P[0-9]|HOME|END|PGUP|PGDN|UP|DOWN|LEFT|RGHT|RIGHT|PDOT|PCMM)/.test(kc))return "num";
if(/KC_[LR](CTL|ALT|GUI|SFT)$/.test(kc))return "mod";
return "base";
}
function keyEl(kc){
const f=fmt(kc);
const d=document.createElement("div");
d.className="key "+(f.cls||"base");
d.title=kc;
d.innerHTML=(f.h?`<span class="h">${f.h}</span>`:"")+`<span class="m">${f.m}</span>`;
return d;
}
function half(rows,thumbs,side){
const h=document.createElement("div");h.className="half";
rows.forEach(r=>r.forEach(kc=>h.appendChild(keyEl(kc))));
const t=document.createElement("div");t.className="thumbs";
// pad: left half thumbs sit in cols 4-6; right half in cols 1-3
const cells = side==="L" ? ["","","",...thumbs] : [...thumbs,"","",""];
cells.forEach(kc=>{ if(kc==="") {const s=document.createElement("div");s.style.visibility="hidden";t.appendChild(s);} else t.appendChild(keyEl(kc)); });
h.appendChild(t);
return h;
}
const app=document.getElementById("app");
Object.entries(L).forEach(([name,d])=>{
const sec=document.createElement("div");sec.className="layer";
sec.innerHTML=`<h2><b>${name}</b> ${d.purpose}</h2><p class="how">reach: ${d.how}</p>`;
const board=document.createElement("div");board.className="board";
board.appendChild(half([d.top[0],d.home[0],d.bot[0]],d.thumbs[0],"L"));
board.appendChild(half([d.top[1],d.home[1],d.bot[1]],d.thumbs[1],"R"));
sec.appendChild(board);app.appendChild(sec);
});
</script>
</body>
</html>

View file

@ -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).

674
users/raphaelmor/LICENSE Normal file
View file

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 <https://www.gnu.org/licenses/>.
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:
<program> Copyright (C) <year> <name of author>
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
<https://www.gnu.org/licenses/>.
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
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View file

@ -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** |

View file

@ -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..)
<!-- layout:begin -->
<!-- Generated by qmk_userspace/users/raphaelmor/tools/kle_docs.py. Do not hand-edit this section; edit around it. -->
## 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
```
<!-- layout:end -->
## 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.

View file

@ -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

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,358 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Corne layers — current · full HD · proposed hybrid</title>
<style>
:root{
--bg:#0f1117; --card:#171a22; --key:#222634;
--ink:#e7e9ee; --dim:#8b90a0; --line:#2c3142;
--alpha:#e7e9ee; --mod:#f6c177; --layer:#9ccfd8; --sem:#c4a7e7;
--sym:#7fd6a6; --media:#eb6f92; --rgb:#f28fad; --num:#a3be8c; --mouse:#e0af68;
--new:#a6e3a1; --chg:#f6c177; --ling:#c4a7e7;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);
font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;padding:0 28px 40px}
h1{font-size:22px;margin:16px 0 4px}
.sub{color:var(--dim);margin:0 0 14px;max-width:980px}
.controls{position:sticky;top:0;z-index:10;background:rgba(15,17,23,.94);backdrop-filter:blur(6px);
border-bottom:1px solid var(--line);padding:14px 0;margin:0 0 18px;display:flex;gap:26px;flex-wrap:wrap;align-items:flex-end}
.ctl{display:flex;flex-direction:column;gap:5px}
.ctl label{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.04em}
.seg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden}
.seg button{background:var(--key);color:var(--dim);border:0;padding:6px 12px;font:inherit;font-size:12px;cursor:pointer}
.seg button.on{background:var(--layer);color:#0f1117;font-weight:700}
.seg.big button{padding:8px 14px;font-size:13px}
.controls:not(.prop) .proponly{display:none}
.callout{background:#1b2030;border:1px solid var(--line);border-left:3px solid var(--layer);
border-radius:8px;padding:12px 16px;margin:0 0 20px;max-width:980px;font-size:13px}
.callout b{color:var(--ink)} .callout code{background:#20222c;padding:1px 5px;border-radius:4px}
.legend{display:flex;flex-wrap:wrap;gap:14px;margin:0 0 22px;font-size:12px}
.legend span{display:inline-flex;align-items:center;gap:6px;color:var(--dim)}
.legend i{width:12px;height:12px;border-radius:3px;display:inline-block}
.layer{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:18px 20px 22px;margin:0 0 22px}
.layer h2{margin:0 0 2px;font-size:17px;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.layer h2 b{font-size:12px;font-weight:600;color:#0f1117;padding:2px 8px;border-radius:20px}
.layer h2 .badge{font-size:11px;font-weight:700;color:#0f1117;background:var(--chg);padding:2px 8px;border-radius:20px}
.layer .how{color:var(--dim);font-size:12px;margin:0 0 16px}
.board{display:flex;gap:44px;flex-wrap:wrap;justify-content:center}
.half{display:grid;grid-template-columns:repeat(6,minmax(0,58px));gap:6px}
.key{background:var(--key);border:1px solid var(--line);border-radius:8px;height:52px;padding:4px 5px;
position:relative;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;overflow:hidden}
.key .m{font-size:13px;font-weight:600;line-height:1.15;word-break:break-word}
.key .h{position:absolute;top:2px;right:4px;font-size:9px;color:var(--dim);font-weight:700}
.key .tag{position:absolute;bottom:1px;left:3px;font-size:8px;font-weight:800;letter-spacing:.03em;display:none}
.key.trns{background:transparent;border-style:dashed} .key.trns .m{color:var(--dim);font-weight:400;font-size:15px}
.key.none{background:transparent;border-color:#20222c} .key.none .m{color:#3a3f52}
.key.mod .m{color:var(--mod)} .key.layer .m{color:var(--layer)}
.key.sem .m{color:var(--sem);font-size:11px} .key.sym .m{color:var(--sym)}
.key.media .m{color:var(--media);font-size:11px} .key.rgb .m{color:var(--rgb);font-size:10px}
.key.num .m{color:var(--num)} .key.mouse .m{color:var(--mouse);font-size:11px}
body.diff .key.NEW{border-color:var(--new)} body.diff .key.NEW .tag{display:block;color:var(--new)}
body.diff .key.CHG{border-color:var(--chg)} body.diff .key.CHG .tag{display:block;color:var(--chg)}
body.diff .key.LING{border-color:var(--ling)} body.diff .key.LING .tag{display:block;color:var(--ling)}
body.diff .key.REM{border-color:var(--media);opacity:.55} body.diff .key.REM .tag{display:block;color:var(--media)}
.thumbs{grid-column:1/7;display:grid;grid-template-columns:repeat(6,minmax(0,58px));gap:6px;margin-top:4px}
.thumbs .key{height:46px}
.engine{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px;margin:6px 0 26px}
.ecard{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px 16px}
.ecard h3{margin:0 0 6px;font-size:14px} .ecard p{margin:0;color:var(--dim);font-size:12px}
.ecard .where{color:var(--ling);font-size:11px;margin-top:8px}
h2.section{font-size:15px;color:var(--dim);margin:26px 0 10px;border-top:1px solid var(--line);padding-top:18px}
.foot{color:var(--dim);font-size:12px;margin-top:8px;max-width:980px}
code{background:#20222c;padding:1px 5px;border-radius:4px}
</style>
</head>
<body class="diff">
<div class="controls prop" id="controls">
<div class="ctl"><label>View</label>
<div class="seg big" id="segView">
<button data-v="current">My current</button>
<button data-v="hd">Full HD (canonical)</button>
<button data-v="proposed" class="on">Proposed hybrid</button>
</div></div>
<div class="ctl proponly"><label>Base alpha</label>
<div class="seg" id="segBase"><button data-v="vanilla" class="on">Vanilla Colemak</button><button data-v="dh">Colemak-DH</button></div></div>
<div class="ctl proponly"><label>SYM layout</label>
<div class="seg" id="segSym"><button data-v="current" class="on">Current (mirror)</button><button data-v="v2">Programming v2</button></div></div>
<div class="ctl proponly"><label>Diff markers</label>
<div class="seg" id="segDiff"><button data-v="on" class="on">Show</button><button data-v="off">Hide</button></div></div>
</div>
<h1 id="title">Proposed hybrid — Miryoku chassis + HD engine</h1>
<p class="sub" id="viewnote"></p>
<div class="legend">
<span><i style="background:var(--alpha)"></i>alpha</span>
<span><i style="background:var(--mod)"></i>mod (hold)</span>
<span><i style="background:var(--layer)"></i>layer / switch</span>
<span><i style="background:var(--sem)"></i>semantic / editing</span>
<span><i style="background:var(--sym)"></i>symbol</span>
<span><i style="background:var(--num)"></i>number</span>
<span><i style="background:var(--mouse)"></i>mouse</span>
<span><i style="background:var(--media)"></i>media</span>
<span><i style="background:var(--rgb)"></i>RGB/cfg</span>
<span class="proponly"><i style="border:1px solid var(--new)"></i>NEW</span>
<span class="proponly"><i style="border:1px solid var(--chg)"></i>CHG</span>
<span class="proponly"><i style="border:1px solid var(--ling)"></i>+hold</span>
<span class="proponly"><i style="border:1px solid var(--media)"></i>removed</span>
</div>
<div id="app"></div>
<div id="enginewrap">
<h2 class="section">Engine features — code, not layout (✅ implemented)</h2>
<div class="engine">
<div class="ecard"><h3>✅ Adaptives — Hands Down base only</h3><p>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.</p><p class="where">ramo_adaptive.c (implemented)</p></div>
<div class="ecard"><h3>✅ Combos</h3><p>qu (P+L), z (N+H), and now th (T+H) live. More digraphs/symbols easy to add.</p><p class="where">ramo_combos.c (implemented)</p></div>
<div class="ecard"><h3>✅ Linger brackets — SYM</h3><p>Hold <code>(</code> or <code>{</code> → auto-pair, caret between: <code>(|)</code>. Tap = single. (<code>[</code> and <code>"</code> not yet — need a SYM home.)</p><p class="where">ramo_linger.c · RAMO_LINGER_TERM</p></div>
<div class="ecard"><h3>✅ Smart caps-word</h3><p>Shifts letters, keeps digits/<code>-</code>/<code>_</code>, turns space→<code>_</code>, auto-exits on punctuation.</p><p class="where">ramo_linger.c (implemented)</p></div>
</div>
</div>
<p class="foot" id="foot"></p>
<script>
let state = {view:"proposed", base:"vanilla", sym:"current", diff:"on"};
/* ============ shared static layers (identical across current & proposed) ============ */
const CMK_THUMBS=[["LT(L_MEDIA,KC_ESC)","LT(L_NAV,KC_SPC)","LT(L_MOUSE,KC_TAB)"],["LT(L_SYM,KC_ENT)","LT(L_NUM,KC_BSPC)","LT(L_FUN,KC_DEL)"]];
const HDP_LAYER={purpose:"Alt base — Hands Down Promethium (canonical arrangement)",accent:"#e0af68",how:"moutis layout · Hyper/Meh kept on fixed top positions · GACS mods · adaptives + qu/z/th + accent/symbol combos · Option-A thumbs",
top:[["KC_NO","KC_V","KC_W","HYPR_T(KC_G)","MEH_T(KC_M)","KC_J"],["KC_HASH","MEH_T(KC_DOT)","HYPR_T(KC_SLSH)","KC_DQUO","KC_QUOT","KC_NO"]],
home:[["KC_NO","LCTL_T(KC_S)","LALT_T(KC_N)","LGUI_T(KC_T)","LSFT_T(KC_H)","KC_K"],["KC_COMM","RSFT_T(KC_A)","RGUI_T(KC_E)","RALT_T(KC_I)","RCTL_T(KC_C)","KC_NO"]],
bot:[["KC_NO","KC_F","KC_P","KC_D","KC_L","KC_X"],["KC_MINS","KC_U","KC_O","KC_Y","KC_B","KC_NO"]],
thumbs:[["LT(L_MEDIA,KC_ESC)","LT(L_NAV,KC_SPC)","LT(L_MOUSE,KC_TAB)"],["LT(L_SYM,KC_R)","LT(L_NUM,KC_BSPC)","LT(L_FUN,KC_ENT)"]]};
const MEDIA_LAYER={purpose:"Media & RGB",accent:"#eb6f92",how:"hold Esc",
top:[["KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO"],["RM_TOGG","RM_NEXT","RM_HUEU","RM_SATU","RM_VALU","KC_NO"]],
home:[["KC_NO","KC_LCTL","KC_LALT","KC_LGUI","KC_LSFT","KC_NO"],["KC_NO","KC_MPRV","KC_VOLD","KC_VOLU","KC_MNXT","KC_NO"]],
bot:[["KC_NO","KC_NO","KC_NO","KC_ALGR","KC_NO","KC_NO"],["KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO"]],
thumbs:[["KC_NO","KC_NO","KC_NO"],["KC_MSTP","KC_MPLY","KC_MUTE"]]};
const MOUSE_LAYER={purpose:"Mouse move / wheel / buttons",accent:"#e0af68",how:"hold Tab (CMK) / R (HD)",
top:[["KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO"],["SCMD(KC_Z)","LCMD(KC_V)","LCMD(KC_C)","LCMD(KC_X)","LCMD(KC_Z)","KC_NO"]],
home:[["KC_NO","KC_LCTL","KC_LALT","KC_LGUI","KC_LSFT","KC_NO"],["KC_NO","MS_LEFT","MS_DOWN","MS_UP","MS_RGHT","KC_NO"]],
bot:[["KC_NO","KC_NO","KC_NO","KC_ALGR","KC_NO","KC_NO"],["KC_NO","MS_WHLL","MS_WHLD","MS_WHLU","MS_WHLR","KC_NO"]],
thumbs:[["KC_NO","KC_NO","KC_NO"],["MS_BTN2","MS_BTN1","MS_BTN3"]]};
const NUM_LAYER={purpose:"Numbers & math",accent:"#a3be8c",how:"hold Bksp (CMK) / Enter (HD)",
top:[["KC_NO","KC_LBRC","KC_7","KC_8","KC_9","KC_RBRC"],["KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO"]],
home:[["KC_NO","KC_SCLN","KC_4","KC_5","KC_6","KC_EQL"],["KC_NO","KC_RSFT","KC_RGUI","KC_RALT","KC_RCTL","KC_NO"]],
bot:[["KC_NO","KC_GRV","KC_1","KC_2","KC_3","KC_BSLS"],["KC_NO","KC_NO","KC_NO","KC_ALGR","KC_NO","KC_NO"]],
thumbs:[["KC_DOT","KC_0","KC_MINS"],["KC_NO","KC_NO","KC_NO"]]};
const FUN_LAYER={purpose:"Function keys & system",accent:"#8b90a0",how:"hold Del (CMK) / Bksp (HD)",
top:[["KC_NO","KC_F12","KC_F7","KC_F8","KC_F9","KC_PSCR"],["KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO"]],
home:[["KC_NO","KC_F11","KC_F4","KC_F5","KC_F6","KC_SCRL"],["KC_NO","KC_RSFT","KC_RGUI","KC_RALT","KC_RCTL","KC_NO"]],
bot:[["KC_NO","KC_F10","KC_F1","KC_F2","KC_F3","KC_PAUS"],["KC_NO","KC_NO","KC_NO","KC_ALGR","KC_NO","KC_NO"]],
thumbs:[["KC_APP","KC_SPC","KC_TAB"],["KC_NO","KC_NO","KC_NO"]]};
/* ============ VIEW 1: my current firmware (live code) ============ */
const CMK_V_TOP=[["KC_NO","KC_Q","KC_W","HYPR_T(KC_F)","MEH_T(KC_P)","KC_G"],["KC_J","MEH_T(KC_L)","HYPR_T(KC_U)","KC_Y","KC_QUOT","KC_NO"]];
const CMK_V_HOME=[["KC_NO","LCTL_T(KC_A)","LALT_T(KC_R)","LGUI_T(KC_S)","LSFT_T(KC_T)","KC_D"],["KC_H","RSFT_T(KC_N)","RGUI_T(KC_E)","RALT_T(KC_I)","RCTL_T(KC_O)","KC_NO"]];
const CMK_V_BOT=[["KC_NO","KC_Z","KC_X","KC_C","KC_V","KC_B"],["KC_K","KC_M","KC_COMM","KC_DOT","KC_SLSH","KC_NO"]];
const CUR_NAV={purpose:"Navigation & base-switch",accent:"#9ccfd8",how:"hold Space (CMK) / Tab (HD)",
top:[["KC_NO","KC_NO","KC_NO","TD(RAMO_TD_CMK)","TD(RAMO_TD_HD)","KC_NO"],["LCMD(KC_Z)","LCMD(KC_V)","LCMD(KC_C)","LCMD(KC_X)","SCMD(KC_Z)","KC_NO"]],
home:[["KC_NO","KC_LCTL","KC_LALT","KC_LGUI","KC_LSFT","KC_NO"],["CW_TOGG","KC_LEFT","KC_DOWN","KC_UP","KC_RGHT","KC_NO"]],
bot:[["KC_NO","KC_NO","KC_NO","KC_ALGR","KC_NO","KC_NO"],["KC_INS","KC_HOME","KC_PGDN","KC_PGUP","KC_END","KC_NO"]],
thumbs:[["KC_NO","KC_NO","KC_NO"],["KC_ENT","KC_BSPC","KC_DEL"]]};
const CUR_SYM={purpose:"Symbols",accent:"#7fd6a6",how:"hold Enter (CMK) / Space (HD)",
top:[["KC_NO","KC_LCBR","KC_AMPR","KC_ASTR","KC_LPRN","KC_RCBR"],["KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO"]],
home:[["KC_NO","KC_COLN","KC_DLR","KC_PERC","KC_CIRC","KC_PLUS"],["KC_NO","KC_RSFT","KC_RGUI","KC_RALT","KC_RCTL","KC_NO"]],
bot:[["KC_NO","KC_TILD","KC_EXLM","KC_AT","KC_HASH","KC_PIPE"],["KC_NO","KC_NO","KC_NO","KC_ALGR","KC_NO","KC_NO"]],
thumbs:[["KC_LPRN","KC_RPRN","KC_UNDS"],["KC_NO","KC_NO","KC_NO"]]};
function CURRENT(){return [
["COLEMAK",{purpose:"Base — vanilla Colemak",accent:"#7fd6a6",how:"GACS home-row mods · thumbs open the 6 layers",top:CMK_V_TOP,home:CMK_V_HOME,bot:CMK_V_BOT,thumbs:CMK_THUMBS}],
["HANDSDOWN",HDP_LAYER],["MEDIA",MEDIA_LAYER],["NAV",CUR_NAV],["MOUSE",MOUSE_LAYER],["SYM",CUR_SYM],["NUM",NUM_LAYER],["FUN",FUN_LAYER],
];}
/* ============ VIEW 2: full canonical HD (from HDPM-layers.html) ============ */
function HD(){return [
["HD",{purpose:"Base alpha — Hands Down Promethium (canonical)",accent:"#e0af68",how:"letters + HRM + thumb layer-taps",
top:[["KC_LNG1","KC_V","KC_W","KC_G","KC_M","KC_J"],["KC_HASH","KC_DOT","KC_SLSH","KC_DQUO","KC_QUOT","KC_LNG2"]],
home:[["KC_ESC","RCTL_T(KC_S)","RALT_T(KC_N)","RGUI_T(KC_T)","RSFT_T(KC_H)","LT(L_NUM,KC_K)"],["KC_COMM","RSFT_T(KC_A)","RGUI_T(KC_E)","RALT_T(KC_I)","RCTL_T(KC_C)","KC_Z"]],
bot:[["KC_TAB","LT(L_FUN,KC_F)","KC_P","KC_D","KC_L","KC_X"],["KC_MINS","KC_U","KC_O","KC_Y","KC_B","KC_Q"]],
thumbs:[["LT(L_FUN,KC_BSPC)","LT(L_NAV,KC_R)","LT(L_CFG,KC_MUTE)"],["LT(L_CFG,KC_MPLY)","LT(L_SYM,KC_SPC)","LT(L_NAV,KC_ENT)"]]}],
["SYM",{purpose:"Symbols & punctuation",accent:"#7fd6a6",how:"hold Space",
top:[["KC_TRNS","KC_Z","KC_Q","SK_PARA","SK_SCRS","SK_DCRS"],["SK_CENT","KC_DLR","SK_EURO","SK_BPND","SK_JPY","KC_TRNS"]],
home:[["KC_TRNS","SK_SECT","KC_LBRC","KC_LPRN","KC_LCBR","SK_BBLT"],["KC_PSLS","RSFT_T(KC_PEQL)","RGUI_T(KC_PAST)","RALT_T(KC_PPLS)","RCTL_T(KC_PMNS)","KC_TRNS"]],
bot:[["KC_TRNS","SK_DEGR","KC_RBRC","KC_RPRN","KC_RCBR","SK_SBLT"],["KC_CIRC","KC_PIPE","KC_AMPR","KC_LT","KC_GT","KC_TRNS"]],
thumbs:[["SK_DELWDL","SK_DELWDR","KC_VOLD"],["KC_VOLU","KC_UNDS","SK_IEXC"]]}],
["FUN",{purpose:"F-keys (left) · number row (right)",accent:"#8b90a0",how:"hold Bksp or F",
top:[["RGB_TOG","KC_F13","KC_F7","KC_F8","KC_F9","KC_F10"],["KC_SLSH","KC_7","KC_8","KC_9","KC_MINS","RGB_MOD"]],
home:[["RGB_VAI","LCTL_T(KC_F14)","LALT_T(KC_F1)","LGUI_T(KC_F2)","LSFT_T(KC_F3)","KC_F11"],["KC_ASTR","RSFT_T(KC_1)","RGUI_T(KC_2)","RALT_T(KC_3)","RCTL_T(KC_PPLS)","RGB_HUI"]],
bot:[["RGB_VAD","KC_F15","KC_F4","KC_F5","KC_F3","KC_F12"],["KC_COMM","KC_4","KC_5","KC_6","KC_EQL","RGB_HUD"]],
thumbs:[["KC_SCLN","TG(L_NUM)","KC_INS"],["KC_TRNS","KC_0","KC_DOT"]]}],
["NUM",{purpose:"Nav (left) · numpad (right)",accent:"#a3be8c",how:"hold K → right numpad; toggle for two-handed",
top:[["KC_TRNS","SK_DOCBEG","KC_HOME","KC_UP","KC_PGUP","SK_PARAPRV"],["KC_PSLS","KC_P7","KC_P8","KC_P9","KC_PMNS","KC_TRNS"]],
home:[["KC_TRNS","SK_WORDPRV","KC_LEFT","KC_DOWN","KC_RGHT","SK_WORDNXT"],["KC_PAST","RSFT_T(KC_P4)","RGUI_T(KC_P5)","RALT_T(KC_P6)","RCTL_T(KC_PPLS)","KC_TRNS"]],
bot:[["KC_TRNS","SK_DOCEND","KC_END","KC_SPC","KC_PGDN","SK_PARANXT"],["KC_PCMM","KC_P1","KC_P2","KC_P3","KC_PEQL","KC_TRNS"]],
thumbs:[["LGUI_T(KC_BSPC)","LSFT_T(KC_ENT)","SK_CLOZ"],["KC_C","KC_P0","KC_PDOT"]]}],
["NAV",{purpose:"Edit & mods (left) · arrows/nav (right)",accent:"#9ccfd8",how:"hold R or Enter",
top:[["KC_TRNS","KC_TAB","SK_CLOZ","SK_SCAP","SK_SCLP","SK_PSTM"],["SK_DOCBEG","KC_HOME","KC_UP","KC_PGUP","SK_PARAPRV","KC_TRNS"]],
home:[["KC_VOLU","KC_LCTL","KC_LALT","KC_LGUI","KC_LSFT","SK_FIND"],["SK_WORDPRV","KC_LEFT","KC_DOWN","KC_RGHT","SK_WORDNXT","KC_TRNS"]],
bot:[["KC_VOLD","SK_UNDO","SK_CUT","SK_COPY","SK_PSTE","SK_FAGN"],["SK_DOCEND","KC_END","KC_DOWN","KC_PGDN","SK_PARANXT","KC_TRNS"]],
thumbs:[["KC_BSPC","KC_ENTER","SK_ZOOMRST"],["SK_ZOOMRST","SK_HISTPRV","SK_HISTNXT"]]}],
["CFG",{purpose:"Keyboard config (left) · media (right)",accent:"#f28fad",how:"hold Mute/Play, or CFG combo",
top:[["KC_NO","SK_Lux","SK_Win","SK_Mac","QK_RBT","SK_KILL"],["SK_ZOOMRST","KC_NUM","KC_EJCT","KC_LALT","LCA(KC_DEL)","RGB_MOD"]],
home:[["RGB_VAI","HD_AdaptKeyToggle","HD_L_QWERTY","HD_L_ALPHA","KC_VOLU","KC_BRIU"],["SK_ZOOMOUT","KC_MPRV","KC_MUTE","KC_MNXT","EE_CLR","RGB_SAI"]],
bot:[["RGB_HUI","QK_BOOT","AG_SWAP","AG_NORM","KC_VOLD","KC_BRID"],["SK_ZOOMIN","KC_MRWD","KC_MPLY","KC_MFFD","QK_BOOT","RGB_HUD"]],
thumbs:[["SK_ZOOMOUT","SK_ZOOMIN","SK_ZOOMRST"],["SK_ZOOMRST","SK_ZOOMIN","SK_ZOOMOUT"]]}],
];}
/* ============ VIEW 3: proposed hybrid ============ */
const CMK_DH_TOP=[["KC_NO","KC_Q","KC_W","HYPR_T(KC_F)","MEH_T(KC_P)",["KC_B","CHG"]],["KC_J","MEH_T(KC_L)","HYPR_T(KC_U)","KC_Y","KC_QUOT","KC_NO"]];
const CMK_DH_HOME=[["KC_NO","LCTL_T(KC_A)","LALT_T(KC_R)","LGUI_T(KC_S)","LSFT_T(KC_T)",["KC_G","CHG"]],[["KC_M","CHG"],"RSFT_T(KC_N)","RGUI_T(KC_E)","RALT_T(KC_I)","RCTL_T(KC_O)","KC_NO"]];
const CMK_DH_BOT=[["KC_NO","KC_Z","KC_X","KC_C",["KC_D","CHG"],["KC_V","CHG"]],["KC_K",["KC_H","CHG"],"KC_COMM","KC_DOT","KC_SLSH","KC_NO"]];
// NAV stays unchanged in the trimmed proposal (Mac-only, arrows only) — uses CUR_NAV.
const PROP_SYM={purpose:"Symbols + linger brackets",accent:"#7fd6a6",how:"hold Enter (CMK) / Space (HD)",
top:[["KC_NO",["KC_LCBR","LING"],"KC_AMPR","KC_ASTR",["KC_LPRN","LING"],"KC_RCBR"],["KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO"]],
home:[["KC_NO","KC_COLN","KC_DLR","KC_PERC","KC_CIRC","KC_PLUS"],["KC_NO","KC_RSFT","KC_RGUI","KC_RALT","KC_RCTL","KC_NO"]],
bot:[["KC_NO","KC_TILD","KC_EXLM","KC_AT","KC_HASH","KC_PIPE"],["KC_NO","KC_NO","KC_NO","KC_ALGR","KC_NO","KC_NO"]],
thumbs:[[["KC_LPRN","LING"],"KC_RPRN","KC_UNDS"],["KC_NO","KC_NO","KC_NO"]]};
// SYM v2 — programming layer for Swift / Odin / Objective-C. Two-handed (mods dropped).
// Left = operators; Right = delimiters + quotes (openers under index, closers under middle).
// Openers (( { [ <) linger-pair on hold. High-freq on home row; digraph rolls: -> :: := == != <>.
const SYM_V2={purpose:"Programming symbols v2 (Swift · Odin · ObjC) — two-handed",accent:"#7fd6a6",
how:"hold Enter (CMK) / Space (HD) · openers ( { [ < linger-pair · optimized for -> :: := == !=",
top:[["KC_NO","KC_NO","KC_TILD","KC_AMPR","KC_PIPE","KC_CIRC"],["KC_GT",["KC_LBRC","LING"],"KC_RBRC",["KC_LT","LING"],"KC_GRV","KC_NO"]],
home:[["KC_NO","KC_EXLM","KC_ASTR","KC_PLUS","KC_MINS","KC_PERC"],["KC_EQL",["KC_LPRN","LING"],"KC_RPRN","KC_COLN","KC_DQUO","KC_NO"]],
bot:[["KC_NO","KC_AT","KC_HASH","KC_SLSH","KC_QUES","KC_DLR"],["KC_BSLS",["KC_LCBR","LING"],"KC_RCBR","KC_SCLN","KC_QUOT","KC_NO"]],
thumbs:[["KC_NO","KC_NO","KC_UNDS"],["KC_NO","KC_NO","KC_NO"]]};
function PROPOSED(){
const cmk = state.base==="dh"
? {top:CMK_DH_TOP,home:CMK_DH_HOME,bot:CMK_DH_BOT}
: {top:CMK_V_TOP,home:CMK_V_HOME,bot:CMK_V_BOT};
// Hands Down base is already live/implemented (canonical arrangement, blanked
// outer columns, Option-A thumbs) — show it as-is with an "implemented" badge.
const hdp = Object.assign({},HDP_LAYER,{badge:"✅ implemented"});
return [
["COLEMAK",{purpose:"Base — "+(state.base==="dh"?"Colemak-DH (6 keys moved)":"vanilla Colemak"),accent:"#7fd6a6",
how:"GACS home-row mods"+(state.base==="dh"?" · DH moves B/G/D/H/V/M positions only":"")+" · unchanged from today",top:cmk.top,home:cmk.home,bot:cmk.bot,thumbs:CMK_THUMBS}],
["HANDSDOWN",hdp],["MEDIA",MEDIA_LAYER],
["NAV",Object.assign({},CUR_NAV,{purpose:"Navigation & base-switch — unchanged",how:"hold Space (CMK) / Tab (HD) · same as today (you only use arrows)"})],
["MOUSE",MOUSE_LAYER],["SYM", state.sym==="v2"?SYM_V2:PROP_SYM],["NUM",NUM_LAYER],["FUN",FUN_LAYER],
];
}
/* ============ labels ============ */
const KC={SPC:"Space",BSPC:"⌫",ENT:"⏎",ENTER:"⏎",TAB:"⇥",ESC:"Esc",DEL:"⌦",INS:"Ins",APP:"▤ Menu",
HOME:"Home",END:"End",PGUP:"PgUp",PGDN:"PgDn",UP:"↑",DOWN:"↓",LEFT:"←",RGHT:"→",RIGHT:"→",
MUTE:"🔇",VOLU:"Vol+",VOLD:"Vol",MPLY:"⏯",MPRV:"⏮",MNXT:"⏭",MSTP:"⏹",MRWD:"⏪",MFFD:"⏩",
PSCR:"PrtSc",SCRL:"ScrLk",PAUS:"Pause",ALGR:"AltGr",EJCT:"⏏",NUM:"NumLk",BRIU:"☀+",BRID:"☀−",
LNG1:"英/かな",LNG2:"かな",T:"T",C:"C",
DLR:"$",HASH:"#",SLSH:"/",DQUO:'"',QUOT:"'",COMM:",",DOT:".",MINS:"",UNDS:"_",EQL:"=",
CIRC:"^",PIPE:"|",AMPR:"&",LT:"<",GT:">",LBRC:"[",RBRC:"]",LPRN:"(",RPRN:")",LCBR:"{",RCBR:"}",
ASTR:"*",PLUS:"+",SCLN:";",COLN:":",TILD:"~",GRV:"`",BSLS:"\\",EXLM:"!",QUES:"?",AT:"@",PERC:"%",
PSLS:"/",PAST:"*",PMNS:"",PPLS:"+",PEQL:"=",PDOT:".",PCMM:",",
P0:"0",P1:"1",P2:"2",P3:"3",P4:"4",P5:"5",P6:"6",P7:"7",P8:"8",P9:"9",
LCTL:"⌃",LALT:"⌥",LGUI:"⌘",LSFT:"⇧",RCTL:"⌃",RALT:"⌥",RGUI:"⌘",RSFT:"⇧",
F1:"F1",F2:"F2",F3:"F3",F4:"F4",F5:"F5",F6:"F6",F7:"F7",F8:"F8",F9:"F9",F10:"F10",F11:"F11",F12:"F12",F13:"F13",F14:"F14",F15:"F15",
"1":"1","2":"2","3":"3","4":"4","5":"5","6":"6","7":"7","8":"8","9":"9","0":"0"};
const SK={UNDO:"Undo",REDO:"Redo",CUT:"Cut",COPY:"Copy",PSTE:"Paste",PSTM:"Paste=",SALL:"Sel All",SWRD:"Sel Wd",
FIND:"Find",FAGN:"Find→",NEW:"New",OPEN:"Open",CLOZ:"Close",QUIT:"Quit",
DOCBEG:"Doc Top",DOCEND:"Doc End",WORDPRV:"Word◀",WORDNXT:"Word▶",LINEBEG:"Ln Beg",LINEEND:"Ln End",
PARAPRV:"¶ ◀",PARANXT:"¶ ▶",HISTPRV:"◀ Back",HISTNXT:"Fwd ▶",ZOOMIN:"Zoom+",ZOOMOUT:"Zoom",ZOOMRST:"Zoom·0",
PARA:"¶",SCRS:"†",DCRS:"‡",CENT:"¢",EURO:"€",BPND:"£",JPY:"¥",SECT:"§",IEXC:"¡",IQUE:"¿",BBLT:"•",SBLT:"·",DEGR:"°",
DELWDL:"Del Wd◀",DELWDR:"Del Wd▶",KILL:"Force-Quit",SCAP:"Shot▸file",SCLP:"Shot▸clip",SRCH:"Search",HENT:"Hard⏎",
Mac:"OS·Mac",Win:"OS·Win",Lux:"OS·Linux",MHEN:"英数",HENK:"かな",DKT8:"Dictate",AIVC:"AI/Siri"};
const MODSYM={SFT:"⇧",CTL:"⌃",ALT:"⌥",GUI:"⌘",HYPR:"✦",MEH:"◆"};
const MS={LEFT:"Ms ←",DOWN:"Ms ↓",UP:"Ms ↑",RGHT:"Ms →",WHLL:"Wh ←",WHLD:"Wh ↓",WHLU:"Wh ↑",WHLR:"Wh →",BTN1:"Btn L",BTN2:"Btn R",BTN3:"Btn M"};
const RM={TOGG:"RGB ⏻",NEXT:"RGB ▸",HUEU:"hue+",SATU:"sat+",VALU:"bright+"};
const RGBOLD={TOG:"RGB ⏻",MOD:"RGB ▸",VAI:"RGB ☀+",VAD:"RGB ☀−",HUI:"hue+",HUD:"hue",SAI:"sat+",SAD:"sat"};
const LAYERNAME={L_MEDIA:"MEDIA",L_NAV:"NAV",L_MOUSE:"MOUSE",L_SYM:"SYM",L_NUM:"NUM",L_FUN:"FUN",L_CFG:"CFG",L_HD:"HD"};
const EXACT={
"TD(RAMO_TD_CMK)":{m:"⇄ Colemak",h:"2×tap",cls:"layer"}, "TD(RAMO_TD_HD)":{m:"⇄ HandsDn",h:"2×tap",cls:"layer"},
"LCMD(KC_Z)":{m:"Undo",h:"⌘Z",cls:"sem"}, "LCMD(KC_V)":{m:"Paste",h:"⌘V",cls:"sem"},
"LCMD(KC_C)":{m:"Copy",h:"⌘C",cls:"sem"}, "LCMD(KC_X)":{m:"Cut",h:"⌘X",cls:"sem"},
"SCMD(KC_Z)":{m:"Redo",h:"⇧⌘Z",cls:"sem"}, "CW_TOGG":{m:"Caps Word",cls:"sem"},
"HD_AdaptKeyToggle":{m:"Adaptive",h:"on/off",cls:"layer"}, "HD_L_QWERTY":{m:"▸QWERTY",cls:"layer"}, "HD_L_ALPHA":{m:"▸HD",cls:"layer"},
"QK_BOOT":{m:"Bootloader",cls:"rgb"}, "QK_RBT":{m:"Reboot",cls:"rgb"}, "EE_CLR":{m:"Clr EEPROM",cls:"rgb"},
"AG_SWAP":{m:"⌥/⌘ swap",cls:"rgb"}, "AG_NORM":{m:"⌥/⌘ norm",cls:"rgb"}, "LCA(KC_DEL)":{m:"⌃⌥⌦",cls:"sym"},
"KC_NO":{m:"∅",cls:"none"}, "KC_TRNS":{m:"▽",cls:"trns"} };
function fmt(kc){
if(EXACT[kc]){const e=EXACT[kc];return {m:e.m,h:e.h||"",cls:e.cls||"base"};}
let mt=kc.match(/^[LR](SFT|CTL|ALT|GUI|HYPR|MEH)_T\((.+)\)$/);
if(mt){return {m:fmt(mt[2]).m,h:MODSYM[mt[1]],cls:"mod"};}
let lt=kc.match(/^LT\((\w+),\s*(.+)\)$/);
if(lt){return {m:fmt(lt[2]).m,h:"▸"+(LAYERNAME[lt[1]]||lt[1].replace("L_","")),cls:"layer"};}
let mo=kc.match(/^(MO|TG|TO|TT|DF)\((\w+)\)$/);
if(mo){return {m:"▸"+(LAYERNAME[mo[2]]||mo[2].replace("L_","")),h:mo[1].toLowerCase(),cls:"layer"};}
let sk=kc.match(/^SK_(\w+)$/); if(sk){return {m:SK[sk[1]]||sk[1],h:"",cls:"sem"};}
let ms=kc.match(/^MS_(\w+)$/); if(ms){return {m:MS[ms[1]]||ms[1],h:"",cls:"mouse"};}
let rm=kc.match(/^RM_(\w+)$/); if(rm){return {m:RM[rm[1]]||("RGB "+rm[1]),h:"",cls:"rgb"};}
let rg=kc.match(/^RGB_(\w+)$/); if(rg){return {m:RGBOLD[rg[1]]||("RGB "+rg[1]),h:"",cls:"rgb"};}
let k=kc.match(/^KC_(\w+)$/);
if(k){let m=KC[k[1]]!==undefined?KC[k[1]]:k[1];return {m:m,h:"",cls:classify(kc,m)};}
return {m:kc,h:"",cls:"base"};
}
function classify(kc){
if(/VOL|MPLY|MPRV|MNXT|MSTP|MRWD|MFFD|MUTE/.test(kc))return "media";
if(/KC_(DLR|HASH|SLSH|DQUO|QUOT|COMM|DOT|MINS|UNDS|EQL|CIRC|PIPE|AMPR|LT|GT|LBRC|RBRC|LPRN|RPRN|LCBR|RCBR|ASTR|PLUS|SCLN|COLN|TILD|GRV|BSLS|EXLM|QUES|AT|PERC|PSLS|PAST|PMNS|PPLS|PEQL|PDOT|PCMM)$/.test(kc))return "sym";
if(/KC_(P?\d|F\d)/.test(kc))return "num";
if(/KC_[LR](CTL|ALT|GUI|SFT)$/.test(kc))return "mod";
return "base";
}
function keyEl(cell){
let kc=cell,diff="";
if(Array.isArray(cell)){kc=cell[0];diff=cell[1]||"";}
const f=fmt(kc);
const d=document.createElement("div");
d.className="key "+(f.cls||"base")+(diff?(" "+diff):"");
d.title=kc+(diff?(" ["+diff+"]"):"");
const tag = diff==="LING"?"+hold":diff==="REM"?"remove":diff;
d.innerHTML=(f.h?`<span class="h">${f.h}</span>`:"")+`<span class="m">${f.m}</span>`+(diff?`<span class="tag">${tag}</span>`:"");
return d;
}
function half(rows,thumbs,side){
const h=document.createElement("div");h.className="half";
rows.forEach(r=>r.forEach(c=>h.appendChild(keyEl(c))));
const t=document.createElement("div");t.className="thumbs";
const cells = side==="L" ? ["","","",...thumbs] : [...thumbs,"","",""];
cells.forEach(c=>{ if(c===""){const s=document.createElement("div");s.style.visibility="hidden";t.appendChild(s);} else t.appendChild(keyEl(c)); });
h.appendChild(t); return h;
}
function card(name,d){
const sec=document.createElement("div");sec.className="layer";
const badge=d.badge?`<span class="badge">${d.badge}</span>`:"";
sec.innerHTML=`<h2><b style="background:${d.accent||'#9ccfd8'}">${name}</b> ${d.purpose} ${badge}</h2><p class="how">reach: ${d.how}</p>`;
const board=document.createElement("div");board.className="board";
board.appendChild(half([d.top[0],d.home[0],d.bot[0]],d.thumbs[0],"L"));
board.appendChild(half([d.top[1],d.home[1],d.bot[1]],d.thumbs[1],"R"));
sec.appendChild(board);return sec;
}
const NOTES={
current:"<b>My current code (live)</b> — what's in the tree now, including the HD-hybrid work: full PM adaptives (HDP base only), qu/z/th combos, linger brackets, smart caps-word, Option-A thumbs, blanked HD outer columns. SYM shown is the current mirror (v2 is still a proposal).",
hd:"<b>Full canonical Hands Down Promethium</b> (moutis) for reference — its own 6 layers + Semantic-Keys engine. This is the 'ideal' we're borrowing ideas from, not something to adopt wholesale.",
proposed:"<b>Proposal layer</b> — most of this is now <b>IMPLEMENTED</b> (adaptives, qu/z/th combos, linger, smart caps-word, Option-A thumbs, blanked HD columns). Still open proposals: <b>SYM Programming v2</b> and <b>Colemak-DH</b> (toggles above). Diff marks = change vs the old baseline."
};
const TITLES={current:"My current layout (as flashed)",hd:"Full Hands Down Promethium (canonical, for reference)",proposed:"Proposed hybrid — Miryoku chassis + HD engine"};
function render(){
document.getElementById("controls").classList.toggle("prop", state.view==="proposed");
document.getElementById("enginewrap").style.display = state.view==="proposed" ? "" : "none";
document.body.classList.toggle("diff", state.view==="proposed" && state.diff!=="off");
document.getElementById("title").textContent=TITLES[state.view];
document.getElementById("viewnote").innerHTML=NOTES[state.view];
const foot=document.getElementById("foot");
foot.innerHTML = state.view==="proposed"
? "Implemented: adaptives (HDP-only), qu/z/th combos, linger ( { , smart caps-word, Option-A thumbs, blanked HD outer columns. Still proposals: SYM Programming v2 · Colemak-DH. Linger [ and \" need a SYM home first."
: state.view==="hd"
? "Only the 3 Corne thumbs are drawn; canonical HD defines up to 6 (RH3/RH4/RH5 omitted). Purple = Semantic Keys."
: "Outer pinky columns are mostly ∅ — you run a 34/36-key layout on a 42-key board.";
const app=document.getElementById("app");app.innerHTML="";
const set = state.view==="current"?CURRENT() : state.view==="hd"?HD() : PROPOSED();
set.forEach(([n,d])=>app.appendChild(card(n,d)));
}
function wire(id,key){
document.getElementById(id).addEventListener("click",e=>{
if(e.target.tagName!=="BUTTON")return;
[...e.currentTarget.children].forEach(b=>b.classList.remove("on"));
e.target.classList.add("on"); state[key]=e.target.dataset.v; render();
});
}
wire("segView","view"); wire("segBase","base"); wire("segSym","sym"); wire("segDiff","diff");
render();
</script>
</body>
</html>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
*/

View file

@ -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;

View file

@ -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"

View file

@ -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

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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,

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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
<!-- layout:begin -->
<!-- layout:end -->
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 = "<!-- layout:begin -->"
END = "<!-- layout:end -->"
GENERATED = ("<!-- Generated by qmk_userspace/users/raphaelmor/tools/kle_docs.py. "
"Do not hand-edit this section; edit around it. -->")
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.
<!-- Write whatever you like here. Only the block below is generated. -->
"""
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())

File diff suppressed because it is too large Load diff

View file

@ -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"),
]

View file

@ -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 <text> 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 = """ <style type='text/css'>
.keycap .border { stroke: black; stroke-width: 2; }
.keycap .inner.border { stroke: rgba(0,0,0,.1); }
</style>
<defs>
<linearGradient id="DCS">
<stop offset="0%" stop-color="black" stop-opacity="0"/>
<stop offset="40%" stop-color="black" stop-opacity="0.1"/>
<stop offset="60%" stop-color="black" stop-opacity="0.1"/>
<stop offset="100%" stop-color="black" stop-opacity="0"/>
</linearGradient>
<radialGradient id="DSA">
<stop offset="0%" stop-color="black" stop-opacity="0.1"/>
<stop offset="10%" stop-color="black" stop-opacity="0.1"/>
<stop offset="100%" stop-color="black" stop-opacity="0"/>
</radialGradient>
<radialGradient id="SA" xlink:href="#DSA" />
</defs>"""
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'<text x="{x:.1f}" y="{top + i * step:.1f}" font-size="{size:.1f}" '
f'fill="{color}" text-anchor="{anchor}">{escape(line)}</text>'
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"<g class='{classes}'>"]
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' <rect {shell} class="outer border"/>',
f' <rect {shell}/>']
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' <rect {top} class="inner border"/>',
f' <rect {top}/>']
# 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("</g>")
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"<svg width='{width:g}px' height='{height:g}px' "
f"viewBox='0 0 {width:g} {height:g}' "
f"xmlns='http://www.w3.org/2000/svg' "
f'xmlns:xlink="http://www.w3.org/1999/xlink" '
f"font-family='Helvetica Neue, Helvetica, Arial, sans-serif'>",
STYLE,
f" <g transform='translate({OUTER_PAD},{OUTER_PAD})'>",
f' <rect width="{plate_w:g}" height="{plate_h:g}" stroke="#ddd" '
f'stroke-width="1" fill="{meta.get("backcolor", "#eeeeee")}" rx="6"/>',
f" <g transform='translate({INNER_PAD},{INNER_PAD})'>",
]
for key in keys:
parts += [" " + line for line in keycap(key)]
parts += [" </g>", " </g>", "</svg>"]
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())