Skip to content

refactor(qmk): multi-target (arsenik/selenium) structure with shared code#101

Open
severindupouy wants to merge 16 commits intoOneDeadKey:mainfrom
severindupouy:qmk-restructure
Open

refactor(qmk): multi-target (arsenik/selenium) structure with shared code#101
severindupouy wants to merge 16 commits intoOneDeadKey:mainfrom
severindupouy:qmk-restructure

Conversation

@severindupouy
Copy link
Contributor

@severindupouy severindupouy commented Mar 12, 2026

Functional changes

Multi-target keymap architecture

Restructure qmk/ to support multiple keymap targets sharing common code:

qmk/
  shared/       — keycodes, layout macros, host layout definitions
  arsenik/      — arsenik keymap (existing, reorganized)
  selenium/     — selenium keymap (new)
  generator.sh  — generates and installs keymaps
  • Extract reusable code (keycodes, layout macros, host layouts) into shared/
  • Each target has its own customization.h, config.h, keymap.c, and rules.mk
  • Rename ARSENIK_LAYOUTONEDEADKEY_LAYOUT (neutral prefix for shared use)
  • Rename _lafayette layer → _symbols for consistency across targets

New generator

Replace arsenik-qmk.sh with generator.sh.

Previous behavior (arsenik-qmk.sh):

  • Copied the single keymap/ folder into QMK
  • Hardcoded to produce an arsenik keymap only
  • Expected QMK firmware at ~/qmk_firmware (or $QMK_PATH)
  • Required passing the keyboard name as a positional argument
  • Opened $EDITOR on config.h after install

New behavior (generator.sh):

  • -target <name> flag selects which keymap to generate (arsenik, selenium, or any future target)
  • Merges shared/ files with the target-specific files into a flat output directory
  • Flattens ../shared/ include paths so the output is self-contained
  • Output folder name matches the target (keymaps/arsenik/ or keymaps/selenium/), allowing both to coexist in QMK
  • Leverages QMK's built-in configuration (qmk config user.keyboard, user.keymap, user.qmk_home) instead of relying on hardcoded paths or environment variables. This means the generator works out of the box for anyone who has already set up QMK, with no extra configuration needed. Values can still be overridden via -kb and -km flags.
  • Detects the keyboard layout automatically from the reference keymap configured in user.keymap (typically default)
./generator.sh -target arsenik -g -cp   # generate + install arsenik
./generator.sh -target selenium -g -cp  # generate + install selenium

Selenium keymap

QMK implementation of Selenium, based on the ZMK reference implementation. Not all features are supported.

Implementation details and design decisions are documented in selenium/IMPLEMENTATION.md.

ZMK → QMK conversion details

QMK and ZMK don't offer the same features. Here's how we handled the differences.

Timing (all 3 ZMK values preserved exactly):

Parameter ZMK value QMK mapping
SHORT_TAPPING_TERM 150ms TAPPING_TERM = 150 (global default)
HRM_TAPPING_TERM 300ms get_tapping_term() returns 300ms for text-producing keys
QUICK_TAP 200ms get_quick_tap_term() returns 200ms via QUICK_TAP_TERM_PER_KEY

QMK only has a single global TAPPING_TERM, so we use TAPPING_TERM_PER_KEY to differentiate between hold-preferred keys (150ms) and tap-preferred keys like HRM and spacebar (300ms). For QUICK_TAP, QMK's action_tapping.h unconditionally redefines QUICK_TAP_TERM = TAPPING_TERM unless QUICK_TAP_TERM_PER_KEY is defined — we use that workaround to set our own 200ms value.

Hold-tap behavior:

A single helper function tap_keycode_used_in_text() drives both timing and hold behavior, cleanly replicating ZMK's per-behavior split:

  • ZMK sc (hold-preferred) → get_hold_on_other_key_press() returns true for non-text keys
  • ZMK lt (tap-preferred) → QMK default behavior for text-producing keys
  • This naturally maps ZMK's two hold-tap flavors onto QMK's per-key callbacks

ZMK feature mapping:

ZMK behavior QMK Notes
EZ_SK(LSHIFT) (sticky key) OSM(MOD_LSFT) Equivalent: one-shot on tap, continuous modifier on hold
EZ_SL (hold=momentary, tap=one-shot) OSL() Equivalent: QMK's OSL() provides both behaviors
&sl { ignore-modifiers; } Default QMK behavior Equivalent: QMK's OSL natively preserves modifiers when chaining OSM→OSL
&sk { quick-release; } Similar QMK default Negligible timing difference (QMK releases on key release, ZMK on key press)
&lt LAYER LS(SPACE) Not implemented Base layer space shares the same LT() keycode as NumLock/NumNav space, making layer-specific interception impossible
sym_shift_altgr (shift→AltGr morph) OSL(_symbols) Not implemented — no clean QMK equivalent
magic_backspace / magic_space (mod-morphs) Not implemented No clean QMK equivalent (position-specific mod-morphs don't exist in QMK)

QMK 0.32.x compatibility

  • Remove sendstring_*.h includes that caused compilation failures for non-QWERTY host layouts (#5139, #16826, #25696)
  • Update mouse wheel keycodes KC_WH_*MS_WHL* (renamed in QMK 0.32)
  • Fix BEPO keycode prefix BE_BP_ (renamed in QMK 0.32)

Tooling & testing

Compile test framework (qmk/tests/)

Verifies that every valid config option combination compiles successfully:

  • Per-option tests: toggle one config option at a time (28 tests)
  • Exhaustive tests: driven by human-readable matrix files (36 arsenik + 40 selenium combinations)
  • Adding new test cases requires editing a matrix file, no code changes

Documentation: qmk/tests/README.md

Keymap layout linter (qmk/tools/)

Custom Python formatter to make pretty the blocks in keymap.c files:

  • Enforces consistent column alignment within and across layers
  • Two modes: --compact (per-layer alignment) and --wide (global alignment across all layers)
  • Recovers from C formatter damage (parses arbitrarily reflowed tokens back into correct shape)
  • Check, diff, and fix modes with colored output

// clang-format off/on markers protect layout blocks from generic C formatters. A .clang-format config handles the non-layout C code.

Documentation: qmk/tools/README.md

IDE integration

  • .zed/tasks.json and .vscode/tasks.json: keymap lint tasks (check, fix, fix wide)
  • qmk/.clang-format: formatting rules for non-layout C code

CI/CD

  • qmk/.env: pinned QMK version (0.32.3) and default keyboard
  • qmk/Dockerfile: reproducible build environment based on qmkfm/qmk_cli
  • .github/workflows/qmk-test.yml:
    • On PRs: per-option compile tests (fast feedback)
    • On merge to main: exhaustive compile tests (full coverage)

Test plan

  • ./generator.sh -target arsenik -g generates a valid keymap
  • ./generator.sh -target selenium -g generates a valid keymap
  • qmk compile -kb <keyboard> -km arsenik compiles successfully
  • qmk compile -kb <keyboard> -km selenium compiles successfully
  • All per-option tests pass (28/28)
  • All exhaustive tests pass (76/76)
  • CI green on all 4 jobs
  • Selenium keymap tested on beekeeb/piantor hardware
  • Arsenik keymap behavior is unchanged from current main

Prepare for multi-target structure by moving all keymap files
into a dedicated arsenik/ directory.
Extract reusable code into shared/:
- shared/keycodes.h: generic shorthands, host layout declarations, ODK sequences, shifted numbers
- shared/layouts.h: ARSENIK_LAYOUT macros for all supported keyboards
- shared/keymap_ergol.h: moved from arsenik/

Arsenik-specific logic (LAFAYETTE, HRM, thumb configs) stays in
arsenik/customization.h, which includes the shared headers.

Rename arsenik_config.h to config.h for consistency.
The _lafayette and _symbols layers serve the same purpose (programming
symbols). Use the more generic _symbols name for consistency across
arsenik and selenium targets.
Use a neutral ONEDEADKEY_ prefix for the layout macro, since it is
shared across multiple keymap targets (arsenik, selenium, etc.).
@severindupouy
Copy link
Contributor Author

Dans l'idéal, il faudrait tester toutes les différentes possibilités de customisation d'arsenik et de selenium (les différentes flavor, etc) et vérifier qu'on génère bien une keymap valide que QMK arrive à compiler.

Je ne suis pas sûr de pouvoir prendre le temps de faire toutes ces vérifications.
Je n'ai testé que pour le cas par défaut pour mon piantor.

En terme d'organisation des repos, on pourrait organiser les choses différemment (générer une keymap selenium dans le dépôt qui s'appelle arsenik n'est pas hyper cohérent, mais pour l'instant c'est comme ça).

@Nuclear-Squid @fabi1cazenave qu'en pensez-vous ?

@Nuclear-Squid
Copy link
Collaborator

Merci pour ta contribution ! Y’a beaucoup de choses à regarder, je vais tâcher de regarder ça dans le weekend. J’aimerai prendre le temps d’avoir une vue holistique de ton travail avant de faire une revue ^^

@severindupouy
Copy link
Contributor Author

N'hésites pas si tu as des questions.

@severindupouy severindupouy force-pushed the qmk-restructure branch 4 times, most recently from b9b9bf8 to 7fbd50e Compare March 14, 2026 13:21
@severindupouy
Copy link
Contributor Author

J'ai ajouté :

  • une CI
  • des tests qui sont joués en CI
    • tester toutes les combinaisons de variantes de customization de chaque keymap
    • vérifier que qmk compile bien le firmware
    • pour arsenik et pour selenium
    • le keymap de base utilisé est beekeeb/piantor, mais ça pourrait être un autre, il en faut juste un avec 2*3 pouces
  • les tests ont remontés des bugs, que j'ai fixés
    • des keycodes qui n'étaient plus compatible avec la version la + récente de QMK
    • un bug connu de QMK, on faisait un import d'une fonction bugguée qu'en fait on n'utilise pas

Si la partie tests / CI te sembe too much, tu me dis.

Comme le repo /OneDeadKey/arsenik n'est pas configuré pour run cette CI, j'ai créé une PR jumelle sur mon fork, histoire de vérifier que cette CI passe bien. Tu peux check ici : severindupouy#4

Une fois validé, avant de merger, il faudra drop le dernier commit ci(qmk): temporarily trigger CI on qmk-restructure branch

Et si tu veux qu'on envisage de ne pas merger et mettre ça dans un autre repo, on peut faire ça.

@severindupouy
Copy link
Contributor Author

Selenium is a keymap converted from the ZMK Selenium project
(zmk-keyboard-quacken). It features configurable hold-tap behavior
(HT_NONE, HT_THUMB_TAPS, HT_HOME_ROW_MODS, HT_TWO_THUMB_KEYS),
VIM_NAVIGATION, HRM_SHIFT, and LEFT_HAND_SPACE options.

Uses the shared/ headers for keycodes, layouts, and keymap_ergol.h.
Replace arsenik-qmk.sh with generator.sh supporting multiple keymap
targets via the -target flag (arsenik, selenium, etc.).

The generator copies shared/ files and target-specific files into a
flat output directory, flattening include paths and replacing the
layout placeholder automatically.

Update and rename readme.md to README.md.
The sendstring_*.h headers redefine ascii_to_keycode_lut and
ascii_to_dead_lut lookup tables, which conflict with the same tables
in QMK's send_string.c. This causes compilation failures for AZERTY,
BEPO, DVORAK, COLEMAK, and WORKMAN host layouts.

These includes are only needed when using send_string() with
layout-specific characters. Arsenik does not use send_string(),
so removing them is safe and fixes all affected host layouts.

Related QMK issues:
- qmk/qmk_firmware#5139
- qmk/qmk_firmware#16826
- qmk/qmk_firmware#25696
- Rename mouse wheel keycodes KC_WH_* to MS_WHL* (renamed in QMK 0.32)
- Fix BEPO keycode prefix from BE_ to BP_ (renamed in QMK 0.32)
Add a test framework that verifies all config option combinations
compile successfully for each keymap target.

- tests/common.sh: shared functions (generate, patch, compile)
- tests/test_per_option.sh: toggle one option at a time
- tests/test_exhaustive.sh: reads matrix files for all combinations
- tests/matrix_arsenik.txt: 36 arsenik combinations
- tests/matrix_selenium.txt: 40 selenium combinations
- tests/run_all.sh: runs per-option then exhaustive
- qmk/.env: pin QMK_VERSION=0.32.3 and QMK_KEYBOARD=beekeeb/piantor
- qmk/Dockerfile: QMK build environment based on qmkfm/qmk_cli
- .github/workflows/qmk-test.yml: CI pipeline
  - Per-option compile tests on PRs (fast gate)
  - Exhaustive compile tests on main merges only
  - Both arsenik and selenium targets run in parallel
Explain how to run compile tests locally, in Docker, and in CI.
Document the matrix file format for adding new test combinations.
Python tool that checks and fixes formatting of ONEDEADKEY_LAYOUT()
layer blocks in keymap.c files. Supports compact (per-layer) and
wide (cross-layer) column alignment modes.
- .clang-format: C formatting rules for non-layout code
- .zed/tasks.json: keymap lint tasks for Zed
- .zed/settings.json: C language settings, format_on_save off
- .vscode/tasks.json: keymap lint tasks for VS Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants