The layer you're on, and every key you press
An on-screen HUD for ZMK keyboards. It shows the layer you are on and lights the keys, combos and macros as you press them, drawn from your ZMK keymap, or from the keymap-drawer file you document your layout with. The keyboard itself reports its layers and key positions, so nothing is guessed.
The words per minute are yours; the keys that light are where each character lives on this layout. Session shows everything typed since the board was chosen. What you type stays in this page: nothing is sent or kept.
On macOS, or Linux on Hyprland:
curl -fsSL https://raw.githubusercontent.com/rafaelromao/zmk-layer-hud/main/install.sh | sh
Drawn from your keymap
import reads your zmk-config once and draws the board with
keymap-drawer: every layer and key,
the combos, and what a drawing leaves out, the id of each ZMK layer and the layers a combo
really fires on. Keep a keymap-drawer YAML for your keymap's diagram, glyphs and key sizes
included? Name it in the config and the board is that one instead.
What import draws goes in a file beside the config, and the HUD reads that and the config and
nothing else: no keymap, no keymap-drawer and no network while it runs. sync draws
it again after you push, and sync --watch each time you save.
zmk-layer-hud import github.com/you/zmk-config
zmk-layer-hud sync --watch
The config can live in that repo too. After zmk-layer-hud config link path/to/config.yaml,
git pull is the whole of setting up a second machine.
Nothing is guessed
An overlay that watches the operating system sees keycodes, not layers: &mo,
&sl and &tog send none, and a combo arrives as whatever key it
types. Here the keyboard says it. A small ZMK module sends the active layers, and with
positions; each key's press and release by position, on a channel of its own.
- Every way of switching layers is reported:
&mo,<,&sl,&to,&tog, conditional layers, auto-layers, other modules. No binding changes. - Nothing rides in the keyboard report. A signal carried there reaches the operating system as a key, and on Linux arrives as phantom key presses; on a channel of its own nothing can be mistaken for one.
- The module drops a frame rather than wait for one, so it never delays a keystroke. The next layer change, or the heartbeat, puts the HUD right.
- Flash the central half, or the dongle. Peripherals need nothing.
| Channel | Carries | Needs |
|---|---|---|
| the module's own (CDC-ACM over USB) | the active layers, and each key press and release by position | the tty: the udev rule on Linux, nothing on macOS |
| the keyboard's HID reports | what you type, and the modifiers | Input Monitoring on macOS; on Linux, the same udev rule |
Lose the first and the board stops following you; lose the second and the strip stays empty
while everything else works. zmk-layer-hud status says which one is live, and
zmk-layer-hud doctor says why when one is not.
What it draws
The exact key
Lit by its position while it is held, whatever it produced: a chord, a macro, a modifier or a layer key.
Combos
Keys pressed within the combo term become the combo the drawer draws, pill and all, on the layers it really fires on.
Layers as they are
Held, toggled or one-shot. A one-shot layer stays up through its key's flash, and transparent keys resolve through the live stack.
Holds
A layer's activator and a held modifier are ringed, and Shift capitalises the legends.
Macros
Keys typed back to back that spell a legend light the key or the combo that has it.
What was typed
A strip under the board shows the characters, with dead keys composed.
Heat
A pressed key glows and cools over a few seconds; one struck again and again stays warm.
Speed
A bar above the panel shows words per minute, the session's average and top speed, accuracy, combo share, and the share of the layer on screen.
Sessions
There is always one, kept like a tmux session: name it, save it, load it back, and see its heatmap, every stat on the bar and each layer's share of the keys. Counts only, never what was typed, in files only you can read.
Off screen, still counting
Hide it from its own button, its menubar icon, Ctrl+Alt+L or
zmk-layer-hud hide, and it goes on counting, the live WPM beside the icon;autostart enablestarts it that way at every login, so the session begins with the day's first keystroke.
Setup
-
The firmware
Add the module to your zmk-config's
config/west.yml:manifest: remotes: - name: zmkfirmware url-base: https://github.com/zmkfirmware - name: rafaelromao # <-- new url-base: https://github.com/rafaelromao # <-- new projects: - name: zmk remote: zmkfirmware revision: main import: app/west.yml - name: zmk-layer-hud # <-- new remote: rafaelromao # <-- new revision: main # <-- new self: path: configPut the node anywhere at the root of your keymap:
/ { layer_signal { compatible = "zmk,layer-signal"; heartbeat-ms = <2000>; // re-send the layers every 2 s, so a HUD started late catches up positions; // also report every key press by position }; };Over USB the signal needs a serial interface of its own, which the module's snippet adds, the way ZMK Studio's
studio-rpc-usb-uartadds one for Studio. Name it on the board's entry inbuild.yamlwhen GitHub Actions builds the firmware, or pass-S layer-hud-usb-uartto a localwest build:include: - board: nice_nano_v2 shield: corne_left snippet: layer-hud-usb-uart # <-- newThen build and flash the central half, or the dongle.
-
The host
curl -fsSL https://raw.githubusercontent.com/rafaelromao/zmk-layer-hud/main/install.sh | shThat puts the tree in
~/.local/share/zmk-layer-hudand runszmk-layer-hud setup, which prepares the machine: the packages, a virtualenv, a config to start from, and the command on your PATH. Nothing runs as root without printing the command and asking first. -
Your keymap
zmk-layer-hud import github.com/you/zmk-config # draws your keymap: layers, ids, combos zmk-layer-hud start zmk-layer-hud autostart enable # optional: start it hidden at every loginTo draw a keymap-drawer YAML of your own instead, name it as
keymap:in the config first (zmk-layer-hud config edit). The panel opens, and within two seconds it follows your keyboard. When it does not,zmk-layer-hud doctorchecks the interpreter, the packages, the config, the permissions and both channels, and names what is missing. The channel alone:zmk-layer-hud feed --stdout --no-ws --no-keymap --no-keys --debugprints
{"kind":"layers","ids":[]}within two seconds, and a layer's id while you hold its key.
Every step, and why: docs/zmk-setup.md.
Runs on
- macOS
- An overlay panel over every Space and full-screen app. Drag it anywhere; it stays where it was put. A menubar icon with the live, average or top WPM shows, hides or starts it, and so do ⌃⌥L and ⌃⌥⌘L.
- Linux on Hyprland
- A layer-shell overlay that takes no room from your windows;
drag it anywhere.
start --reservetiles them beside it instead, for recording. On Omarchy,zmk-layer-hud menubar enableputs the same icon in the bar, its stats a hover away, and the same shortcuts are Hyprland binds (Super for ⌘). On pacman systems the GTK packages are installed for you. - Anything else
zmk-layer-hud feedserves every message on a local WebSocket (docs/protocol.md). Windows is not supported.
It needs Python 3.10 or newer, and a ZMK keyboard whose firmware you build.
Hack on it
- The protocol. The feed's WebSocket carries the keymap, the layers, the positions and
the keys, replays the last keymap, layers and device to a new client, and takes layers,
positions and keys in.
zmk-layer-hud pokeis a client of it. - Demos. A demo script is a JSON file of frames, text and pauses.
zmk-layer-hud demo --playplays one live, anddocs/make-gif.shrenders one to a GIF. The demos on this page are those scripts. - Tests.
make testruns the firmware's wire policy in C, the host in Python, and the HUD page and this one under node. No npm, nothing to build.
Limits and questions
- How many layers and keys?
- Layer ids 0 to 31, and key positions 0 to 255.
- What does it read, and where does it go?
- Layers and positions come from the
module. What you type comes from the keyboard's HID reports and feeds the strip; on macOS
that needs Input Monitoring, and
--no-hid-keysdrops both the strip and the permission. While macOS says a password is being typed, the HUD shows and counts nothing of it. Sessions keep counts, never what was typed, and nothing leaves the machine. - Does it get in ZMK Studio's way?
- No. The feed tries each serial port the board exposes and moves on from one that says nothing, so a Studio or logging port beside the signal is left alone.
- Bluetooth?
- Yes: over Bluetooth the signal rides the module's own GATT service. Pair the keyboard with the computer first; on macOS the feed finds a keyboard that is already connected, with no address to give. A dongle is USB, and works too.
- Which ZMK?
- The module follows ZMK
main; there is no tagged release to pin yet.