zmk-layer-hud

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.

Heat
Keys

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

Set up your keyboard ↓ The source, on GitHub

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.

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.

ChannelCarriesNeeds
the module's own (CDC-ACM over USB)the active layers, and each key press and release by positionthe tty: the udev rule on Linux, nothing on macOS
the keyboard's HID reportswhat you type, and the modifiersInput 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.

  1. 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: config

    Put 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-uart adds one for Studio. Name it on the board's entry in build.yaml when GitHub Actions builds the firmware, or pass -S layer-hud-usb-uart to a local west build:

    include:
      - board: nice_nano_v2
        shield: corne_left
        snippet: layer-hud-usb-uart   # <-- new

    Then build and flash the central half, or the dongle.

  2. The host

    curl -fsSL https://raw.githubusercontent.com/rafaelromao/zmk-layer-hud/main/install.sh | sh

    That puts the tree in ~/.local/share/zmk-layer-hud and runs zmk-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.

  3. 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 login

    To 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 doctor checks 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 --debug

    prints {"kind":"layers","ids":[]} within two seconds, and a layer's id while you hold its key.

Every step, and why: docs/zmk-setup.md.

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 --reserve tiles them beside it instead, for recording. On Omarchy, zmk-layer-hud menubar enable puts 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 feed serves 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.

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-keys drops 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.