Workstations and toolingguide
Creating QMK, VIA, and Vial keymaps
QMK, VIA, and Vial overview
Section titled “QMK, VIA, and Vial overview”QMK is open-source keyboard firmware. You write C code defining your keymaps, compile it, and flash it. Changes require editing code, recompiling, and reflashing.
VIA adds dynamic keymap storage on top of QMK. Flash once, then remap keys through the web interface without recompiling. VIA is closed-source and requires JSON definitions for keyboard recognition. It doesn’t support rotary encoders, MIDI, or full RGB matrix control.
Vial is an open-source fork of VIA that addresses these limitations. It includes tap dance, combos, rotary encoders, MIDI, and complete RGB matrix support. Keyboard definitions are compiled into firmware instead of sideloaded.
Quick comparison
Section titled “Quick comparison”| Feature | QMK | VIA | Vial |
|---|---|---|---|
| License | GPL-2.0 (open) | Closed source | GPL-2.0 (open) |
| Live remapping | No | Yes | Yes |
| Tap dance | Yes | No | Yes |
| Combos | Yes | Limited | Yes |
| Rotary encoders | Yes | No | Yes |
| MIDI | Yes | No | Yes |
| RGB Matrix | Yes | Basic | Full support |
| Keyboard detection | N/A | JSON sideload required | Built into firmware |
| Web interface | No | usevia.app | vial.rocks |
| Desktop app | No | No | Yes (Win/Mac/Linux) |
Setting up QMK
Section titled “Setting up QMK”Requirements
Section titled “Requirements”- Linux/macOS/WSL
- Git
- Python 3.8+
- QMK-compatible keyboard
Clone QMK
Section titled “Clone QMK”git clone https://github.com/qmk/qmk_firmware.gitcd qmk_firmwareInstall QMK CLI
Section titled “Install QMK CLI”Two installation approaches:
Option A: Virtual Environment
python -m venv venvsource venv/bin/activate # Windows: venv\Scripts\activate
# NO --user flag in a venvpython -m pip install qmkOption B: Global Install
# Deactivate any venv firstdeactivate
# Use --user for global installpython -m pip install --user qmkDownload submodules
Section titled “Download submodules”make git-submoduleQMK depends on external libraries (LUFA, ChibiOS, etc.). Without this step, compilation fails:
tmk_core/protocol/lufa/lufa.mk:13: lib/lufa/LUFA/makefile: No such file or directorymake: *** No rule to make target 'lib/lufa/LUFA/makefile'. StopVerify it works
Section titled “Verify it works”qmk compile -kb planck/rev6 -km defaultIf this compiles without errors, the setup is complete.
Keymap anatomy
Section titled “Keymap anatomy”Keymaps live under keyboards/<vendor>/<model>/keymaps/<keymap_name>/. Standard structure:
keyboards/boardsource/5x12/keymaps/├── default/ # Factory keymap│ └── keymap.c├── via/ # VIA-enabled default│ ├── keymap.c│ └── rules.mk└── my-layout/ # Your custom keymap ├── keymap.c └── rules.mk # Optional: feature flagsMinimal keymap.c
Section titled “Minimal keymap.c”#include QMK_KEYBOARD_H
enum layers { _BASE = 0, _LOWER = 1, _RAISE = 2,};
#define LOWER MO(_LOWER)#define RAISE MO(_RAISE)
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { [_BASE] = LAYOUT_ortho_5x12( KC_GRV, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_BSPC, KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_DEL, KC_ESC, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_LSFT, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_ENT, KC_LCTL, KC_LGUI, KC_LALT, KC_APP, LOWER, KC_SPC, KC_SPC, RAISE, KC_LEFT, KC_DOWN, KC_UP, KC_RGHT ), // ... more layers};Breaking it down:
- Layer enum: Name your layers. Numbers must be sequential starting from 0.
- Defines: Shortcuts for layer access (
MO= momentary, hold to activate). - Keymap array: The actual key assignments.
LAYOUT_ortho_5x12is board-specific, check your keyboard’sinfo.jsonor existing keymaps for the right macro. - Order matters: Keys map to physical positions left-to-right, top-to-bottom.
Common keycodes
Section titled “Common keycodes”Full reference: QMK Keycodes
Letters, numbers, modifiers
Section titled “Letters, numbers, modifiers”KC_A through KC_Z // Self-explanatoryKC_1 through KC_0 // Number row
KC_LSFT, KC_RSFT // Left/Right ShiftKC_LCTL, KC_RCTL // Left/Right ControlKC_LALT, KC_RALT // Left/Right AltKC_LGUI, KC_RGUI // Super/Win/Cmd
KC_SPC, KC_BSPC, KC_ENT, KC_ESC, KC_TAB, KC_DELKC_F1 through KC_F12KC_LEFT, KC_RIGHT, KC_UP, KC_DOWNSymbols (where you’ll get burned)
Section titled “Symbols (where you’ll get burned)”These are unshifted keys. To get shifted symbols (!@#$%), use LSFT():
KC_MINUS // - (unshifted) and _ (shifted)KC_EQL // = (unshifted) and + (shifted)KC_LBRC // [ and {KC_RBRC // ] and }KC_BSLS // \ and |KC_SCLN // ; and :KC_QUOT // ' and "KC_GRV // ` and ~KC_COMM // , and <KC_DOT // . and >KC_SLSH // / and ?
// For shifted symbols:LSFT(KC_1) // !LSFT(KC_2) // @LSFT(KC_EQL) // +LSFT(KC_MINUS) // _Numpad keycodes
Section titled “Numpad keycodes”Avoid numpad keycodes on standard typing layers:
KC_KP_PLUS // Numpad plus - WRONG for a symbol layerKC_KP_MINUS // Numpad minus - WRONG for a symbol layerThese send different scancodes than the main keyboard equivalents. Use standard keys for symbol layers:
KC_EQL // Correct for = keyKC_MINUS // Correct for - keyLSFT(KC_EQL) // Correct for + symbolLayer control
Section titled “Layer control”MO(layer) // Hold to activate layerLT(layer, kc) // Tap for key, hold for layerTG(layer) // Toggle on/offDF(layer) // Set as default base layerCreating your keymap
Section titled “Creating your keymap”Start by copying
Section titled “Start by copying”Don’t write from scratch. Copy a working keymap:
cd keyboards/boardsource/5x12/keymapscp -r default my-colemakcd my-colemakEdit keymap.c
Section titled “Edit keymap.c”Example Colemak-DH layout with symbol layers:
enum layers { _COLEMAK = 0, _QWERTY = 1, _LOWER = 2, _RAISE = 3, _ADJUST = 4,};
#define LOWER MO(_LOWER)#define RAISE MO(_RAISE)
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { [_COLEMAK] = LAYOUT_ortho_5x12( KC_GRV, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_BSPC, KC_TAB, KC_Q, KC_W, KC_F, KC_P, KC_B, KC_J, KC_L, KC_U, KC_Y, KC_SCLN, KC_DEL, KC_ESC, KC_A, KC_R, KC_S, KC_T, KC_G, KC_M, KC_N, KC_E, KC_I, KC_O, KC_QUOT, KC_LSFT, KC_Z, KC_X, KC_C, KC_D, KC_V, KC_K, KC_H, KC_COMM, KC_DOT, KC_SLSH, KC_ENT, KC_LCTL, KC_LGUI, KC_LALT, KC_APP, LOWER, KC_SPC, KC_SPC, RAISE, KC_LEFT, KC_DOWN, KC_UP, KC_RGHT ),
[_LOWER] = LAYOUT_ortho_5x12( LSFT(KC_GRV), LSFT(KC_1), LSFT(KC_2), LSFT(KC_3), LSFT(KC_4), LSFT(KC_5), LSFT(KC_6), LSFT(KC_7), LSFT(KC_8), LSFT(KC_9), LSFT(KC_0), KC_BSPC, KC_ESC, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_DEL, KC_DEL, KC_F1, KC_F2, KC_F3, KC_F4, KC_F5, KC_F6, KC_MINUS, KC_EQL, KC_LBRC, KC_RBRC, KC_BSLS, KC_RSFT, KC_F7, KC_F8, KC_F9, KC_F10, KC_F11, KC_F12, _______, _______, _______, _______, KC_ENT, _______, RGB_TOG, RGB_MOD, RGB_RMOD,_______, KC_SPC, KC_SPC, _______, _______, _______, _______, _______ ),
[_RAISE] = LAYOUT_ortho_5x12( LSFT(KC_GRV), LSFT(KC_1), LSFT(KC_2), LSFT(KC_3), LSFT(KC_4), LSFT(KC_5), LSFT(KC_6), LSFT(KC_7), LSFT(KC_8), LSFT(KC_9), LSFT(KC_0), KC_BSPC, KC_ESC, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_DEL, KC_DEL, KC_F1, KC_F2, KC_F3, KC_F4, KC_F5, KC_F6, LSFT(KC_MINUS), LSFT(KC_EQL), LSFT(KC_LBRC), LSFT(KC_RBRC), LSFT(KC_BSLS), KC_RSFT, KC_F7, KC_F8, KC_F9, KC_F10, KC_F11, KC_F12, _______, KC_MPRV, KC_MNXT, _______, KC_ENT, _______, _______, _______, _______, _______, KC_SPC, KC_SPC, _______, KC_MUTE, KC_VOLD, KC_VOLU, KC_MPLY ),
[_ADJUST] = LAYOUT_ortho_5x12( _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, QK_BOOT, KC_PWR, KC_SLEP, KC_WAKE, _______, _______, _______, _______, DF(_QWERTY), DF(_COLEMAK), _______, _______ ),};
// Tri-layer: Hold LOWER+RAISE to activate ADJUSTlayer_state_t layer_state_set_user(layer_state_t state) { return update_tri_layer_state(state, _LOWER, _RAISE, _ADJUST);}Notes:
_______is transparent (passes through to lower layer)XXXXXXXdisables the key completelyDF(_QWERTY)switches the default base layerQK_BOOTputs the keyboard in bootloader mode for reflashing
Test compilation
Section titled “Test compilation”Before flashing, make sure it compiles:
qmk compile -kb boardsource/5x12 -km my-colemakCommon errors:
- “Undefined keycode”: Typo. Check against the keycode list.
- “Expected N arguments, got M”: Wrong number of keys in
LAYOUT_*()macro. - “Missing comma”: Every keycode needs a comma after it (except the last one in a row).
Enabling VIA
Section titled “Enabling VIA”VIA requires two tiny changes.
1. Create rules.mk
Section titled “1. Create rules.mk”In your keymap directory, create rules.mk:
VIA_ENABLE = yesIf your keyboard supports other features (RGB, audio, etc.):
VIA_ENABLE = yesRGBLIGHT_ENABLE = yesAUDIO_ENABLE = yes2. Explicit layer numbering
Section titled “2. Explicit layer numbering”VIA needs layers explicitly numbered. Change your enum:
enum layers { _COLEMAK = 0, // Must be explicit _QWERTY = 1, _LOWER = 2, _RAISE = 3, _ADJUST = 4,};That’s it. Compile and flash:
qmk compile -kb boardsource/5x12 -km my-colemakAfter flashing, visit usevia.app. Your keyboard should be detected automatically. If not, check that VIA is enabled in rules.mk and you recompiled after adding it.
Enabling Vial
Section titled “Enabling Vial”Vial requires the vial-qmk fork instead of mainline QMK.
Clone vial-qmk
Section titled “Clone vial-qmk”Do not nest this inside your existing qmk_firmware directory. Clone it separately:
cd ~git clone https://github.com/vial-kb/vial-qmk.gitcd vial-qmkmake git-submoduleCreate Vial keymap
Section titled “Create Vial keymap”The keymap must be named vial:
cd keyboards/boardsource/5x12/keymapscp -r default vialcd vial1. Create rules.mk
Section titled “1. Create rules.mk”VIA_ENABLE = yesVIAL_ENABLE = yesYes, you need both. Vial builds on VIA’s protocol.
2. Generate keyboard UID
Section titled “2. Generate keyboard UID”Every Vial keyboard needs a unique 8-byte identifier:
# From the vial-qmk root directorypython3 util/vial_generate_keyboard_uid.pyThis outputs something like:
{0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX}3. Create config.h
Section titled “3. Create config.h”In your keymaps/vial/ directory:
#pragma once
#define VIAL_KEYBOARD_UID {0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX}
// Unlock key combination (prevents malicious host from changing settings)// Define at least 2 keys: top-left and bottom-right corners are common#define VIAL_UNLOCK_COMBO_ROWS { 0, 4 }#define VIAL_UNLOCK_COMBO_COLS { 0, 11 }Replace the 0xXX bytes with what the UID generator gave you.
The unlock combo is a security feature. You hold those keys while plugging in the keyboard to unlock security settings. Pick two keys far apart (like top-left and bottom-right).
4. Create vial.json
Section titled “4. Create vial.json”This defines your keyboard’s physical layout for the Vial UI. It’s not the same as QMK’s info.json.
You can either:
- Use the VIA Layout Editor to generate one
- Convert your existing
info.json(check Vial porting docs) - Copy from a similar keyboard and modify
Minimal example for a 5x12 ortho:
{ "name": "Boardsource 5x12", "lighting": "none", "matrix": { "rows": 5, "cols": 12 }, "layouts": { "keymap": [ ["0,0", "0,1", "0,2", "0,3", "0,4", "0,5", "0,6", "0,7", "0,8", "0,9", "0,10", "0,11"], ["1,0", "1,1", "1,2", "1,3", "1,4", "1,5", "1,6", "1,7", "1,8", "1,9", "1,10", "1,11"], ["2,0", "2,1", "2,2", "2,3", "2,4", "2,5", "2,6", "2,7", "2,8", "2,9", "2,10", "2,11"], ["3,0", "3,1", "3,2", "3,3", "3,4", "3,5", "3,6", "3,7", "3,8", "3,9", "3,10", "3,11"], ["4,0", "4,1", "4,2", "4,3", "4,4", "4,5", "4,6", "4,7", "4,8", "4,9", "4,10", "4,11"] ] }}The "row,col" strings map to your matrix positions.
5. Update keymap.c
Section titled “5. Update keymap.c”Use the same keymap as before with explicit layer numbering:
enum layers { _COLEMAK = 0, _QWERTY = 1, _LOWER = 2, _RAISE = 3, _ADJUST = 4,};Compile Vial firmware
Section titled “Compile Vial firmware”From the vial-qmk root directory:
make boardsource/5x12:vialDo not use qmk compile with vial-qmk; use make directly. The build system is slightly different.
Flash and test
Section titled “Flash and test”make boardsource/5x12:vial:flashAfter flashing, visit vial.rocks or download the desktop app. Your keyboard should be detected immediately, no JSON sideloading needed.
Vial directory structure
Section titled “Vial directory structure”Final structure should look like:
keyboards/boardsource/5x12/keymaps/vial/├── config.h # Keyboard UID and unlock combo├── keymap.c # Your actual keymap├── rules.mk # VIA_ENABLE and VIAL_ENABLE└── vial.json # Physical layout definitionKeeping keymaps outside qmk_firmware (External Userspace)
Section titled “Keeping keymaps outside qmk_firmware (External Userspace)”Everything above keeps keymaps inside a qmk_firmware clone, which means maintaining a fork. Mine sat on a May 2024 base until October 2026. QMK’s External Userspace keeps only your keymaps in a small repo and builds them against an upstream checkout. Start from the qmk_userspace template.
The repo mirrors the qmk_firmware layout:
qmk_userspace/├── qmk.json└── keyboards/boardsource/equals/60/keymaps/ ├── erfi/ keymap.c rules.mk config.h └── erfi_cast/ keymap.c rules.mk config.hqmk.json lists what to build:
{ "userspace_version": "1.0", "build_targets": [ ["boardsource/equals/60", "erfi"], ["boardsource/equals/60", "erfi_cast"] ]}Keymap names in qmk.json must match ^[0-9a-z_]*$ (data/schemas/definitions.jsonschema, the filename type). A hyphenated name such as via-colemak-mod-dh-erfi fails schema validation and the CLI reports “Could not determine QMK userspace location”, so rename keymaps when you move them over.
Anything a fork changed at keyboard level moves into the keymap’s config.h. My fork had added DYNAMIC_KEYMAP_LAYER_COUNT 5 and the RP2040 double-tap reset to keyboards/boardsource/equals/60/config.h; upstream’s copy has neither, so both now sit in each keymap’s config.h:
#pragma once
#define DYNAMIC_KEYMAP_LAYER_COUNT 5
// Double-tap the PCB reset button to enter the RP2040 bootloader.#define RP2040_BOOTLOADER_DOUBLE_TAP_RESET#define RP2040_BOOTLOADER_DOUBLE_TAP_RESET_TIMEOUT 200UBuild locally in the QMK container
Section titled “Build locally in the QMK container”The ghcr.io/qmk/qmk_cli image carries the toolchains and the qmk CLI, so nothing needs installing on the host. Clone upstream once (a shallow clone with submodules was 1.5 GB on 2026-10-30), then point the CLI at both trees:
git clone --depth 1 --recurse-submodules --shallow-submodules \ https://github.com/qmk/qmk_firmware.git ~/qmk_firmware
cd qmk_userspacedocker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp \ -v ~/qmk_firmware:/qmk_firmware -v "$PWD":/userspace -w /userspace \ ghcr.io/qmk/qmk_cli:latest sh -c \ 'qmk config user.qmk_home=/qmk_firmware user.overlay_dir=/userspace && qmk userspace-compile -p'qmk userspace-compile builds every target in qmk.json and leaves the .uf2 files in the userspace root.
Build in CI on Forgejo Actions
Section titled “Build in CI on Forgejo Actions”The template ships a GitHub Actions workflow that calls QMK’s reusable workflows in qmk/.github. On Forgejo Actions I replaced it with one job that runs inside ghcr.io/qmk/qmk_cli. That image has no Node.js, so JavaScript actions (actions/checkout, actions/cache, actions/upload-artifact) cannot run in the job: checkout is a git fetch with the job token, and publishing is curl against the release API. Each push to main rebuilds every target and replaces a rolling latest release that holds the .uf2 files and their SHA-256 sums. The first run took 2m08s and a second 1m02s (2026-10-30). The general porting notes are in Porting a GitHub Actions workflow to Forgejo Actions, and release storage in Forgejo releases and registry.
name: Build firmwareon: push: branches: [main] workflow_dispatch:
concurrency: group: build cancel-in-progress: false
jobs: build: runs-on: ubuntu-latest container: ghcr.io/qmk/qmk_cli:latest timeout-minutes: 45 env: QMK_REPO: https://github.com/qmk/qmk_firmware.git QMK_REF: master steps: - name: checkout userspace env: TOKEN: ${{ github.token }} run: | set -eu git init -q userspace cd userspace git -c http.extraHeader="Authorization: token $TOKEN" \ fetch -q --depth 1 "${{ forgejo.server_url }}/${{ github.repository }}.git" "$GITHUB_SHA" git checkout -q FETCH_HEAD
- name: checkout qmk_firmware run: | set -eu git clone -q --depth 1 --branch "$QMK_REF" --recurse-submodules --shallow-submodules "$QMK_REPO" qmk_firmware git -C qmk_firmware rev-parse HEAD | tee qmk_sha
- name: build run: | set -eu qmk config user.qmk_home="$PWD/qmk_firmware" user.overlay_dir="$PWD/userspace" \ userspace_compile.parallel="$(nproc)" qmk userspace-doctor cd userspace qmk userspace-compile -p sha256sum *.uf2 > checksums.txt
- name: publish rolling "latest" release env: TOKEN: ${{ github.token }} run: | set -eu cd userspace api="${{ forgejo.server_url }}/api/v1/repos/${{ github.repository }}" auth="Authorization: token $TOKEN" old="$(curl -sf -H "$auth" "$api/releases/tags/latest" | jq -r '.id' || true)" if [ -n "$old" ] && [ "$old" != "null" ]; then curl -sf -X DELETE -H "$auth" "$api/releases/$old" fi curl -s -o /dev/null -X DELETE -H "$auth" "$api/tags/latest" || true rel="$(curl -sf -X POST -H "$auth" -H 'Content-Type: application/json' \ --data "$(jq -n --arg sha "$GITHUB_SHA" \ '{tag_name: "latest", name: "Latest firmware", target_commitish: $sha}')" \ "$api/releases" | jq -r '.id')" for f in *.uf2 checksums.txt; do curl -sf -X POST -H "$auth" -H 'Content-Type: application/octet-stream' \ --data-binary "@$f" "$api/releases/$rel/assets?name=$f" >/dev/null doneThe job clones upstream in full every run; caching it would need either Node.js in the job or a host-side cache.
Variants of one keymap
Section titled “Variants of one keymap”erfi_cast is erfi with Win and Menu blanked on the base layers, for a period when stray presses on those two keys kept opening the Start menu. Rather than copy the keymap, the variant includes it:
#define ERFI_CAST#include "../erfi/keymap.c"The base keymap switches the two keys on that define, and puts them on RAISE in the cast build so Win+L still works on purpose:
#ifdef ERFI_CAST# define B_GUI KC_NO# define B_APP KC_NO# define R_GUI KC_LGUI# define R_APP KC_APP#else# define B_GUI KC_LGUI# define B_APP KC_APP# define R_GUI _______# define R_APP _______#endifA quoted #include is resolved relative to the file that contains it. Each variant still needs its own rules.mk and config.h.
VIA keeps the old keymap after a same-day flash
Section titled “VIA keeps the old keymap after a same-day flash”VIA stores the keymap in EEPROM and only reloads the compiled one when the EEPROM magic stops matching. via_eeprom_is_valid() in quantum/via.c derives that magic from the year, month and day of QMK_BUILDDATE, so flashing a build from the same day as the previous one keeps the old keys. Both variants from one CI run share a date. This hook in the base keymap reloads the compiled keymap once per build, keyed on the full build timestamp plus the variant:
#include "version.h"
void keyboard_post_init_user(void) { uint32_t id = 2166136261u; // FNV-1a over build timestamp + variant const char *p = QMK_BUILDDATE; while (*p) { id = (id ^ (uint8_t)*p++) * 16777619u; } id = (id ^ ERFI_VARIANT) * 16777619u; // 'c' for cast, 'n' otherwise if (eeconfig_read_user() != id) { dynamic_keymap_reset(); eeconfig_update_user(id); }}Edits made in VIA afterwards persist until the next flash.
Tested on 2026-10-30 with both .uf2 files from one CI run, which carry the same QMK_BUILDDATE (2026-10-30-04:34:18), so via_eeprom_is_valid() passes on both flashes and stock VIA keeps EEPROM. Before flashing, a VIA edit set ADJUST (0,0) from KC_TRNS to KC_F13. The board was read back over VIA’s raw HID protocol (0x04 get keycode) after each flash:
| State | Base row 4, cols 1 and 3 | RAISE row 4, cols 1 and 3 | ADJUST (0,0) |
|---|---|---|---|
erfi_cast + VIA edit | 0000 0000 | 00e3 0065 | 0068 (the edit) |
flash erfi | 00e3 0065 | 0001 0001 | 0001 |
flash erfi_cast | 0000 0000 | 00e3 0065 | 0001 |
The compiled keymap replaced EEPROM on both flashes and the VIA edit was gone after the first. The stock-VIA control (the same flashes without the hook) was not run; that half rests on the via.c source above.
The reverse also applies: when the build date does change, eeconfig_is_enabled() returns false and QMK re-initialises all of EEPROM, so RGB and other saved settings return to their defaults, not just the keymap.
Check the keymap in the binary
Section titled “Check the keymap in the binary”A green build does not prove the right keys landed. The compiled keymaps array is layers x 60 x 2 bytes on a 5x12 board, so the bottom row of layer n starts at byte n*120 + 96. Inside the container:
arm-none-eabi-gdb -q -batch -ex 'x/5hx (char*)&keymaps+96' \ .build/boardsource_equals_60_erfi_cast.elf0x1000fcc8 <keymaps+96>: 0x00e0 0x0000 0x00e2 0x0000 0x52230x00e3 (LGUI) and 0x0065 (APP) in the erfi build are 0x0000 (KC_NO) in erfi_cast.
Flashing your keyboard
Section titled “Flashing your keyboard”Put keyboard in bootloader mode
Section titled “Put keyboard in bootloader mode”Most boards: Press the physical RESET button on the PCB, or press the QK_BOOT key in your keymap (usually on an _ADJUST layer).
Flash it
Section titled “Flash it”For ATmega32U4/STM32 boards (most keyboards):
qmk flash -kb boardsource/5x12 -km my-colemakThis compiles, waits for bootloader detection, and flashes automatically. If it hangs on “Waiting for device…”, your keyboard isn’t in bootloader mode.
Linux permissions issue?
sudo qmk flash -kb boardsource/5x12 -km my-colemakOr add udev rules (see QMK docs).
For RP2040 boards (Boardsource Equals 60):
RP2040 boards use UF2 bootloader and appear as USB mass storage devices. No flashing tool needed:
- Put keyboard in bootloader mode (RESET button or
QK_BOOTkey) - A drive named
RPI-RP2appears - Compile the firmware:
Terminal window qmk compile -kb boardsource/equals/60 -km my-keymap - Copy the
.uf2file to theRPI-RP2drive:Terminal window cp .build/boardsource_equals_60_my-keymap.uf2 /media/$USER/RPI-RP2/ - The keyboard reboots automatically after the file copies
The .uf2 file is in .build/ after compilation.
Debugging when things break
Section titled “Debugging when things break”Console output
Section titled “Console output”Enable debug printing in rules.mk:
CONSOLE_ENABLE = yesAdd prints to keymap.c:
#include "print.h"
bool process_record_user(uint16_t keycode, keyrecord_t *record) { if (record->event.pressed) { uprintf("Pressed: 0x%04X\n", keycode); } return true;}View output:
qmk consoleCommon issues
Section titled “Common issues”| Symptom | Cause | Fix |
|---|---|---|
| Key does nothing | Wrong keycode or transparent key with no lower layer | Check keycode, use XXXXXXX to explicitly disable |
| Wrong symbol appears | Used numpad key (KC_KP_*) instead of main keyboard key | Use KC_MINUS, KC_EQL, etc. |
| Layer won’t activate | Layer index mismatch or wrong MO() value | Verify enum numbers match layer definitions |
| VIA doesn’t detect | VIA_ENABLE missing or not recompiled | Add to rules.mk, recompile, reflash |
| Vial doesn’t detect | Missing vial.json or wrong UID | Check VIAL_KEYBOARD_UID in config.h, verify vial.json exists |
| Compile fails after git pull | Submodules out of sync | Run make git-submodule again |
| vial-qmk compile error | Using qmk compile instead of make | Use make keyboard:keymap with vial-qmk |
Real examples (my actual boards)
Section titled “Real examples (my actual boards)”Boardsource 5x12
Section titled “Boardsource 5x12”# Standard QMK (qmk_firmware)qmk compile -kb boardsource/5x12 -km 5x12-mod-dh-erfiqmk flash -kb boardsource/5x12 -km 5x12-mod-dh-erfi
# VIA-enabled (qmk_firmware)qmk compile -kb boardsource/5x12 -km via-colemak-mod-dh-erfiqmk flash -kb boardsource/5x12 -km via-colemak-mod-dh-erfi
# Vial-enabled (vial-qmk)cd ~/vial-qmkmake boardsource/5x12:vialmake boardsource/5x12:vial:flashBoardsource equals 60
Section titled “Boardsource equals 60”Since 2026-10-30 the Equals 60 keymaps live in an External Userspace (see Keeping keymaps outside qmk_firmware): erfi (Colemak Mod-DH, VIA on) and erfi_cast. CI publishes boardsource_equals_60_erfi.uf2 and boardsource_equals_60_erfi_cast.uf2; flashing is a copy onto the RPI-RP2 drive. The board enumerates as USB 4273:7688 and exposes VIA’s raw HID interface (usage page 0xFF60).
The older fork-based commands, kept for the 5x12 and for reference:
# Standard QMK (qmk_firmware)qmk compile -kb boardsource/equals/60 -km colemak-mod-dh-erficp .build/boardsource_equals_60_colemak-mod-dh-erfi.uf2 /media/$USER/RPI-RP2/
# VIA-enabled (qmk_firmware)qmk compile -kb boardsource/equals/60 -km via-colemak-mod-dh-erficp .build/boardsource_equals_60_via-colemak-mod-dh-erfi.uf2 /media/$USER/RPI-RP2/
# Vial-enabled (vial-qmk)cd ~/vial-qmkmake boardsource/equals/60:vialcp .build/boardsource_equals_60_vial.uf2 /media/$USER/RPI-RP2/Or use qmk flash if you prefer automated flashing.
All three use the same base keymap, the difference is in rules.mk and whether you’re using qmk_firmware or vial-qmk.
QMK has no Hall-effect support, so the 5x12 Hall-effect board I’m building runs libhmk instead, with the Equals 60 keymap ported to it: GRIDDLE LX: an open 5x12 Hall-effect keyboard.
Advanced stuff you might want
Section titled “Advanced stuff you might want”Custom keycodes (macros)
Section titled “Custom keycodes (macros)”enum custom_keycodes { SHRUG = SAFE_RANGE,};
bool process_record_user(uint16_t keycode, keyrecord_t *record) { switch (keycode) { case SHRUG: if (record->event.pressed) { SEND_STRING("¯\\_(ツ)_/¯"); } return false; } return true;}Use SHRUG in your keymap like any other keycode.
Tap dance
Section titled “Tap dance”Different actions based on tap count. Requires TAP_DANCE_ENABLE = yes in rules.mk:
enum { TD_ESC_CAPS,};
tap_dance_action_t tap_dance_actions[] = { [TD_ESC_CAPS] = ACTION_TAP_DANCE_DOUBLE(KC_ESC, KC_CAPS),};Tap once for Esc, twice for Caps Lock. Use TD(TD_ESC_CAPS) in your keymap.
RGB control
Section titled “RGB control”# In rules.mkRGBLIGHT_ENABLE = yesThen use in keymap:
RGB_TOG // Toggle RGB on/offRGB_MOD // Next modeRGB_RMOD // Previous modeRGB_HUI // Hue +RGB_SAI // Saturation +RGB_VAI // Brightness +Quick reference
Section titled “Quick reference”# QMK Setupgit clone https://github.com/qmk/qmk_firmware.gitcd qmk_firmwarepython -m venv venv && source venv/bin/activatepip install qmkmake git-submodule
# Vial Setup (separate from QMK)cd ~git clone https://github.com/vial-kb/vial-qmk.gitcd vial-qmkmake git-submodule
# List keyboardsqmk list-keyboards | grep -i boardsource
# Compile (QMK/VIA)qmk compile -kb <keyboard> -km <keymap>qmk flash -kb <keyboard> -km <keymap>
# Compile (Vial)cd ~/vial-qmkmake <keyboard>:vialmake <keyboard>:vial:flash
# Clean build artifactsqmk clean # For QMKmake clean # For Vial
# View keyboard infoqmk info -kb <keyboard>Resources
Section titled “Resources”QMK:
VIA:
Vial (Open Source):
- Vial Homepage
- Vial Web App
- Vial Desktop Downloads
- Vial Documentation
- vial-qmk GitHub
- vial-gui GitHub
Troubleshooting checklist
Section titled “Troubleshooting checklist”Before asking for help:
General:
- Ran
make git-submodule - Keymap compiles without errors
- Layer enum numbers match layer array indices (0, 1, 2, 3…)
- No typos in keycodes
- Correct number of keys in
LAYOUT_*()macro - Keyboard in bootloader mode when flashing
- Tried turning it off and on again
VIA-specific:
-
VIA_ENABLE = yesinrules.mk - Using
qmk_firmware(not vial-qmk) - Browser has USB permission for usevia.app
- Keyboard JSON definition loaded (if required)
Vial-specific:
- Both
VIA_ENABLE = yesandVIAL_ENABLE = yesinrules.mk - Using
vial-qmkfork (not standard qmk_firmware) -
VIAL_KEYBOARD_UIDset inconfig.h -
vial.jsonexists in keymap directory - Keymap named exactly
vial(notviaor anything else) - Using
makecommands, notqmk compile - Unlock combo defined (or
VIAL_INSECUREset)
For additional help, see QMK Discord.
Verification
Section titled “Verification”| Check | How | Expected |
|---|---|---|
| The toolchain works before you touch a keymap | qmk compile -kb planck/rev6 -km default after make git-submodule | compiles with no errors; a missing submodule fails on lib/lufa/LUFA/makefile |
| Your keymap compiles | qmk compile -kb boardsource/5x12 -km my-colemak | no errors before flashing |
| Vial firmware builds | make boardsource/5x12:vial from the vial-qmk root | no errors; qmk compile against vial-qmk is the wrong entry point |
| VIA sees the board | flash, then open usevia.app | detected automatically; if not, VIA_ENABLE = yes is in rules.mk and the firmware was rebuilt |
| Vial sees the board | flash, then open vial.rocks or the desktop app | detected with no JSON sideload; a miss points at VIAL_KEYBOARD_UID or vial.json |
| Key presses arrive | CONSOLE_ENABLE = yes in rules.mk and qmk console | Pressed: 0x.... lines as keys are hit |
| The userspace is valid | qmk userspace-doctor in the container | Userspace enabled: True; a schema error names the bad build_targets entry |
| The intended keys are in the firmware | arm-none-eabi-gdb -batch -ex 'x/12hx (char*)&keymaps+<n*120+96>' <elf> | the keycodes you expect on that row |
Gotchas and lessons learned
Section titled “Gotchas and lessons learned”- Do not mix the two QMK CLI installs.
--userinside a venv fails withCan not perform a '--user' install. - A missing submodule fails the first compile, on
lib/lufa/LUFA/makefile. - Numpad keycodes are the wrong keycodes for a symbol layer -
KC_KP_*sends different scancodes fromKC_MINUSandKC_EQL. - VIA needs at least four layers in firmware. Empty layers filled with
_______count. - Vial needs both
VIA_ENABLEandVIAL_ENABLE, and its keymap directory must be named exactlyvial. vial.jsonis not QMK’sinfo.json; it is Vial’s own layout definition.make, notqmk compile, with vial-qmk.VIAL_INSECUREdisables the unlock-combo protection.- RP2040 flashing is a file copy. The
.uf2is left in.build/and dropped onto theRPI-RP2drive that appears in bootloader mode. - Userspace keymap names are
[0-9a-z_]only. Hyphens fail theqmk.jsonschema. - VIA only reloads the compiled keymap on a new build date, so a same-day reflash keeps the old keys unless the keymap resets them (see above).
- RGB Matrix keycodes were renamed. Current QMK defines
RM_TOGG,RM_NEXTandRM_PREVinquantum/keycodes.h;RGB_TOGsurvives only in the legacy keycode data, so a 2024 keymap needs the new names. - The
qmk_cliimage has no Node.js, so a CI job running inside it cannot use JavaScript actions.
File reference
Section titled “File reference”| File | What it is |
|---|---|
keyboards/<vendor>/<model>/keymaps/<keymap_name>/keymap.c | The keymap: the layer enum, the LOWER / RAISE defines and the keymaps[][] array |
rules.mk | Feature flags in the keymap directory: VIA_ENABLE, VIAL_ENABLE, CONSOLE_ENABLE, TAP_DANCE_ENABLE, RGBLIGHT_ENABLE |
config.h | Vial only: VIAL_KEYBOARD_UID and the VIAL_UNLOCK_COMBO_ROWS / VIAL_UNLOCK_COMBO_COLS pair |
vial.json | Vial only: the physical layout the Vial UI draws, keyed on matrix positions |
.build/<keyboard>_<keymap>.uf2 | The RP2040 build output, copied to the RPI-RP2 drive |
the qmk_firmware and ~/vial-qmk clones | The two checkouts; vial-qmk must not be nested inside qmk_firmware |
qmk.json | External Userspace only: userspace_version and the build_targets list |
.forgejo/workflows/build.yml | External Userspace only: the CI build and rolling release |