Skip to content

Workstations and toolingguide

Creating QMK, VIA, and Vial keymaps


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.

FeatureQMKVIAVial
LicenseGPL-2.0 (open)Closed sourceGPL-2.0 (open)
Live remappingNoYesYes
Tap danceYesNoYes
CombosYesLimitedYes
Rotary encodersYesNoYes
MIDIYesNoYes
RGB MatrixYesBasicFull support
Keyboard detectionN/AJSON sideload requiredBuilt into firmware
Web interfaceNousevia.appvial.rocks
Desktop appNoNoYes (Win/Mac/Linux)

  • Linux/macOS/WSL
  • Git
  • Python 3.8+
  • QMK-compatible keyboard
Terminal window
git clone https://github.com/qmk/qmk_firmware.git
cd qmk_firmware

Two installation approaches:

Option A: Virtual Environment

Terminal window
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# NO --user flag in a venv
python -m pip install qmk

Option B: Global Install

Terminal window
# Deactivate any venv first
deactivate
# Use --user for global install
python -m pip install --user qmk
Terminal window
make git-submodule

QMK 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 directory
make: *** No rule to make target 'lib/lufa/LUFA/makefile'. Stop
Terminal window
qmk compile -kb planck/rev6 -km default

If this compiles without errors, the setup is complete.


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 flags
GPL-2.0-or-later
#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:

  1. Layer enum: Name your layers. Numbers must be sequential starting from 0.
  2. Defines: Shortcuts for layer access (MO = momentary, hold to activate).
  3. Keymap array: The actual key assignments. LAYOUT_ortho_5x12 is board-specific, check your keyboard’s info.json or existing keymaps for the right macro.
  4. Order matters: Keys map to physical positions left-to-right, top-to-bottom.

Full reference: QMK Keycodes

KC_A through KC_Z // Self-explanatory
KC_1 through KC_0 // Number row
KC_LSFT, KC_RSFT // Left/Right Shift
KC_LCTL, KC_RCTL // Left/Right Control
KC_LALT, KC_RALT // Left/Right Alt
KC_LGUI, KC_RGUI // Super/Win/Cmd
KC_SPC, KC_BSPC, KC_ENT, KC_ESC, KC_TAB, KC_DEL
KC_F1 through KC_F12
KC_LEFT, KC_RIGHT, KC_UP, KC_DOWN

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) // _

Avoid numpad keycodes on standard typing layers:

KC_KP_PLUS // Numpad plus - WRONG for a symbol layer
KC_KP_MINUS // Numpad minus - WRONG for a symbol layer

These send different scancodes than the main keyboard equivalents. Use standard keys for symbol layers:

KC_EQL // Correct for = key
KC_MINUS // Correct for - key
LSFT(KC_EQL) // Correct for + symbol
MO(layer) // Hold to activate layer
LT(layer, kc) // Tap for key, hold for layer
TG(layer) // Toggle on/off
DF(layer) // Set as default base layer

Don’t write from scratch. Copy a working keymap:

Terminal window
cd keyboards/boardsource/5x12/keymaps
cp -r default my-colemak
cd my-colemak

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 ADJUST
layer_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)
  • XXXXXXX disables the key completely
  • DF(_QWERTY) switches the default base layer
  • QK_BOOT puts the keyboard in bootloader mode for reflashing

Before flashing, make sure it compiles:

Terminal window
qmk compile -kb boardsource/5x12 -km my-colemak

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

VIA requires two tiny changes.

In your keymap directory, create rules.mk:

VIA_ENABLE = yes

If your keyboard supports other features (RGB, audio, etc.):

VIA_ENABLE = yes
RGBLIGHT_ENABLE = yes
AUDIO_ENABLE = yes

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:

Terminal window
qmk compile -kb boardsource/5x12 -km my-colemak

After 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.


Vial requires the vial-qmk fork instead of mainline QMK.

Do not nest this inside your existing qmk_firmware directory. Clone it separately:

Terminal window
cd ~
git clone https://github.com/vial-kb/vial-qmk.git
cd vial-qmk
make git-submodule

The keymap must be named vial:

Terminal window
cd keyboards/boardsource/5x12/keymaps
cp -r default vial
cd vial
VIA_ENABLE = yes
VIAL_ENABLE = yes

Yes, you need both. Vial builds on VIA’s protocol.

Every Vial keyboard needs a unique 8-byte identifier:

Terminal window
# From the vial-qmk root directory
python3 util/vial_generate_keyboard_uid.py

This outputs something like:

{0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX}

In your keymaps/vial/ directory:

GPL-2.0-or-later
#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).

This defines your keyboard’s physical layout for the Vial UI. It’s not the same as QMK’s info.json.

You can either:

  1. Use the VIA Layout Editor to generate one
  2. Convert your existing info.json (check Vial porting docs)
  3. 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.

Use the same keymap as before with explicit layer numbering:

enum layers {
_COLEMAK = 0,
_QWERTY = 1,
_LOWER = 2,
_RAISE = 3,
_ADJUST = 4,
};

From the vial-qmk root directory:

Terminal window
make boardsource/5x12:vial

Do not use qmk compile with vial-qmk; use make directly. The build system is slightly different.

Terminal window
make boardsource/5x12:vial:flash

After flashing, visit vial.rocks or download the desktop app. Your keyboard should be detected immediately, no JSON sideloading needed.

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 definition

Keeping 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.h

qmk.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 200U

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:

Terminal window
git clone --depth 1 --recurse-submodules --shallow-submodules \
https://github.com/qmk/qmk_firmware.git ~/qmk_firmware
cd qmk_userspace
docker 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.

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.

.forgejo/workflows/build.yml
name: Build firmware
on:
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
done

The job clones upstream in full every run; caching it would need either Node.js in the job or a host-side cache.

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:

keymaps/erfi_cast/keymap.c
#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 _______
#endif

A 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:

StateBase row 4, cols 1 and 3RAISE row 4, cols 1 and 3ADJUST (0,0)
erfi_cast + VIA edit0000 000000e3 00650068 (the edit)
flash erfi00e3 00650001 00010001
flash erfi_cast0000 000000e3 00650001

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.

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:

Terminal window
arm-none-eabi-gdb -q -batch -ex 'x/5hx (char*)&keymaps+96' \
.build/boardsource_equals_60_erfi_cast.elf
0x1000fcc8 <keymaps+96>: 0x00e0 0x0000 0x00e2 0x0000 0x5223

0x00e3 (LGUI) and 0x0065 (APP) in the erfi build are 0x0000 (KC_NO) in erfi_cast.


Most boards: Press the physical RESET button on the PCB, or press the QK_BOOT key in your keymap (usually on an _ADJUST layer).

For ATmega32U4/STM32 boards (most keyboards):

Terminal window
qmk flash -kb boardsource/5x12 -km my-colemak

This 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?

Terminal window
sudo qmk flash -kb boardsource/5x12 -km my-colemak

Or 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:

  1. Put keyboard in bootloader mode (RESET button or QK_BOOT key)
  2. A drive named RPI-RP2 appears
  3. Compile the firmware:
    Terminal window
    qmk compile -kb boardsource/equals/60 -km my-keymap
  4. Copy the .uf2 file to the RPI-RP2 drive:
    Terminal window
    cp .build/boardsource_equals_60_my-keymap.uf2 /media/$USER/RPI-RP2/
  5. The keyboard reboots automatically after the file copies

The .uf2 file is in .build/ after compilation.


Enable debug printing in rules.mk:

CONSOLE_ENABLE = yes

Add 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:

Terminal window
qmk console
SymptomCauseFix
Key does nothingWrong keycode or transparent key with no lower layerCheck keycode, use XXXXXXX to explicitly disable
Wrong symbol appearsUsed numpad key (KC_KP_*) instead of main keyboard keyUse KC_MINUS, KC_EQL, etc.
Layer won’t activateLayer index mismatch or wrong MO() valueVerify enum numbers match layer definitions
VIA doesn’t detectVIA_ENABLE missing or not recompiledAdd to rules.mk, recompile, reflash
Vial doesn’t detectMissing vial.json or wrong UIDCheck VIAL_KEYBOARD_UID in config.h, verify vial.json exists
Compile fails after git pullSubmodules out of syncRun make git-submodule again
vial-qmk compile errorUsing qmk compile instead of makeUse make keyboard:keymap with vial-qmk

Terminal window
# Standard QMK (qmk_firmware)
qmk compile -kb boardsource/5x12 -km 5x12-mod-dh-erfi
qmk flash -kb boardsource/5x12 -km 5x12-mod-dh-erfi
# VIA-enabled (qmk_firmware)
qmk compile -kb boardsource/5x12 -km via-colemak-mod-dh-erfi
qmk flash -kb boardsource/5x12 -km via-colemak-mod-dh-erfi
# Vial-enabled (vial-qmk)
cd ~/vial-qmk
make boardsource/5x12:vial
make boardsource/5x12:vial:flash

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:

Terminal window
# Standard QMK (qmk_firmware)
qmk compile -kb boardsource/equals/60 -km colemak-mod-dh-erfi
cp .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-erfi
cp .build/boardsource_equals_60_via-colemak-mod-dh-erfi.uf2 /media/$USER/RPI-RP2/
# Vial-enabled (vial-qmk)
cd ~/vial-qmk
make boardsource/equals/60:vial
cp .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.


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.

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.

# In rules.mk
RGBLIGHT_ENABLE = yes

Then use in keymap:

RGB_TOG // Toggle RGB on/off
RGB_MOD // Next mode
RGB_RMOD // Previous mode
RGB_HUI // Hue +
RGB_SAI // Saturation +
RGB_VAI // Brightness +

Terminal window
# QMK Setup
git clone https://github.com/qmk/qmk_firmware.git
cd qmk_firmware
python -m venv venv && source venv/bin/activate
pip install qmk
make git-submodule
# Vial Setup (separate from QMK)
cd ~
git clone https://github.com/vial-kb/vial-qmk.git
cd vial-qmk
make git-submodule
# List keyboards
qmk list-keyboards | grep -i boardsource
# Compile (QMK/VIA)
qmk compile -kb <keyboard> -km <keymap>
qmk flash -kb <keyboard> -km <keymap>
# Compile (Vial)
cd ~/vial-qmk
make <keyboard>:vial
make <keyboard>:vial:flash
# Clean build artifacts
qmk clean # For QMK
make clean # For Vial
# View keyboard info
qmk info -kb <keyboard>

QMK:

VIA:

Vial (Open Source):


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 = yes in rules.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 = yes and VIAL_ENABLE = yes in rules.mk
  • Using vial-qmk fork (not standard qmk_firmware)
  • VIAL_KEYBOARD_UID set in config.h
  • vial.json exists in keymap directory
  • Keymap named exactly vial (not via or anything else)
  • Using make commands, not qmk compile
  • Unlock combo defined (or VIAL_INSECURE set)

For additional help, see QMK Discord.

CheckHowExpected
The toolchain works before you touch a keymapqmk compile -kb planck/rev6 -km default after make git-submodulecompiles with no errors; a missing submodule fails on lib/lufa/LUFA/makefile
Your keymap compilesqmk compile -kb boardsource/5x12 -km my-colemakno errors before flashing
Vial firmware buildsmake boardsource/5x12:vial from the vial-qmk rootno errors; qmk compile against vial-qmk is the wrong entry point
VIA sees the boardflash, then open usevia.appdetected automatically; if not, VIA_ENABLE = yes is in rules.mk and the firmware was rebuilt
Vial sees the boardflash, then open vial.rocks or the desktop appdetected with no JSON sideload; a miss points at VIAL_KEYBOARD_UID or vial.json
Key presses arriveCONSOLE_ENABLE = yes in rules.mk and qmk consolePressed: 0x.... lines as keys are hit
The userspace is validqmk userspace-doctor in the containerUserspace enabled: True; a schema error names the bad build_targets entry
The intended keys are in the firmwarearm-none-eabi-gdb -batch -ex 'x/12hx (char*)&keymaps+<n*120+96>' <elf>the keycodes you expect on that row
  • Do not mix the two QMK CLI installs. --user inside a venv fails with Can 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 from KC_MINUS and KC_EQL.
  • VIA needs at least four layers in firmware. Empty layers filled with _______ count.
  • Vial needs both VIA_ENABLE and VIAL_ENABLE, and its keymap directory must be named exactly vial.
  • vial.json is not QMK’s info.json; it is Vial’s own layout definition.
  • make, not qmk compile, with vial-qmk.
  • VIAL_INSECURE disables the unlock-combo protection.
  • RP2040 flashing is a file copy. The .uf2 is left in .build/ and dropped onto the RPI-RP2 drive that appears in bootloader mode.
  • Userspace keymap names are [0-9a-z_] only. Hyphens fail the qmk.json schema.
  • 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_NEXT and RM_PREV in quantum/keycodes.h; RGB_TOG survives only in the legacy keycode data, so a 2024 keymap needs the new names.
  • The qmk_cli image has no Node.js, so a CI job running inside it cannot use JavaScript actions.
FileWhat it is
keyboards/<vendor>/<model>/keymaps/<keymap_name>/keymap.cThe keymap: the layer enum, the LOWER / RAISE defines and the keymaps[][] array
rules.mkFeature flags in the keymap directory: VIA_ENABLE, VIAL_ENABLE, CONSOLE_ENABLE, TAP_DANCE_ENABLE, RGBLIGHT_ENABLE
config.hVial only: VIAL_KEYBOARD_UID and the VIAL_UNLOCK_COMBO_ROWS / VIAL_UNLOCK_COMBO_COLS pair
vial.jsonVial only: the physical layout the Vial UI draws, keyed on matrix positions
.build/<keyboard>_<keymap>.uf2The RP2040 build output, copied to the RPI-RP2 drive
the qmk_firmware and ~/vial-qmk clonesThe two checkouts; vial-qmk must not be nested inside qmk_firmware
qmk.jsonExternal Userspace only: userspace_version and the build_targets list
.forgejo/workflows/build.ymlExternal Userspace only: the CI build and rolling release