Multiple keyboards, multiple layouts

If you have more than one (physical) keyboard connected to your Fedora box, with different layouts, you can load multiple key-maps at the same time.

Yes, technically, Gnome doesn’t permit this. The switcher in the GUI will reset you to one keymap whenever you log in, switch users, or put the machine to sleep. But, for a little inconvenience, you can get your layouts to work in parallel.

What I use is a little script named k that lives in /usr/local/bin. It’s a pretty simple script, once you work out what it needs to do.

How X does keys

Your keyboard connects up through a kernel device driver — almost certainly, the USB or “AT” (as in, the IBM PC/AT that introduced the round DIN keyboard socket) interface. This gets translated into a character device special file that programs (eg, X) can read from; perhaps something like /dev/input/event6.

X connects this character device special file to its XInput layer as one input device, and gets from it scancodes — more-or-less, these are codes that mean something like “the fourth key on the second row,” but don’t carry any symbolic meaning. XKB then translates these into keyboard symbols, like “x” or “b” or “Right Shift” or “Control + System Request.” (If you use a more complex writing-system, another level of translation might be in play, as well; perhaps converting Pin1Yin1 sequences into Han characters, or stacking Jamo into syllable blocks … but, you already knew about that, if you speak Chinese, Japanese, or Korean.)

Normally, Gnome sets XKB to use the same mapping for all connected keyboards. We’re going to break that.

In my case, I use a highly customized modified USB Dvorak keyboard, but I want to leave the internal keyboard in my laptop and the PS/2-connected alternate keyboard on my desktop machine as “normal” US Qwerty layouts.

Gathering the pieces together

First, you need to know the X keymap that you want as your base. Go through the Settings panel Region & Language → Input Sources to set it up as you would normally; then, extract its internal code name with (from a Terminal) — gsettings get org.gnome.desktop.input-sources sources — you’ll see a result like [(‘xkb’, ‘us’)] — the code you want here is ‘us’. Repeat for your other keyboard’s map (my other keyboard is type spacey, which you’ll see below).

Then, let’s identify your keyboards’ ID’s at the XInput level — this is where the physical keys’ signals are coming in, before XKB assigns them to key symbols. Just run xinput (again, from a terminal) and you’ll see something with a “Virtual core keyboard” and a set of “slave keyboards” under it. If you have a keyboard on a PS/2 or AT connector, you’ll probably see something like “AT Translated Set 2 keyboard;” USB keyboards are just, “USB Keyboard.” (You’ll probably also see video devices, laptop hot-keys, or other “keyboard-like” devices listed, which you can totally ignore for this purpose.)

To clarify which keyboard is which, you might use xinput list-props nn to see the details of a particular device. EG: if you have two USB keyboards, one of which is 9 and the other is 13, xinput list-props 9 might show that one of them has Device Node (270): “/dev/input/event8” … and find /sys/devices -name event8 might give you a beautiful string like /sys/devices/pci0000:00/0000:00:14.0/usb1/1-13/1-13.2/1-13.2:1.0/0003:04D9:0169.000B/input/input28/event8 (OK, maybe it’s not so beautiful.) The “nice” thing about that is that you can lsusb to find the brand name of your keyboard and match it up to the numbers in the middle of that sequence — eg. “Bus 001 Device 085: ID 04d9:0169 Holtek Semiconductor, Inc.” If you have two keyboards with the same model, you could have a problem here …

Building the script

So, on to the script. There are three steps we’ll need to perform:

  1. Set the default key map for all devices;
  2. Locate the keyboard XInput device ID(s) of the one(s) that will be different;
  3. Change only its (their) keymap(s).

The second step, unsurprisingly, is the “hard part.”

The script, with step 2 skipped, looks something like

#!/bin/bash
setxkbmap 'us'
for device in $(TODO MAGIC)
do
   setxkbmap -device $device 'spacey'
done

So … what goes into TODO MAGIC … ?

If your keyboards are visibly distinct in the xinput list, you could do something like:

⇒ xinput -list | grep ‘USB Keyboard’

… to locate the one you want. This will give you a small list, like:

↳ USB Keyboard id=12 [slave keyboard (3)]
↳ USB Keyboard id=13 [slave keyboard (3)]

(Note that these are both the same physical keyboard, in my case.)

If that’s what you wanted, you can trim the results with

… | cut -d= -f2 | cut -f1

which, all together, gives us

⇒ xinput -list | grep ‘USB Keyboard’ | cut -d= -f2 | cut -f 1
12
13

… Just what we wanted for the for loop.

#!/bin/sh
setxkbmap us
for device in $(xinput -list | grep 'USB Keyboard' | cut -d= -f2 | cut -f 1)
do
 setxkbmap -device $device spacey
done

The above is more-or-less identical to what I use.

Multiple USB keyboards?

If you need to do more work to narrow down the device, you might have to wrangle the USB vendor/product ID’s into play.

usbid=04D9:0169
for device in $( xinput … )
do
    dev=$(xinput list-props $device | grep 'Device Node' | cut -d: -f2)
    devpath=$(find /sys/devices -name $(basename $dev))
    case "$devpath" in
       *"$usbid"*)
          setxkbmap -device $device spacey
          ;;
    esac
done

This is using a relatively obscure shell feature: case “$devpath” in *”$usbid”*) … ;; esac is checking whether the usbid string exists within the (longer) devpath string. You could think of this as a way of asking, “if devpath contains the substring usbid.” (If you’re unfamiliar with Bourne shell, note that esac is case spelled backwards. In the same way, the end of an if block is fi. do, however, ends with done.)

Gnome Session

In order to make life a little more pleasant, I have also added a session script to run k whenever I log in.

In ~/.config/autostart/keyboard I have:

[Desktop Entry]
Version=1.0
Type=Application
Name=Set keyboard maps
GenericName=Set keyboard maps
Comment=Set keyboard maps
Icon=keyboard
Categories=System;
Exec=k
TryExec=k
Terminal=false
X-GNOME-Autostart-Delay=4

And, likewise, for GDM:

 ⇒ sudo su - gdm -s /bin/bash
-bash-4.3$ mkdir -p ~/.config/autostart
-bash-4.3$ cat >  ~/.config/autostart/keyboard.desktop
[Desktop Entry]
Version=1.0
Type=Application
Name=Set keyboard maps
GenericName=Set keyboard maps
Comment=Set keyboard maps
Icon=keyboard
Categories=System;
Exec=k
TryExec=k
Terminal=false
X-GNOME-Autostart-Delay=4
^D^D
-bash-4.3$ logout

I do still have to run this manually, eg, after switching users or the machine sleeping. Thus, the immensely short name “k,” so I’m not fumbling around trying to enter a long name on the wrong keymap.

PS.

My actual k script has these two lines to set key repeats properly, due to deficiencies in my spacey key map:

xset r 66 ; xset r 105

Maybe some day I’ll fix the keymaps, but, for now, this works for me.

What do you think? Add your comments →