Skip to content

YiftahCooper/Moonlander-Custom-Config

Repository files navigation

Hey! This repo combines my ZSA Moonlander's online Oryx layout with custom QMK firmware, a custom Windhawk mod, and CopyQ text tools. The Oryx web configurator is limited so I supplemented it with custom code. The additions:

  • A 12-note chromatic MIDI piano layer
  • Hebrew/English language-aware RGB: English keeps Oryx's green base colour; Hebrew substitutes the brown used by Layer 2's black MIDI keys while preserving individually coloured keys (requires the accompanying Windhawk mod)
  • Smart Title Case and case switching functionality (requires the accompanying CopyQ commands)
  • A language correction feature that transplants Hebrew characters to their physical English QWERTY equivalents, and vice versa (requires CopyQ). This fixes the problem of typing an entire sentence with the wrong input language active.
  • Some custom keys and functions; for example, quickly double tapping the right space key outputs ". " just like on my mobile phone

The beauty of this setup is that it uses CI to build everything automatically, merging Oryx and my custom code cleanly and outputting a compiled .bin file to flash. Full credit to poulainpi for that functionality: https://github.com/poulainpi/oryx-with-custom-qmk

Open https://configure.zsa.io/moonlander/layouts/3aMQz/latest/0 to see the base layout — most custom keys have peach-colored labels that mark function-key placeholders which get replaced with real behavior by the patch script after the firmware builds.

The rest of this README is AI-generated technical documentation that explains each feature in depth. You can contact me at me@yiftah.com with questions or feedback.

Custom QMK Firmware for ZSA Moonlander

Advanced MIDI keyboard, language-aware RGB, and Windows text automation — managed through Oryx with zero-merge-conflict CI/CD injection.

Overview

This repository provides custom QMK firmware for the ZSA Moonlander keyboard, combining:

  • Layer-0 Keymap: Modified QWERTY with dual-function thumb keys, 3 tap-dances, and F-key placeholders for Windows text automation
  • Language-Aware RGB: Hebrew/English indicator with Windows sync
  • CopyQ Text Tools: Clipboard-protected Smart Title Case, case cycling, and Hebrew/English transplantation
  • Windhawk Companion: Windows language switching and Hebrew/English RGB synchronization
  • CI/CD Pipeline: Automated Oryx → patch → build → release workflow
  • MIDI Engine (Layer 2): A 26-note polyphonic MIDI controller featuring independent melody/bass splits and a dynamic thumb-controlled transpose shifter

The key innovation: Oryx (ZSA's online layout editor) has no native MIDI support. Instead of manually merging generated keymap.c files, CI downloads fresh Oryx source, applies deterministic custom patches, builds in Docker, publishes a verified .bin, and then synchronizes the patched 3aMQz/ snapshot back to the repository. Custom logic remains separate from generated source, while the generated snapshot stays available for inspection and rollback.

F-key Availability Quick Reference

Use this table at a glance when adding a new function:

F-key Status Notes
F14, F15, F16, F17 Free No standard OS/browser shortcuts
F20, F21, F23, F24 Free Not emitted by the current firmware or assigned to a host tool; F24 appears only in legacy migration detection
F1–F12 Reserved Layer 1 function row (also all have standard OS/browser shortcuts for refresh, search, fullscreen, etc.)
F18 Reserved Language switch — wired to the left-thumb tap-dance key (k40) and Windhawk
F13 Reserved CopyQ Smart Title Case — right-space triple tap
F19 Reserved CopyQ upper/lower toggle — left-space triple tap
F22 Reserved CopyQ Hebrew/English transplantation — language-key double tap

The 8 free keys above (F14F17, F20, F21, F23, F24) are neither mapped in the current firmware nor assigned to these host tools.

Project Structure

Working-Oryx-QMK-Sync/
├── custom_qmk/               ← Canonical custom firmware (custom_code.c)
│   └── custom_code.c         ← MIDI bass shifter and language RGB overlay
├── scripts/                  ← Python patching engine
│   └── patch_keymap.py       ← 11+ deterministic transformations injected into Oryx source
├── host_tools/copyq/         ← CopyQ transformations, protected transaction, installer, tests
├── host_tools/reselect/      ← .NET 10 grapheme-aware reselection helper
├── host_tools/windhawk/      ← Windows Windhawk mod (v2.0.1)
│   ├── moonlander_language_sync.wh.cpp  ← F18 language switching + RGB sync only
│   └── deprecated/           ← Preserved v1.2.8 F19/F22 rollback source; do not delete on merge
├── .github/workflows/        ← CI: fetch Oryx → patch → build → release
│   └── fetch-and-build-layout.yml
├── 3aMQz/                    ← Auto-synced patched layout snapshot (committed by workflow)
│   ├── keymap.c              ← Oryx-generated + patched MIDI layer
│   ├── config.h              ← MIDI_ADVANCED, low-latency settings
│   └── rules.mk              ← MIDI_ENABLE = yes
├── Dockerfile                ← Dated Debian arm-none-eabi QMK build container
└── qmk_firmware/             ← Ignored runtime-only ZSA QMK clone for local builds

Features

1. Base Layer Keymap (Layer 0)

A modified QWERTY layout with dual-function thumb keys (managed in Oryx, snapshot at 3aMQz/keymap.c). F18 is owned by Windhawk; F13/F19/F22 selected-text actions are owned by CopyQ.

Row 0 (k00–k0d, 14 keys)

Left half Right half
ESC 1 2 3 4 5 = MEH_T(PageUp) 6 7 8 9 0 Home
  • MEH_T: Hold activates MeH (Alt+Ctrl+Shift), tap sends the key.

Row 1 (k10–k1d, 14 keys)

Left half Right half
DUAL_FUNC_0 Q W E R T - ALL_T(PgDn) Y U I O P End
  • ALL_T: Hold activates ALL (Ctrl+Alt+Shift+Gui), tap sends the key.
  • DUAL_FUNC_0 (custom override): tap → DELETE, hold → CTRL+DELETE

Row 2 (k20–k2d, 14 keys)

Left half Right half
DUAL_FUNC_1 A S D F G ` TG(1) H J K L ; \
  • TG(1): Momentary toggle to Layer 1.
  • DUAL_FUNC_1 (custom override): tap → BACKSPACE, hold → CTRL+BACKSPACE

Row 3 (k30–k3b, 12 keys)

Left half Right half
Shift Z X C V B N M , . Up OSL(1)
  • OSL(1): One-shot Layer 1 (next keypress only).

Row 4 (k40–k4b, 12 keys)

Left half Right half
DANCE_0 Gui Alt [ ] MT(RAlt,Tab) DUAL_FUNC_2 ' /
  • MT(RAlt, Tab): Hold Right Alt, tap Tab.
  • DANCE_0 (language key): single tap → F18, hold → LeftCtrl, second press → immediate F22 transplantation
  • DUAL_FUNC_2 (custom override): tap → ENTER, hold → SHIFT+ENTER

Row 5 — Thumb Cluster (k50–k55, 6 keys)

Left cluster Right cluster
DANCE_1 MT(RCtrl,NO) MT(Shift+Ctrl,NO) Delete Backspace DANCE_2
  • MT(RCtrl, NO): Hold = Right Ctrl; taps do nothing.
  • MT(Shift+Ctrl, NO): Hold = Left Shift + Left Ctrl; taps do nothing.
  • DANCE_1 (left space/caps): single tap → SPACE, hold → LEFT_SHIFT, double tap → CAPS LOCK, third press → immediate F19
  • DANCE_2 (right space/numdot): single tap → SPACE, hold → SPACE, double tap → KP_DOT + SPACE, third press → immediate F13

Once a terminal text-tool action fires, surplus taps in the same tap-dance window are ignored. The internal timeout closes the gesture without emitting a delayed duplicate or turning an extra tap into Space, Caps Lock, period-space, or F18. A held terminal press still fires, matching normal key-press behavior. Oryx normally generates “repeat the ordinary tap three times” callbacks because its generic tap-dance exporter has no custom semantic for count 3; the patcher replaces that fallback for the language dance and both space dances.

Per-Key Tapping Term Overrides

Three keys have reduced tapping terms to favor tapping over holding during fast typing:

Key Override Effect
KC_I TAPPING_TERM - 70 Faster tap registration on the home-row letter I
KC_DELETE TAPPING_TERM - 120 Faster tap on the Delete key in the thumb cluster
KC_BSPC TAPPING_TERM - 120 Faster tap on the Backspace key in the thumb cluster

2. Language-Aware RGB

The base layer keeps its normal Oryx colours for English. For Hebrew, only Oryx's dominant base colour is replaced with the brown assigned to Layer 2's ten black/accidental MIDI melody keys (currently HSV 12, 255, 255). State is synced from Windows over RAW HID using Oryx's ORYX_STATUS_LED_CONTROL command (0x0A):

  • Param[0] = 0x00 → English
  • Param[0] = 0x01 → Hebrew

patch_keymap.py detects Layer 0's unique dominant non-black HSV triplet and reads the replacement from the ten semantic LED positions used by Layer 2's black MIDI melody keys. All ten source positions must agree on one non-black colour or patching fails. The Hebrew overlay changes only LEDs whose original Layer 0 colour exactly matches the detected base triplet, rendering the replacement through Oryx's normal brightness-aware HSV path. Individually assigned colours—including the language key colour—remain under Oryx control, Caps Lock still overrides its own key, and all higher-layer colours remain unchanged.

3. Tap-Dance Stabilization

patch_keymap.py normalizes tap-dance handlers to fix tapping-term races:

  • SINGLE_HOLDSINGLE_TAP fallback
  • DOUBLE_SINGLE_TAPDOUBLE_TAP
  • Hold-preference on Space/Shift and F18 language dances
  • Signature-detected immediate terminal actions (F22 on language press two, F19/F13 on space press three), with build failure if any target cannot be identified safely

4. Windows Host Tools: CopyQ + Windhawk

Host-key ownership is deliberately split:

Hotkey Firmware sources Owner Function
F13 Right-space triple tap CopyQ Smart Title Case; finishes unselected with caret at end
F18 Language-key single tap Windhawk Windows language switch
F19 Left-space triple tap CopyQ lower ↔ UPPER; mixed case → UPPER; remains selected
F22 Language-key double tap CopyQ Hebrew/English physical-key transplantation; finishes unselected

Smart Title Case uses the configured small-word list (a, an, and, as, at, but, by, for, in, nor, of, on, or, per, so, the, to, up, via, vs, yet), while always capitalizing the first/last lexical word and the first word after a colon or dash. Ordinary words are normalized; acronym detection is intentionally omitted.

Transplantation follows Microsoft kbdhebl3 physical positions. Direction is chosen by the predominant recognizable alphabet. Lowercase English is mapped to Hebrew, uppercase A–Z is preserved, Hebrew maps back to lowercase English, and alphabet ties are no-ops. This preserves the useful pattern where an initial shifted English capital survives while the following accidental lowercase English becomes Hebrew.

CopyQ performs each transformation as a protected, non-blocking transaction: it snapshots every clipboard MIME format, disables history storage, captures and pastes the selection immediately, and restores monitoring. CopyQ then uses afterMilliseconds() to reselect F19 output after 35 ms and safely restore the original clipboard after 250 ms only if no newer copy has replaced the generated text. Generation guards make stale callbacks harmless during rapid repeats, so delayed restoration does not freeze the CopyQ UI. The .NET helper used by F19 receives UTF-8 text over standard input, counts Unicode grapheme clusters, and reselects with the caret at the right edge. It has no clipboard, network, administrator, tray, or persistent-process access.

See host_tools/copyq/README.md for build, staged installation, activation, acceptance checks, and rollback.

Windhawk Setup

The v2 Windhawk source owns only F18 and RGB synchronization. Install or update it from host_tools/windhawk/moonlander_language_sync.wh.cpp, then use:

  • enableF18Hotkey = true
  • shortcutMode = 1 (Win+Space), 2 (Alt+Shift), 3 (Ctrl+Shift), or 0 (none)
  • pollIntervalMs = 120
  • onlyMoonlander = true

The previous v1.2.8 implementation is preserved under host_tools/windhawk/deprecated/ and marked deprecated. It must remain available as the immediate rollback during and after merge; do not enable it simultaneously with active CopyQ F19/F22 shortcuts.

Windhawk sends an Oryx-native RAW HID command to sync Windows language state to keyboard RGB:

  • Command: ORYX_STATUS_LED_CONTROL (0x0A)
  • Payload: param[0] = 0x00 (English), 0x01 (Hebrew)
  • Transport: raw HID output report to any ZSA Moonlander device (filtered by manufacturer + product string)
  • Firmware reads mirrored state from rawhid_state.status_led_control in custom_qmk/custom_code.c

The language RGB overlay only responds on Layer 0; MIDI and other layers use Oryx-configured per-layer colours. The language key has no firmware colour override and can be assigned a fixed colour directly in Oryx.

Troubleshooting:

  • Language switches but RGB does not update: set debugLogging = true and check Windhawk's log; temporarily set onlyMoonlander = false to test broader HID matching.
  • F18 should not trigger Windows language shortcut: set enableF18Hotkey = false. Keyboard-side RGB sync still runs.
  • Different Windows shortcut preferred: shortcutMode = 1 Win+Space (recommended), 2 Alt+Shift, 3 Ctrl+Shift, 0 None.

5. Macros (ST_MACRO_0 through ST_MACRO_17)

Layer 1 and Layer 3 carry Unicode and editor macros. Highlights:

Macros Purpose
ST_MACRO_0 Unicode U+20AC (Euro €) + Enter
ST_MACRO_1 Cut (Ctrl+X) + type B
ST_MACRO_5ST_MACRO_11 Various Ctrl+X/zoom/search shortcuts
ST_MACRO_12, ST_MACRO_13 Alt-code input for U+002E (period)
ST_MACRO_14ST_MACRO_17 Unicode U+002E input variants

These macros include a deliberate 100ms delay (SS_DELAY(100)) between keystrokes to ensure OS registration; they can be optimized to 10ms or 0ms in config.h if faster output is desired.

6. MIDI Layer (Layer 2)

Two octaves of polyphonic MIDI input with split melody/bass and a transpose shifter. Uses MIDI_ADVANCED (not MIDI_BASIC) so note keycodes route through process_midi() which decodes by keycode value — required for the bass shifter to work correctly.

Note Map

Melody (Row 2, natural/white keys):

Hand Keys Notes
Left k20–k26 C3 D3 E3 F3 G3 A3 B3
Right k27–k2d C4 D4 E4 F4 G4 A4 B4

Sharps/flats (Row 1, biased LEFT so each accidental sits directly above the natural it sharpens):

Hand Notes
Left (octave 3) C#3 D#3 F#3 G#3 A#3
Right (octave 4) Db4 Eb4 Gb4 Ab4 Bb4 (enharmonic = C#4 D#4 F#4 G#4 A#4)

Bass (Rows 3–4 + thumb cluster, left hand only; full chromatic C2–B2):

Row Keys Notes
Row 3 (BASS1–6) k30–k35 C2 C#2 D2 D#2 E2 F2
Row 4 (BASS7–11) k40–k44 F#2 G2 G#2 A2 A#2
Row 5 (BASS12) k50 B2 (left thumb cluster, one semitone below BASS1 root)

Bass Shifter (Thumb Cluster)

Three keys share the left thumb cluster on Layer 2: BASS12 (k50, the 12th chromatic bass note B2), BASS_up (k51), and BASS_down (k52). The two shifter keys each tap to transpose all bass notes ±1 semitone (free transpose across octaves, clamped −24..+24). Melody keys are in a higher keycode range and are never affected.

Implementation: The firmware intercepts bass keys by MIDI keycode range (MI_C2..MI_B2), not by matrix position. On press, the shifter forwards a transposed note keycode to process_midi(), which emits the real MIDI note. Each held key snapshots its shifted note keycode so that a shift change mid-hold cannot strand a stuck note.

Note on pitch: Under QMK MIDI, the keycode octave is relative to the global midi_config.octave (default puts MI_C at note 48 / C3). Keycode names describe note relationships; absolute pitch follows the global octave. The firmware sets midi_config.octave = 1 at init so MI_C2 sounds as C2 (not C5).

Build Requirements (injected automatically by patch_keymap.py)

  • MIDI_ENABLE = yes in rules.mk
  • #define MIDI_ADVANCED in config.h
  • DEBOUNCE_TYPE = sym_eager_pk, DEBOUNCE = 5, and USB_POLLING_INTERVAL_MS = 1 for low-latency MIDI
  • QMK_KEYS_PER_SCAN = 12 for instant polyphonic MIDI chords

Build Pipeline

Triggered manually via Actions → Fetch and build layout (fetch-and-build-layout.yml).

Parameters: Layout ID (3aMQz), geometry (moonlander/reva).

Steps:

  1. Validates the requested layout and Oryx response—including Oryx's integer-valued QMK strings such as "25.0"—then downloads and checks the source ZIP.
  2. Runs the repository test suite before resolving QMK.
  3. Applies scripts/patch_keymap.py twice and requires byte-identical patched trees.
  4. Checks out the exact firmware<version> ZSA QMK branch at a recorded commit; missing branches fail instead of falling back to master.
  5. Pulls the dated base image explicitly, builds the QMK container, and records the resolved base-image digest, compiler, and Python versions.
  6. Requires exactly one non-empty firmware candidate and its matching ELF.
  7. Publishes a unique, non-overwriting release containing the .bin, its SHA-256 checksum, and a provenance manifest.
  8. Downloads the published assets and verifies their names and checksum before synchronizing 3aMQz/.

Firmware assurance levels

  • Build passed → the source compiled successfully.
  • CI-verified candidate → tests, patch idempotence, exact QMK identity, closed artifact inventory, manifest, checksum, publication, and downloaded-asset verification all passed.
  • Hardware accepted → Wally, Keymapp, or ZSA's flasher successfully flashed that exact .bin, and the Moonlander reconnected and operated normally.

A green workflow deliberately stops at CI-verified candidate. GitHub Actions cannot prove that the physical keyboard accepted the image.

Verified reference candidate

The first complete run of the hardened pipeline passed on 2026-07-21: workflow run 29808688464. It built Oryx revision v6Grvl with ZSA QMK firmware25 at commit 717745325af7e8c92a1ec78faaa9abf8db321d5e, then published and downloaded the exact three-asset release for verification:

  • Release and flashable firmware
  • Firmware: zsa_moonlander_reva_3aMQz.bin — 55,872 bytes, SHA-256 05b5a9a40b1fd9d4a23f17e09e73af59aacba35ac9a8eabaf8bab586ddc7440b
  • Provenance: zsa_moonlander_reva_3aMQz.bin.manifest.json
  • Portable checksum: zsa_moonlander_reva_3aMQz.bin.sha256

This receipt proves the complete CI path for the then-current turquoise Hebrew overlay, immediate thumb-tap triggers, release inventory, and downloaded checksum. It is retained as a historical pipeline reference, but it does not verify the newer brown black-MIDI-key colour source. The next workflow run must create a new CI-verified candidate for that change, and hardware acceptance still requires flashing and testing that exact new binary.

What the patch script injects:

  • MIDI custom-keycode enum near top of keymap.c
  • Layer 2 MIDI keycodes (preserving user-added keys like RGB toggles)
  • #define MIDI_ADVANCED in config.h
  • MIDI_ENABLE = yes in rules.mk
  • Low-latency settings (DEBOUNCE_TYPE sym_eager_pk, DEBOUNCE 5, USB_POLLING_INTERVAL_MS 1, QMK_KEYS_PER_SCAN 12)
  • MIDI octave fix (midi_config.octave = 1 in keyboard_post_init_user)
  • Tap-dance stabilization patches
  • Language RGB indicator hook

Requirements

  • Hardware: ZSA Moonlander keyboard
  • Firmware flashing: Wally or ZSA Keymapp
  • GitHub account: for Actions (CI builds)
  • Windhawk (Windows-only, optional): https://windhawk.net/ — install moonlander_language_sync.wh.cpp
  • Docker (for local builds; not needed for CI-only usage)

Development Workflow

Making Layout Changes

  1. Edit your layout in Oryx (https://configure.zsa.io/)
  2. Trigger Actions → Fetch and build layout on GitHub
  3. Wait ~3-5 minutes for the build
  4. git pull to get the updated 3aMQz/ folder
  5. Flash the .bin from the release artifacts

Adding Custom Firmware Code

  1. Edit custom_qmk/custom_code.c
  2. Commit and push
  3. The next workflow run will copy your changes into the build

Modifying the Patch Script

  1. Edit scripts/patch_keymap.py
  2. Test locally: python3 scripts/patch_keymap.py 3aMQz
  3. Commit and push
  4. The next workflow run will use your updated patch

Local Testing

# From the repository root, reproduce the QMK input recorded by the
# 2026-07-21 reference manifest. Use a later manifest's identities when
# reproducing a later release.
git init qmk_firmware
git -C qmk_firmware remote add origin https://github.com/zsa/qmk_firmware.git
git -C qmk_firmware fetch --depth 1 origin 717745325af7e8c92a1ec78faaa9abf8db321d5e
git -C qmk_firmware checkout --detach 717745325af7e8c92a1ec78faaa9abf8db321d5e
git -C qmk_firmware submodule update --init --recursive --depth 1

mkdir -p qmk_firmware/keyboards/zsa/moonlander/reva/keymaps
cp -r 3aMQz qmk_firmware/keyboards/zsa/moonlander/reva/keymaps/

docker pull debian:bookworm-20260713-slim
docker build --pull --tag moonlander-qmk:local .
docker run --rm --volume "$PWD/qmk_firmware:/root" moonlander-qmk:local \
  /bin/sh -c 'make zsa/moonlander/reva:3aMQz'

Design Decisions

Why MIDI_ADVANCED instead of MIDI_BASIC?

Investigation of QMK source confirmed:

  • MIDI_BASIC routes note keycodes through process_music(), which (a) requires MIDI mode toggled ON (MI_ON) and (b) derives the note from matrix position, ignoring the note keycode entirely. register_code16() never emits MIDI.
  • MIDI_ADVANCED routes note keycodes through process_midi(), which decodes the note by keycode value (midi_compute_note) and tracks note-on/off. No mode toggle needed.

Decision: Use MIDI_ADVANCED (strict superset; future-proof). The bass shifter forwards a transposed note keycode to process_midi(), keeping the per-key snapshot so press/release use the same shifted keycode (no stuck notes).

Why F13/F19/F22 for CopyQ?

F19 is emitted only by the left-space triple tap. F22 is emitted only by the language-key double tap. The two adjacent modifier-only thumb keys use KC_NO as their tap action, so tapping or double tapping them does nothing. F13 is assigned to right-space Smart Title Case. CopyQ owns all three selected-text hotkeys so they share one protected clipboard transaction.

F18 remains separate because it is not a selected-text action: Windhawk converts it to the configured Windows language shortcut and synchronizes RGB state.

Why preserve-by-default for Layer 2?

The original patch script replaced the entire Layer 2 keymap, which erased user-configured keys from Oryx (such as RGB or layer toggles). The modern parser selectively merges QMK MIDI notes into designated matrix positions, leaving custom Oryx layout adjustments untouched.

Troubleshooting

Windhawk mod not working

  • Ensure Windhawk is installed and the mod is enabled
  • Check that F18 is emitted by DANCE_0 (left thumb)
  • Ordinary language-state checks use the configured background interval (120 ms by default). After F18, the mod checks every 10 ms for up to 250 ms and sends RGB only after Windows confirms the new language.
  • The mod targets explorer.exe; if another app has focus the window-level shortcut may not fire

CopyQ text tools not working

  • Confirm CopyQ is running and the three exact Moonlander: commands are enabled.
  • If F19 or F22 cannot be registered, update the installed Windhawk mod to v2 before activating CopyQ shortcuts; the old mod owns those keys.
  • Stage without shortcuts first and run each command manually from CopyQ.
  • Do not invoke a transformation without selected text in a terminal where Ctrl+C would interrupt a process.

MIDI notes sound one octave too high

The firmware sets midi_config.octave = 1 at init. If notes are still wrong, check your DAW's MIDI channel mapping.

MIDI latency is too high

The firmware uses DEBOUNCE_TYPE sym_eager_pk, DEBOUNCE 5, and USB_POLLING_INTERVAL_MS 1. If latency persists:

  • Disable RGB effects on the MIDI layer (use the RGB_TOG key you added)
  • Reduce your DAW's audio buffer size (128 or 256 samples)
  • Use ASIO drivers on Windows

Build fails with "not enough USB endpoints"

The Moonlander with MIDI + RAW HID + NKRO can exceed the STM32F303's USB endpoint limit. The firmware already disables MOUSEKEY_ENABLE and CONSOLE_ENABLE to free endpoints. If it still fails, you may need to disable additional features.

License

This project is provided as-is for personal use. QMK firmware is licensed under GPL-2.0.

Acknowledgments

  • ZSA Technology Labs for the Moonlander and Oryx
  • QMK community for the firmware framework
  • Microsoft for the kbdhebl3 Hebrew keyboard layout specification

About

Custom QMK build for ZSA Moonlander (Oryx Sync)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors