Contextual QMK layer switching based on X11 windows

I’m a big fan of having an efficient workstation that encourages flow, so every hiccup in that flow really stands out.

After experimenting a lot with my ErgoDox configuration, I’ve found that it’s really nice to have a few application-specific layers for smaller keyboards. For reference, this is what my layers currently look like:

In order to access any of the application-specific layers, I switch to the layer 4, and press keys on that layer to switch to an application. As you might imagine, when frequently switching between applications with different layouts, this is a waste of time. In fact, given how many times I’ve died in CS:GO because I tabbed back in on a weird layer, I’d say this is an issue of life or death.

My solution to this problem is pretty simple:

  1. Track application focus.
  2. Switch to the appropriate layers.

Setting up the keyboard

To communicate between from the computer to the keyboard, we’ll use raw HID, which QMK supports nearly out of the box. All that’s required to enable this feature is to set the following in your keyboard firmware’s rules.mk:

RAW_ENABLE = yes

You’ll also want to look in your config.h to find the values for RAW_USAGE_PAGE and RAW_USAGE_ID. Here’s what mine looked like:

#define RAW_USAGE_PAGE 0xFF60
#define RAW_USAGE_ID 0x61

Then, in your keymap.c, add the following:

#include "raw_hid.h"

/* ... */

void raw_hid_receive(uint8_t *data, uint8_t length) {
    if (length == 3) {
        uint8_t layer      = data[1];
        uint8_t layer_mask = data[2] | (1 << layer);
        if ((layer_state & layer_mask) == 0)
            layer_move(data[1]);
    }
}

The raw HID events sent to the device will contain both a destination layer and a “protected layer mask,” which specifies certain layers to which the keyboard should give priority. In this way, you can tell the keyboard “switch to layer 5, unless layers 2, 3, or 4 are enabled” without requiring any sort of state or bidirectional communication.

Finally, flash your device with the newly compiled firmware.

Appeasing udev

If you’re using udev, you’ll need a rule to allow you to access raw HID as a user. Here’s an example for an ErgoDox, in /etc/udev/rules.d/50-ergodox.rules:

KERNEL=="hidraw*", ATTRS{busnum}=="3", ATTRS{idVendor}=="3297", ATTRS{idProduct}=="4976", MODE="0666"

Reload udev. If this doesn’t work, try plugging your device in again.

Setting up the software

Next, we need to set up the software on the computer. Currently, this requires the usage of X11, though a Wayland port of this software would not be too difficult.

A few dependencies are required:

Clone the repository, and build it using Cargo.

Before you run the resultant layerizer binary, you’ll need a config! You can either put it in a place like ~/.config/layerizer/config.toml, or specify the path with the --config argument.

Here’s an example for an ErgoDox, using the HID usage page and ID we found before:

# Vendor and product IDs of the device.
vendor_id = 0x3297
product_id = 0x4976

# HID usage page and ID of the device.
usage_page = 0xFF60
usage_id = 0x61

# The layer to switch to by default.
default_layer = 0

# Layers which have higher priority over application rules.
protected_layers = [1, 2, 3]

# Application rules.
#
# Can be selected by class and/or name, with regex support for both.
# You may also specify additional protected layers for each entry.
rules = [
    { class = "gzdoom", layer = 5 },
    { class = "csgo_linux64", layer = 5 },
]

Final thoughts

Hopefully you were able to make it this far! If not, please do open an issue on the repository and let me know.

This project isn’t quite complete yet, so I’d love to have help. Future work might include: