26

im trying to load a new keyboard layout when I plug in a usb keyboard but my udev rule isnt working.

SUBSYSTEM=="input", ATTR{idVendor}=="062a", ATTR{idProduct}=="0201", GOTO="usb_xmodmap_auto"

LABEL="usb_xmodmap_auto"
ACTION=="add", RUN+="/usr/bin/xmodmap ~/.usbXmodmap"
ACTION=="remove", RUN+="/usr/bin/xmodmap ~/.pndXmodmap"

I have reloaded the rules using:

>sudo udevadm control --reload-rules

and by restarting the system but when I plug in the usb keyboard the orginal xmodmap is still loaded and thus the keyboard layout is wrong, but if i run the command in the terminal

>/usr/bin/xmodmap ~/.usbXmodmap
or
>/usr/bin/xmodmap ~/.pndXmodmap

they work just fine.

hope soneone can help.

Edit:

just to help more I ran some udevadm tests:

>udevadm test --action=add /devices/platform/ehci-omap.0/usb1/1-2/1-2.3/1-2.3:1.1/input/input10

outputs:

run_command: calling: test
udevadm_test: version 151
This program is for debugging only, it does not run any program,
specified by a RUN key. It may show incorrect results, because
some values may be different, or not available at a simulation run.

[...]
parse_file: reading '/etc/udev/rules.d/usb-keyboard.rules' as rules file
udev_rules_new: rules use 100572 bytes tokens (8381 * 12 bytes), 21523 bytes buffer
udev_rules_new: temporary index used 35380 bytes (1769 * 20 bytes)
udev_device_new_from_syspath: device 0x3b4d8 has devpath '/devices/platform/ehci-omap.0/usb1/1-2/1-2.3/1-2.3:1.1/input/input10'
udev_rules_apply_to_event: RUN '/sbin/modprobe -b $env{MODALIAS}' /etc/udev/rules.d/80-drivers.rules:5
udev_rules_apply_to_event: RUN 'socket:@/org/freedesktop/hal/udev_event' /etc/udev/rules.d/90-hal.rules:2
udev_rules_apply_to_event: RUN '/sbin/modprobe $env{MODALIAS}' /etc/udev/rules.d/local.rules:31
udev_rules_apply_to_event: RUN 'socket:/org/kernel/udev/monitor' /etc/udev/rules.d/run.rules:2
udev_rules_apply_to_event: RUN '/usr/bin/xmodmap ~/.usbXmodmap' /etc/udev/rules.d/usb-keyboard.rules:4
udevadm_test: UDEV_LOG=6
udevadm_test: DEVPATH=/devices/platform/ehci-omap.0/usb1/1-2/1-2.3/1-2.3:1.1/input/input10
udevadm_test: PRODUCT=3/62a/201/110
udevadm_test: NAME="USB-compliant keyboard"
udevadm_test: PHYS="usb-ehci-omap.0-2.3/input1"
udevadm_test: UNIQ=""
udevadm_test: EV==1f
udevadm_test: KEY==837fff 2c3027 bf004444 0 0 1fe3 c04 a27c000 267bfa d941dfed 9e0000 0 0 0
udevadm_test: REL==143
udevadm_test: ABS==1 0
udevadm_test: MSC==10
udevadm_test: MODALIAS=input:b0003v062Ap0201e0110-e0,1,2,3,4,k71,72,73,74,77,80,82,83,85,86,87,88,89,8A,8B,8C,8E,8F,90,96,98,9B,9C,9E,9F,A1,A3,A4,A5,A6,A7,A8,A9,AB,AC,AD,AE,B1,B2,B5,CE,CF,D0,D1,D2,D5,D9,DB,E2,EA,EB,100,101,105,106,107,108,109,10A,10B,10C,162,166,16A,16E,178,179,17A,17B,17C,17D,17F,180,181,182,185,18C,18D,192,193,195,1A0,1A1,1A2,1A3,1A4,1A5,1A6,1A7,1A8,1A9,1AA,1AB,1AC,1AD,1AE,1B0,1B1,1B7,r0,1,6,8,a20,m4,lsfw
udevadm_test: ACTION=add
udevadm_test: SUBSYSTEM=input
udevadm_test: run: '/sbin/modprobe -b input:b0003v062Ap0201e0110-e0,1,2,3,4,k71,72,73,74,77,80,82,83,85,86,87,88,89,8A,8B,8C,8E,8F,90,96,98,9B,9C,9E,9F,A1,A3,A4,A5,A6,A7,A8,A9,AB,AC,AD,AE,B1,B2,B5,CE,CF,D0,D1,D2,D5,D9,DB,E2,EA,EB,100,101,105,106,107,108,109,10A,10B,10C,162,166,16A,16E,178,179,17A,17B,17C,17D,17F,180,181,182,185,18C,18D,192,193,195,1A0,1A1,1A2,1A3,1A4,1A5,1A6,1A7,1A8,1A9,1AA,1AB,1AC,1AD,1AE,1B0,1B1,1B7,r0,1,6,8,a20,m4,lsfw'
udevadm_test: run: 'socket:@/org/freedesktop/hal/udev_event'
udevadm_test: run: '/sbin/modprobe input:b0003v062Ap0201e0110-e0,1,2,3,4,k71,72,73,74,77,80,82,83,85,86,87,88,89,8A,8B,8C,8E,8F,90,96,98,9B,9C,9E,9F,A1,A3,A4,A5,A6,A7,A8,A9,AB,AC,AD,AE,B1,B2,B5,CE,CF,D0,D1,D2,D5,D9,DB,E2,EA,EB,100,101,105,106,107,108,109,10A,10B,10C,162,166,16A,16E,178,179,17A,17B,17C,17D,17F,180,181,182,185,18C,18D,192,193,195,1A0,1A1,1A2,1A3,1A4,1A5,1A6,1A7,1A8,1A9,1AA,1AB,1AC,1AD,1AE,1B0,1B1,1B7,r0,1,6,8,a20,m4,lsfw'
udevadm_test: run: 'socket:/org/kernel/udev/monitor'
udevadm_test: run: '/usr/bin/xmodmap ~/.usbXmodmap'

and

>udevadm test --action=remove /devices/platform/ehci-omap.0/usb1/1-2/1-2.3/1-2.3:1.1/input/input10

outputs:

run_command: calling: test
udevadm_test: version 151
This program is for debugging only, it does not run any program,
specified by a RUN key. It may show incorrect results, because
some values may be different, or not available at a simulation run.

[...]
parse_file: reading '/etc/udev/rules.d/usb-keyboard.rules' as rules file
udev_rules_new: rules use 100572 bytes tokens (8381 * 12 bytes), 21523 bytes buffer
udev_rules_new: temporary index used 35380 bytes (1769 * 20 bytes)
udev_device_new_from_syspath: device 0x3b4d8 has devpath '/devices/platform/ehci-omap.0/usb1/1-2/1-2.3/1-2.3:1.1/input/input10'
udev_rules_apply_to_event: RUN 'socket:@/org/freedesktop/hal/udev_event' /etc/udev/rules.d/90-hal.rules:2
udev_rules_apply_to_event: RUN 'socket:/org/kernel/udev/monitor' /etc/udev/rules.d/run.rules:2
udev_rules_apply_to_event: RUN '/usr/bin/xmodmap ~/.pndXmodmap' /etc/udev/rules.d/usb-keyboard.rules:5
udevadm_test: UDEV_LOG=6
udevadm_test: DEVPATH=/devices/platform/ehci-omap.0/usb1/1-2/1-2.3/1-2.3:1.1/input/input10
udevadm_test: PRODUCT=3/62a/201/110
udevadm_test: NAME="USB-compliant keyboard"
udevadm_test: PHYS="usb-ehci-omap.0-2.3/input1"
udevadm_test: UNIQ=""
udevadm_test: EV==1f
udevadm_test: KEY==837fff 2c3027 bf004444 0 0 1fe3 c04 a27c000 267bfa d941dfed 9e0000 0 0 0
udevadm_test: REL==143
udevadm_test: ABS==1 0
udevadm_test: MSC==10
udevadm_test: MODALIAS=input:b0003v062Ap0201e0110-e0,1,2,3,4,k71,72,73,74,77,80,82,83,85,86,87,88,89,8A,8B,8C,8E,8F,90,96,98,9B,9C,9E,9F,A1,A3,A4,A5,A6,A7,A8,A9,AB,AC,AD,AE,B1,B2,B5,CE,CF,D0,D1,D2,D5,D9,DB,E2,EA,EB,100,101,105,106,107,108,109,10A,10B,10C,162,166,16A,16E,178,179,17A,17B,17C,17D,17F,180,181,182,185,18C,18D,192,193,195,1A0,1A1,1A2,1A3,1A4,1A5,1A6,1A7,1A8,1A9,1AA,1AB,1AC,1AD,1AE,1B0,1B1,1B7,r0,1,6,8,a20,m4,lsfw
udevadm_test: ACTION=remove
udevadm_test: SUBSYSTEM=input
udevadm_test: run: 'socket:@/org/freedesktop/hal/udev_event'
udevadm_test: run: 'socket:/org/kernel/udev/monitor'
udevadm_test: run: '/usr/bin/xmodmap ~/.pndXmodmap'

which seems to show it should work, but it doesnt hope this helps get an answer.

1
  • 1
    related.
    – user250846
    Oct 5, 2013 at 15:16

9 Answers 9

21

I found a way to work around this, though its a little hacky.

I got to the same exact point today in trying to set up two keyboards with udev, setxkbmap, and xinput --list and for them to work with usb hotplugging. I am swapping keys around, not changing the layout, but its all the same, once you've identified your keyboard on a hotplug and can conditonally call setxkbmap, then you should be able to set the language of only the keyboard you've specified. The list of keyboard layouts can be found here ls -l /usr/share/kbd/keymaps/i386/ and you can find your device name to gre on with xinput -list.

  1. You'll want to replace rizumu with your username, as I found it wasn't possible a way to do this without being explicit.
  2. Make sure you grep on the your keyboard name.
  3. Use lsusb to discover the Hardware ID that you need to set in the udev rule. My das keyboard looks like this Bus 002 Device 009: ID 04d9:2013 Holtek Semiconductor, Inc.

I first set up the udev rule to autodetct the keyboard is by creating a udev rule:

In the file /etc/udev/rules.d/00-usb-keyboards.rules:

ACTION=="add", ATTRS{idVendor}=="04d9", ATTRS{idProduct}=="2013", RUN+="/home/rizumu/bin/kbd_udev", OWNER="rizumu"

I have two files ~/bin/kbd and ~/bin/kbd_udev. Make sure they have the right permissions chmod 755 ~/bin/kbd*

The ~/bin/kbd_udev script contains:

#!/bin/bash
/home/rizumu/bin/kbd &

And you'll notice that all it does is call ~/bin/kbd in the background, so that udev can complete its process and activate the keyboard. Inside the ~/bin/kbd script we sleep for a second, because we need to wait until the keyboard is activated so we can get the device id using xinput. To achive this I've set some variables and exported them so xinput setxkbmap can do thier work: DISPLAY, XAUTHORITY, HOME, and one daskb_id for the id of my daskeyboard:

#!/bin/bash
sleep 1
DISPLAY=":0.0"
HOME=/home/rizumu/
XAUTHORITY=$HOME/.Xauthority
export DISPLAY XAUTHORITY HOME
daskb_id=`xinput -list | grep -i 'daskeyboard' | grep -o id=[0-9]. | grep -o [0-9]. | head -1`

xset r rate 200 30
setxkbmap -layout colemak
setxkbmap -option ctrl:nocaps
if [ "${daskb_id}" ]; then
    setxkbmap -device "${daskb_id}" -option altwin:swap_lalt_lwin
fi
8
  • Thanks a lot for helping me answer my own question is AskUbuntu: askubuntu.com/questions/337411/…
    – Sadi
    Sep 22, 2013 at 16:34
  • 1
    And I wonder if you can also help me add a notification message at the end of this script (e.g. notify-send "USB Keyboard is plugged in and ready for use now." -i gtk-dialog-info -t 1000 -u normal). As I don't know much about scripting, I tried inserting it before or after "fi" but in both cases the notification message kept on appearing over and over again :-(
    – Sadi
    Sep 22, 2013 at 16:42
  • 1
    What does the xset r rate 200 30 line do? xset is not available on my Ubuntu 17.04 installation. Jul 19, 2017 at 8:30
  • 2
    I'm not able to run xmodmap $HOME/.Xmodmap using a script analogous to your "/home/rizumu/bin/kbd". Why would that be?
    – Geremia
    Dec 10, 2017 at 6:26
  • 2
    Whoa! setxkbmap has a -device property, meaning you can have multiple keyboards configured with different layout each, and every one works with their own layouts. A-mazing! thank you really much for this! Sep 4, 2018 at 18:29
7

Depending on your distro, you may already have a udev rule for keyboards in /lib/udev/rules.d/64-xorg-xkb.rules. On Ubuntu, this imports /etc/default/keyboard, which has options roughly like this:

XKBMODEL="pc105"
XKBLAYOUT="us"
XKBVARIANT=""
XKBOPTIONS=""

For my setup, I found that this built-in rule was executing after my custom udev rule, and was overriding my settings. Instead I changed XKBOPTIONS in /etc/default/keyboard to be:

XKBOPTIONS="-option ctrl:nocaps"

To get the "Caps Lock is Control" behavior I wanted on all keyboards.

3
  • 3
    Great idea! Mine works with just XBKOPTIONS="ctrl:nocaps"
    – RasmusWL
    Jan 5, 2017 at 19:01
  • The simplest solution I ever found.
    – azzamsa
    Feb 12, 2021 at 6:22
  • This is the response I did not know I needed.
    – soger
    Oct 15, 2021 at 12:07
3

If you're running GNOME then you'll need to disable its keyboard management plugin so that it doesn't override your layout changes.

gconftool-2 --toggle /apps/gnome_settings_daemon/plugins/keyboard/active

Run the same command again to enable it as desired.

5
  • im running angstrom. will this work? Feb 23, 2011 at 2:06
  • Are you using GNOME on Ångström? Feb 23, 2011 at 2:18
  • nope im using xfce 4.6.1 Feb 23, 2011 at 2:40
  • 1
    In my Ubuntu 13.04, this is in dconf under /org/gnome/settings-daemon/plugins/keyboard/active.
    – nh2
    Aug 16, 2013 at 14:06
  • 1
    And the command for Ubuntu 13.04 is: gsettings set org.gnome.settings-daemon.plugins.keyboard active false
    – Sadi
    Sep 22, 2013 at 16:33
3

It does not work because udev and xmodmap don't have access to your X11 display. In fact, udev doesn't even know if there are active X11 displays.

  • Note: displays, plural. It can't use "the" X11 display because there can be more than one. For example, if you use "fast user switching".
4
  • so How could I make this work? Feb 23, 2011 at 13:27
  • anyone know how I can fix this? Feb 23, 2011 at 15:27
  • 1
    I've gotten udev to call setxkbmap. The udev rule calls a script which backgrounds another script (so udev can complete). The second script pauses for a second, sets up the expected X11 variables, and triggers setxkbmap. See my answer to the main question for more details. Oct 25, 2011 at 18:41
  • @rizumu: Ahh, good luck getting that to work with GDM though. Oct 25, 2011 at 19:41
3

To answer your question about accessing the running display, you can export the appropriate DISPLAY variable in the script, assuming that permissions for the display are correctly set. (man xset for display permissions.)

In many usual cases you can simply export DISPLAY=:0 for the command, as that's the first display on a single user system. It's probably easiest to run a script rather than xmodmap directly, as this will allow you to have more control over environment variables and the rest. (So replace "/usr/bin/xmodmap ~/.usbXmodmap" in your rule with "/usr/local/bin/keyboard_plug.sh" and put the appropriate commands in that script along with the DISPLAY variable.)

As noted above, though, if you assume DISPLAY=:0 then you might run into problems later on if you have multiple users or displays. You can write scripts to detect the appropriate display, but in that case you're on your own (as far as this answer is concerned). :)

3

I think I found a much cleaner way of configuring this, which does not require special X11 hack.

The idea behind this is that udev will only detect for new keyboard input and create a symlink for each layout, then inotify will watch for new layout in userspace.

udev rules

#/etc/udev/rules.d/61-usb-keyboard-layout.rules

# will match my Logitech keyboard with US layout 
SUBSYSTEM=="input", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c31c", GROUP="plugdev", MODE="0660", SYMLINK+="input/by-layout/us"

# will match my Lenovo integrated keyboard with IT layout
SUBSYSTEM=="input", ENV{ID_PATH}=="platform-i8042-serio-0", SYMLINK+="input/by-layout/it"

# force the directory to be recreated, just in case you unplug all input
SUBSYSTEM=="input", RUN="/bin/mkdir -p /dev/input/by-layout"

With this rules, I have a directory under dev (/dev/input/by-layout) to watch for changes in userspace scripts.

Userspace script for KDE

For example, when using KDE I have this script (auto)running:

#!/bin/bash

# In case no link are found, switch to this layout
DEFAULT="it"

switch_layout () {
        [ ! -z "$1" ] || return 0
        /usr/bin/qdbus org.kde.keyboard /Layouts org.kde.KeyboardLayouts.setLayout $1
}

best_layout() {
        local LAYOUT=$(ls -1t /dev/input/by-layout/ | head -n 1)
        if [ -z "$LAYOUT" ] ; then
                LAYOUT=$DEFAULT
        fi
        echo $LAYOUT
}

switch_layout $(best_layout)

while true ; do
        EVENT=$(inotifywait -q -e create -e delete --exclude '.*tmp.*' /dev/input/by-layout/)

        if echo "$EVENT" | grep -qe CREATE ; then
                LAYOUT=${EVENT#?*CREATE }
        fi

        if echo "$EVENT" | grep -qe DELETE ; then
                LAYOUT=$(best_layout)
        fi

        switch_layout $LAYOUT
done

This works like a charm to me. To change system layout (which I don't need right now), a similar script which use loadkeys can be demonized using a system init script.

1
  • Thanks, this made me realize I can just use inotifywait to run a setup script on any change in /dev/input, as the script itself is idempotent. May 4, 2019 at 16:29
3

What about X.Org configuration? From Gentoo Wiki: X.Org/Input_drivers - udev:

Example: If you have a Logitech Access keyboard for the French part of Switzerland, you can use the following:

File: /etc/X11/xorg.conf.d/10-keyboard.conf

Section "InputClass"
    Identifier             "evdev keyboard catchall"
    MatchIsKeyboard        "on"
    MatchDevicePath        "/dev/input/event*"
    Driver                 "evdev"
    Option                 "XkbModel" "logiaccess"
    Option                 "XkbLayout" "ch"
    Option                 "XkbVariant" "fr"
EndSection

For an in-depth explanation, read:

man xorg.conf

and:

man evdev

ArchWiki demonstrates using the same syntax in xorg.conf but notes that "nowadays you should create a separate config file, like /etc/X11/xorg.conf.d/90-keyboard-layouts.conf". I use Arch and configured my own USB keyboard in the existing /etc/X11/xorg.conf.d/vim 10-evdev.conf Worked for me.

@rizumu: Clever kludge, thanks for sharing.

1
  • 1
    I don't have a x.org.conf.d directory on Linux Mint 18.2
    – Max N
    Aug 19, 2017 at 21:04
3

Since I couldn't get the hacks to make udev rules work, I wrote a small Python script using pyudev to monitor for input events.

#! /usr/bin/env python3

import pyudev
import time
import subprocess

ctx = pyudev.Context()
monitor = pyudev.Monitor.from_netlink(ctx)
monitor.filter_by("input")

def defer_xmodmap():
    time.sleep(1) # not sure if there's a race here, but it feels like there could be.
    subprocess.run("xmodmap ~/dotfiles/.xmodmap", shell=True)


for device in iter(monitor.poll, None):
    # there might be a way to add the action condition to the filter, but I couldn't find it
    if device.action != "add":
        continue

    # ensure the KB is initialized -- not sure if this is actually a needed check
    if not device.is_initialized:
        continue

    # my keyboard, from the output of `lsusb`
    if not "045E:07A5" in device.device_path:
        continue

    # it's the keyboard being added.
    defer_xmodmap()

I then use this systemd user unit file to keep it running (systemctl --user enable name_of_service_file):

[Unit]
Description=udev xmodmap monitor

[Service]
ExecStart=/usr/bin/env python3 %h/local/bin/monitor_kb_udev
Restart=always
RestartSec=10

[Install]
WantedBy=default.target

The inotifywait solution from @giosh94mhz is a little simpler, and avoids the dependency on pyudev. However, for some reason I found the inotify event wasn't being triggered for 10-20 seconds after my keyboard was connected.

1
  • 1
    Thanks for this working code! for my external keyboard, this triggers the xmodmap function 6 times, because my keyboard shows as multiple devices for some reason - I need to find a way to narrow further. It would also be great if it could generally match other keyboards.
    – jameh
    May 18, 2020 at 3:37
0

I rewrote a more generic python script that uses udev to call keyboard.sh after (hopefully) any usb keyboard gets plugged in.

#! /usr/bin/env python3

import re, subprocess, sys
from pyudev import Context, Monitor, MonitorObserver

def hotplug_handler():
    print("Calling keyboard.sh")
    subprocess.run("~/.local/lib/X11/keyboard.sh", shell=True)
    sys.stdout.flush()

def is_keyboard(device):
    if device.action != 'add':
        return False
    if 'ID_INPUT_KEYBOARD' not in device.properties or device.properties['ID_INPUT_KEYBOARD'] != '1':
        return False
    if re.search('^/sys/devices/virtual', device.sys_path):
        return False
    if re.search('^event[0-9]+\Z', device.sys_name):
        return False
    if 'ID_USB_INTERFACE_NUM' not in device.properties or device.properties['ID_USB_INTERFACE_NUM'] != '00':
        return False

    print("Added keyboard:")
    print(device.properties['NAME'])
    print(device.sys_path)

    return True

if __name__ == '__main__':
    context = Context()
    monitor = Monitor.from_netlink(context)
    monitor.filter_by(subsystem='input')

    hotplug_handler()
    for device in filter(is_keyboard, iter(monitor.poll, None)):
        hotplug_handler()

I have tested it with two usb keyboards, and it does the trick! Let me know if you have a keyboard this doesn't work for - to check your keyboard's properties you can use:

for key in device.properties:
    print(device.properties[key])

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .