Running kanata (keyboard remapping software)

Description

This is a guide for running [kanata | github.com/jtroo/kanata] on QubesOS. This might work for other keyboard remappers, but it wasn’t tested.

Note: This guide was written for kanata v1.9.0
Note: This guide assumes that you have a separate USB Qube and that it’s disposable

:warning: To prevent conflicts use linux-dev-names-include in your kanata configuration (see: [kanata v1.9.0 docs | github.com])

Overview

  • Create a new template for kanata
  • Configure kanata in the template
  • Set the template to be the base of sys-usb

1. Installing kanata

  • Create a new AppVM based on fedora-41-xfce and name it sys-usb-dvm
  • Obtain a kanata binary (either build it yourself or download it from [their GitHub releases | github.com]) and copy it to sys-usb-dvm
  • sys-usb-dvm: Copy the received binary to ~/kanata/ (create if needed)

2. Configuring kanata

  • sys-usb-dvm: Copy your kanata config to ~/kanata/kanata.cfg
  • sys-usb-dvm: Create the following files:
File ~/kanata/kanata-exclusions.conf
[Service]
ExecStart=
ExecStart=/usr/local/bin/launch-input-sender.py qubes.InputKeyboard /dev/input/%i "$TARGET_DOMAIN"
File ~/kanata/kanata.service
[Unit]
Description=Improve keyboard comfort and usability with advanced customization

[Service]
Type=simple
ExecStart=/usr/local/bin/kanata -c /rw/home/user/kanata/kanata.cfg
Restart=on-failure
  • sys-usb-dvm: Add this to the bottom of /rw/config/rc.local:
rc.local
# === kanata configuration
# exclusion:
cp /rw/home/user/kanata/launch-input-sender.py /usr/local/bin/
chmod +x /usr/local/bin/launch-input-sender.py
mkdir /etc/systemd/system/qubes-input-sender-keyboard@.service.d/
cp /home/user/kanata/kanata-exclusions.conf /etc/systemd/system/qubes-input-sender-keyboard@.service.d/kanata-exclusions.conf

# kanata:
cp /rw/home/user/kanata/kanata /usr/local/bin/
chmod +x /usr/local/bin/kanata
cp /rw/home/user/kanata/kanata.service /etc/systemd/system/

systemctl daemon-reload
systemctl restart qubes-input-sender-keyboard@*
systemctl enable kanata
systemctl start kanata
# ===
  • Go to sys-usb. sys-usb: Run cat /proc/bus/input/devices. You should get something like this:
...

I: Bus=0003 Vendor=REDACTED Product=REDACTED Version=0110
N: Name="REDACTED Keyboard"
<more lines>
...
  • Find all the keyboards you want to be remapped and save their Vendor and Product values (if the same keyboard is listed multiple times, save them all)
  • Go to sys-usb-dvm. sys-usb-dvm Create ~/kanata/launch-input-sender.py with the following contents (don’t forget to replace VENDOR and PRODUCT at the top):
File ~/kanata/launch-input-sender.py
#!/usr/bin/env python3

# Put your keyboard ids here
KEYBOARDS = [
  "VENDOR:PRODUCT"
]

QUBES_INPUT_SENDER = "/usr/bin/qubes-input-sender"

import os
import subprocess
import sys

_, arg_rpc, arg_input, arg_domain = sys.argv

with open("/proc/bus/input/devices") as f:
    deviceinfo = f.read()

devices = deviceinfo.split("\n\n")
inputs = []
keyboards_not_found = KEYBOARDS.copy()

def get_infoline(infolines, i):
    for line in infolines:
        if line.startswith(i+":"):
            return line

for device in devices:
    infolines = device.split("\n")
    I_line = get_infoline(infolines, "I")
    if not I_line: continue

    vendor_pos = I_line.find("Vendor=")
    product_pos = I_line.find("Product=")
    vendor = I_line[vendor_pos+7:vendor_pos+11]
    product = I_line[product_pos+8:product_pos+12]
    if f"{vendor}:{product}" not in KEYBOARDS: continue

    S_line = get_infoline(infolines, "S")
    sysfs_pos = S_line.find("Sysfs=")
    sysfs = S_line[sysfs_pos+6:]
    sysfs_path = f"/sys{sysfs}"
    sysfs_files = os.listdir(sysfs_path)
    event_files = list(filter(lambda i: i.startswith("event"), sysfs_files))
    inputs += event_files

    if f"{vendor}:{product}" in keyboards_not_found:
        keyboards_not_found.remove(f"{vendor}:{product}")
    print(f"INFO: Found keyboard {vendor}:{product} with {len(event_files)} events: {' '.join(event_files)}")

input_paths = list(map(lambda i: f"/dev/input/{i}", inputs))

if arg_input in input_paths:
    print("INFO: Not launching input-sender")
    while True: pass


print(f"INFO: Launching {QUBES_INPUT_SENDER}")

os.execvp(QUBES_INPUT_SENDER, [QUBES_INPUT_SENDER, arg_rpc, arg_input, arg_domain])

3. Changing the template of sys-usb

  • Shut down sys-usb-dvm
  • dom0: Make sys-usb-dvm a disposable template (“Qube Manager” > “sys-usb-dvm” > “Settings” > “Advanced” > “Other” > “Disposable template”)
  • Change the template of sys-usb:
    • :warning: This might lock you out of the system. Find a PS/2 keyboard to prevent that
    • dom0: qvm-shutdown --wait sys-usb; qvm-prefs -s sys-usb template sys-usb-dvm; qvm-start sys-usb

Troubleshooting

  • To launch a shell in usb-sys-dvm after it’s been made into a disposable template use dom0: qvm-run sys-usb-dvm xfce4-terminal
  • To revert sys-usb use dom0: qvm-shutdown --wait sys-usb; qvm-prefs -s sys-usb template default-dvm; qvm-start sys-usb