Why global keyboard remapping can be bad

In an earlier post, I blogged about remapping keys on a keyboard for better typing comfort. The change was to swap the Control and Windows keys on a CM Storm Stealth USB keyboard.

CM Storm Stealth keyboard

Using the "partial fist" method I described in my prior post, the pinky ends up hitting the Windows key, both with left and right pinkies. And because I use vim heavily, I want the control key at this position. The CM Storm keyboard is nice because its placement of the Control and Window keys are symmetrical in relation to the hands on the home row of the keyboard.

The xmodmap mappings described in my earlier post works fine to swap the Control and Windows keys, but because it changes the mappings globally, it remaps the Control and Windows keys on all attached keyboards. I use a Thinkpad X201 notebook, and swapping its Control and Windows keys is not desirable.

Thinkpad X201 keyboard

The position of the Control keys on the Thinkpad keyboard is already ideal according to my preference. So what I really want is a method to change the mappings for only one keyboard device attached to the computer, and not all of them.

How to implement device specific remapping

xkb allows per-device mapping

The solution is to ditch the xmodmap solution presented before and instead use the capabilities of xkb. Using the Xorg evdev input driver, it is possible to configure different settings for different input devices. For keyboards, we can therefore select different xkb options.

The configuration changes describe here are available in the cmstorm repository.

Step 1 -- Create a new xkb option

I first created a new xkb option. New file /usr/share/X11/xkb/symbols/ctrlwin contains the option named ctrlwin:swap_ctrl_win:

// Swap the Ctrl and Win keys.
partial modifier_keys
xkb_symbols "swap_ctrl_win" {
    key <LWIN> { [ Control_L ] };
    key <LCTL> { [ Super_L ] };
    key <RWIN> { [ Control_R ] };
    key <RCTL> { [ Super_R ] };
};

Step 2 -- Reference the new option so it can be used

The new option should show up in the proper reference files to be usable. Specifically, the evdev and evdev.lst files, found in the /usr/share/X11/xkb/rules directory, at least on Ubuntu 14.04.

Here's the patch for evdev.

--- evdev.orig  2014-01-15 07:42:33.000000000 -0700
+++ evdev   2015-01-30 20:20:08.708389769 -0700
@@ -971,6 +971,7 @@
   altwin:hyper_win =   +altwin(hyper_win)
   altwin:alt_super_win =   +altwin(alt_super_win)
   altwin:swap_alt_win  =   +altwin(swap_alt_win)
+  ctrlwin:swap_ctrl_win    =   +ctrlwin(swap_ctrl_win)
   grp:switch       =   +group(switch)
   grp:lswitch      =   +group(lswitch)
   grp:win_switch   =   +group(win_switch)

Here is the patch for evdev.lst.

--- evdev.lst.orig  2014-01-15 07:42:33.000000000 -0700
+++ evdev.lst   2015-01-30 20:20:20.976390083 -0700
@@ -800,6 +800,8 @@
   altwin:hyper_win     Hyper is mapped to Win-keys
   altwin:alt_super_win Alt is mapped to Right Win, Super to Menu
   altwin:swap_alt_win  Alt is swapped with Win
+  ctrlwin              Ctrl/Win key behavior
+  ctrlwin:swap_ctrl_win Ctrl and Win keys are swapped
   Compose key          Position of Compose key
   compose:ralt         Right Alt
   compose:lwin         Left Win

With these changes, one can activate the option on a given X keyboard input device to swap the Control and Windows keys. Something like this:

setxkbmap -device 10 -option ctrlwin:swap_ctrl_win

where 10 is the id of the device. One can see all the X input devices by running at a shell prompt:

xinput -list

To de-activate the option, issue the command again but with "" as the option argument. Or, since this device is a USB keyboard, simply unplug it and then plug it back in. At this point, we have shown the option works correctly (or not!) but the change is not persistent.

Step 3 -- Set the new option for the appropriate devices

In Ubuntu 14.04, /usr/share/X11/xorg.conf.d/ contains a set of files used for the configuration of X. Add the new file /usr/share/X11/xorg.conf.d/90-evdev-CM-Storm.conf:

Section "InputClass"
    Identifier "CM Storm keyboard"
    MatchIsKeyboard "on"
    MatchDevicePath "/dev/input/event*"
    Driver "evdev"
    MatchUSBID "2516:0017"
    Option "XkbOptions" "ctrlwin:swap_ctrl_win"
EndSection

This file defines an InputClass that overrides the default for keyboards defined in 10-evdev.conf. It is applicable for devices that are keyboards, whose device path starts with /dev/input/event, and shows USB VID:PID is 2516:0017. The CM Storm keyboard has this VID:PID, although it's hard to track it down via lsusb. Here is the lsusb output on my Thinkpad:

Bus 002 Device 002: ID 8087:0020 Intel Corp. Integrated Rate Matching Hub
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 080: ID 17ef:4816 Lenovo
Bus 001 Device 088: ID 1366:0101 SEGGER J-Link ARM
Bus 001 Device 087: ID 046d:c52f Logitech, Inc. Unifying Receiver
Bus 001 Device 086: ID 1a40:0101 Terminus Technology Inc. 4-Port HUB
Bus 001 Device 085: ID 1fc9:0083 NXP Semiconductors
Bus 001 Device 084: ID 1058:1110 Western Digital Technologies, Inc.
Bus 001 Device 083: ID 1a40:0201 Terminus Technology Inc. FE 2.1 7-port Hub
Bus 001 Device 082: ID 046d:0825 Logitech, Inc. Webcam C270
Bus 001 Device 092: ID 2516:0017
Bus 001 Device 079: ID 17ef:1005 Lenovo
Bus 001 Device 002: ID 8087:0020 Intel Corp. Integrated Rate Matching Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

But the Xorg log file has the information

$ grep "CM Storm.*Vendor" /var/log/Xorg.0.log.old | tail -1
[100514.151] (--) evdev: CM Storm Side print: Vendor 0x2516 Product 0x17

Update 2015-09-17: I purchased another one of these keyboards for a second location. I couldn't find a Stealth model with Cherry MX Brown key switches, so purchased instead the Rapid model. The only difference is the latter has the keycaps printed on the top face, as do most keyboards. It's USB VID:PID is also different: 2516:0004. So I've changed the MatchUSBID value in /usr/share/X11/xord.conf.d/90-evdev-CM-Storm.conf to use a wildcard entry to capture both keyboards: 2516:*.

Step 4 -- Log out and back in

X only reads the configurations in /usr/share/X11/xorg.conf.d on startup, so the X session needs to be restarted. The simplest way to do this is to log out and back in again. Now, upon detecting the CM Storm keyboard, its Control and Windows keys should be swapped without affecting any other keyboards attached to the system -- in my case the Thinkpad's built-in keyboard. Here's an Xorg snippet that shows the more specific InputClass definition working:

[101104.743] (II) config/udev: Adding input device CM Storm Side print (/dev/input/event14)
[101104.743] (**) CM Storm Side print: Applying InputClass "evdev keyboard catchall"
[101104.743] (**) CM Storm Side print: Applying InputClass "CM Storm keyboard"
[101104.743] (II) Using input driver 'evdev' for 'CM Storm Side print'
[101104.743] (**) CM Storm Side print: always reports core events
[101104.743] (**) evdev: CM Storm Side print: Device: "/dev/input/event14"
[101104.743] (--) evdev: CM Storm Side print: Vendor 0x2516 Product 0x17
[101104.743] (--) evdev: CM Storm Side print: Found keys
[101104.743] (II) evdev: CM Storm Side print: Configuring as keyboard
[101104.743] (**) Option "config_info" "udev:/sys/devices/pci0000:00/0000:00:1a.0/usb1/1-1/1-1.5/1-1.5.2/1-1.5.2:1.1/input/input60/event14"
[101104.743] (II) XINPUT: Adding extended input device "CM Storm Side print" (type: KEYBOARD, id 9)
[101104.744] (**) Option "xkb_rules" "evdev"
[101104.744] (**) Option "xkb_model" "pc105"
[101104.744] (**) Option "xkb_layout" "us"
[101104.744] (**) Option "xkb_options" "ctrlwin:swap_ctrl_win"

Line three of the output shows use of the new InputClass, and the last line of the output above shows the xkb option applied by that class.

Step 5 -- Swap the key caps on the keyboard

The CM Storm comes with a key cap puller, so it's trivial to swap the Control and Windows keys and have the key legends match with the new keyboard mapping.