I recently needed to remap keys from their default codes, using XKB configuration. I'll show a general solution to remap keys on specific keyboards, which I needed because I had separate "keyboards" connected where some keycodes from each keyboard overlapped, but corresponded to different things. For example the "power" key of the remote control (which presents itself as a USB keyboard to the system), generated the F9 keycode.

Supporting this involved a trifecta of obtuse configuration:

  1. xinput: I've needed many times to query the X input ID corresponding to a USB ID or udev input event. Currently custom scripts are required for this
  2. udev: There are many gotchas when writing udev scripts compounded by the fact that documentation has gotten a little out of date. At least in this case the udev script is simple and standard
  3. XKB: This takes the biscuit for undocumented obtuseness

Well I guess saying XKB is undocumented is a bit harsh, but others have found the documentation hard to find as it's sprinkled across the net. So the main reason for this post is to collate the following links I found when researching this:

The remapping script

I encapsulated parts 1. and 3. above in the following script, which is either run from your X startup script or from a udev rule to support hotplug after X has started.

# The remote control device (USB keyboard)
# has keys that overlap with standard keyboards.
# Therefore we map the offending keys here.

# re-execute this script as "$xuser" if that's not root,
in case the root user is not permitted to access the X session.
xuser=$(ps -C Xorg -C X -ouser=)
[ $(id -un) = root ] && [ "$xuser" != root ] && exec su -c $0 "$xuser"
export DISPLAY=:0

# First find the X input ID corresponding to the
# keyboard we're interested in.
kbd_id() {
  xinput list |
  sed -n "s/.*$keyboard.*id=\([0-9]*\).*keyboard.*/\1/p"
# In our case the USB:ID was shown in the `xinput list` output,
# but this is unusual and you may have to match
# on names or even correlate with /dev/input/by-id/*
remote_id=$(kbd_id 'face:1234')
[ "$remote_id" ] || exit

# Write out the XKB config to remap just
# the keys we're interested in
mkdir -p /tmp/xkb/symbols
cat >/tmp/xkb/symbols/custom <<\EOF
xkb_symbols "remote" {
    key <FK09>   { [ XF86PowerOff   ] };
    key <FK10>   { [ XF86Launch0    ] };
    key <FK11>   { [ XF86Launch1    ] };
    key <FK12>   { [ XF86Launch2    ] };
    key <HOME>   { [ XF86VendorHome ] };

# Amend the current keyboard map with
# the above key mappings, and apply to the particular device.
# Note xkbcomp >= 1.2.1 is needed to support this
setxkbmap -device $remote_id -print |
sed 's/\(xkb_symbols.*\)"/\1+custom(remote)"/' |
xkbcomp -I/tmp/xkb -i $remote_id -synch - $DISPLAY 2>/dev/null

Note the gotcha above, where you need a patch to support per device xbcomp, if your xkbcomp version is <= 1.2.0 which is the case on Fedora 14 at least.

Update: July 2016: This is also useful if you want to map a keyboard as dvorak, while not impacting the output from a yubikey:
kbd_id() {
  xinput list |
  sed -n "s/.*$keyboard.*id=\([0-9]*\).*keyboard.*/\1/p"
yubikey=$(kbd_id 'Yubikey')
[ "$yubikey" ] || exit

setxkbmap -device $yubikey -layout us

Related utilities

Useful commands I found while generating the above config were, xmodmap -pke to show the current keycode to keysym map, and xev | grep -Fi key to display the code for a pressed key.

I also noticed after doing the above, that evdev can remap keys at a lower level, which is detailed at the mythtv remote key remapping info. This involves adding syntax like Option "event_key_remap" "402=111 403=116" to xorg.conf. This is a less general method than XKB though, and probably alludes to the awkwardness of doing this with XKB.

Once the mapping is done, then you can refer to the keysyms in config if required. In my case I added None XF86PowerOff :Exec shutdown -h now to ~/.fluxbox/keys
© Jul 14 2011