July 17, 2021

Automatic xmodmap on keyboard connection

In continuation to my earlier post, this shows how I automated xmodmap with udev.

Description 🔗

Xorg keymap modifications done with xmodmap are keyboard specific. In addition, modifications are reset when keyboard is reconnected. I use USB switch to share most of my USB devices with multiple computers. I.e., devices are often connected and disconnected.

Single device connection may raise multiple udev add events. For example, attachment of ThinkPad USB keyboard raised in total 9 events. Udev rules and script executions are per event, and I wanted to limit xmodmap execution only once per attachment. Albeit, my xmodmap configuration is idempotent.

I had two options:

  1. Find so strict udev rule which appears only once per attachment
  2. Implement filtering of repeated events in script

I chose the latter option because it was simpler, more reliable, and script can easily reused with other similar scenarios.

Lastly, udev waits until script exits, before it continues. My script invokes xmodmap asynchronously to not block udev. Moreover, xmodmap invocation is delayed, to be sure keyboard is ready for the keymap modification.

Udev rule 🔗

Rule for Lenovo ThinkPad Compact Keyboard with TrackPoint.

File /etc/udev/rules.d/90-keyboard.rules:

1
ACTION=="add", ATTRS{idVendor}=="17ef", ATTRS{idProduct}=="6047", RUN+="/bin/udev-xmodmap.sh"

Udev script 🔗

This script is called for all udev add events. When the first events comes, it schedules xmodmap after 1 second. It ignores are following script calls during the next 5 seconds.

It creates and uses two external files:

  • /tmp/last-udev-xmodmap for timestamp of the last xmodmap execution
  • /tmp/last-udev-xmodmap.log for all output of xmodmap execution

File /bin/udev-xmodmap.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env bash
set -euo pipefail

export DISPLAY=":0"
export HOME=/home/{{ normal_user }}

min_seconds_between_executions=5
date_file="/tmp/last-udev-xmodmap"
now=$(date +%s)

if [[ -f $date_file ]]; then
    prev_ts=$(cat "$date_file")
else
    prev_ts=0
fi

if ((now - prev_ts <= min_seconds_between_executions)); then
    exit 0
fi

echo "$now" > "$date_file"

do_xmodmap() {
    sleep 1
    xmodmap $HOME/.Xmodmap
    notify-send "Xmodmap" "Keymap successfully activated"
}

do_xmodmap &> "${date_file}.log" &

Note, the example is actually a jinja2 template with normal_user variable.