Turning NumLock on/off in Linux when plugging in keyboard

Posted on January 25, 2010

My Linux machine is a System76 laptop that runs Ubuntu. Normal keyboards hurt my wrists, so I have a Microsoft ergonomic keyboard that’s very comfortable for extended typing sessions.

Sometimes, though, I have to unplug my laptop and take it away from my desk. Typically I have NumLock turned on when the Microsoft keyboard is plugged in, so that I can use the number pad. But when I unplug the keyboard, NumLock remains on. This is annoying because the “number pad” on the laptop overlaps the main keyboard, resulting in typing mistakes when I inevitably forget to switch off NumLock. Wouldn’t it be nice if Linux would automatically toggle NumLock depending if the keyboard is plugged in or not?

Luckily, the Linux device manager, udev, is pretty slick. Udev follows a collection of built-in and user-created “rules” that determine what happens when devices are added and removed. If you have any recent distribution of Linux, you probably are running udev.

Numlockx

To toggle NumLock in a udev rule, you need a way to enable and disable NumLock from a script. Luckily, there is a program called numlockx that makes this easy:

numlockx on # turns on NumLock
numlockx off # turns off NumLock
numlockx toggle # toggles NumLock status

The Ubuntu software repositories have a package for numlockx already, so it’s just a matter of installing it with Synaptic or apt. If you have another distro, you can search for a numlockx package or compile it from the source.

Getting the Device Info

Before you write a udev rule, you have to get information about the device. Because I’m working with a USB keyboard, I can get messages from dmesg when I plug in the keyboard:

[20906.985102] usb 3-2: new low speed USB device using uhci_hcd and address 6
[20907.166403] usb 3-2: configuration #1 chosen from 1 choice
[20907.192904] input: Microsoft Natural® Ergonomic Keyboard 4000 as /devices/pci0000:00/0000:00:1a.0/usb3/3-2/3-2:1.0/input/input20
[20907.193100] microsoft 0003:045E:00DB.000B: input,hidraw1: USB HID v1.11 Keyboard [Microsoft Natural® Ergonomic Keyboard 4000] on usb-0000:00:1a.0-2/input0
[20907.217810] input: Microsoft Natural® Ergonomic Keyboard 4000 as /devices/pci0000:00/0000:00:1a.0/usb3/3-2/3-2:1.1/input/input21
[20907.217979] microsoft 0003:045E:00DB.000C: input,hidraw2: USB HID v1.11 Device [Microsoft Natural® Ergonomic Keyboard 4000] on usb-0000:00:1a.0-2/input1

The path (/devices/...) is the important bit, as it tells us exactly where the device is located. Copy this path for the next steps.

The udev system comes with the udevadm tool, which lets us manage the devices in udev. First, print out all the information about the device using udevadm info:

> udevadm info -a -p /devices/pci0000:00/0000:00:1a.0/usb3/3-2/3-2:1.1/input/input21

  looking at device '/devices/pci0000:00/0000:00:1a.0/usb3/3-2/3-2:1.1/input/input21':
    KERNEL=="input21"
    SUBSYSTEM=="input"
    DRIVER==""
    ATTR{phys}=="usb-0000:00:1a.0-2/input1"
    ATTR{uniq}==""
    ATTR{modalias}=="input:b0003v045Ep00DBe0111-e0,1,2,3,4,14,k71,72,73,74,75,77,79,7A,7B,7C,7D,7E,7F,80,81,82,83,84,85,86,87,88,89,8A,8B,8C,8E,90,96,98,9B,9C,9E,9F,A1,A3,A4,A5,A6,A7,A8,A9,AB,AC,AD,AE,B0,B1,B2,B3,B4,B5,B6,B8,B9,BA,BB,BC,BD,BE,BF,C0,C1,C2,CE,CF,D0,D1,D2,D5,D9,DB,DF,E2,E7,E8,E9,EA,EB,F0,100,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,r6,a20,m4,lsfw"

  looking at parent device '/devices/pci0000:00/0000:00:1a.0/usb3/3-2/3-2:1.1':
    KERNELS=="3-2:1.1"
    SUBSYSTEMS=="usb"
    DRIVERS=="usbhid"
    ATTRS{bInterfaceNumber}=="01"
    ATTRS{bAlternateSetting}==" 0"
    ATTRS{bNumEndpoints}=="01"
    ATTRS{bInterfaceClass}=="03"
    ATTRS{bInterfaceSubClass}=="00"
    ATTRS{bInterfaceProtocol}=="00"
    ATTRS{modalias}=="usb:v045Ep00DBd0173dc00dsc00dp00ic03isc00ip00"
    ATTRS{supports_autosuspend}=="1"

  looking at parent device '/devices/pci0000:00/0000:00:1a.0/usb3/3-2':
    KERNELS=="3-2"
    SUBSYSTEMS=="usb"
    DRIVERS=="usb"
    ATTRS{configuration}==""
    ATTRS{bNumInterfaces}==" 2"
    ATTRS{bConfigurationValue}=="1"
    ATTRS{bmAttributes}=="a0"
    ATTRS{bMaxPower}=="100mA"
    ATTRS{urbnum}=="532"
    ATTRS{idVendor}=="045e"
    ATTRS{idProduct}=="00db"
    ATTRS{bcdDevice}=="0173"
    ATTRS{bDeviceClass}=="00"
    ATTRS{bDeviceSubClass}=="00"
    ATTRS{bDeviceProtocol}=="00"
    ATTRS{bNumConfigurations}=="1"
    ATTRS{bMaxPacketSize0}=="8"
    ATTRS{speed}=="1.5"
    ATTRS{busnum}=="3"
    ATTRS{devnum}=="6"
    ATTRS{version}==" 2.00"
    ATTRS{maxchild}=="0"
    ATTRS{quirks}=="0x0"
    ATTRS{authorized}=="1"
    ATTRS{manufacturer}=="Microsoft"

  looking at parent device '/devices/pci0000:00/0000:00:1a.0/usb3':
    KERNELS=="usb3"
    SUBSYSTEMS=="usb"
    DRIVERS=="usb"
    ATTRS{configuration}==""
    ATTRS{bNumInterfaces}==" 1"
    ATTRS{bConfigurationValue}=="1"
    ATTRS{bmAttributes}=="e0"
    ATTRS{bMaxPower}=="  0mA"
    ATTRS{urbnum}=="127"
    ATTRS{idVendor}=="1d6b"
    ATTRS{idProduct}=="0001"
    ATTRS{bcdDevice}=="0206"
    ATTRS{bDeviceClass}=="09"
    ATTRS{bDeviceSubClass}=="00"
    ATTRS{bDeviceProtocol}=="00"
    ATTRS{bNumConfigurations}=="1"
    ATTRS{bMaxPacketSize0}=="64"
    ATTRS{speed}=="12"
    ATTRS{busnum}=="3"
    ATTRS{devnum}=="1"
    ATTRS{version}==" 1.10"
    ATTRS{maxchild}=="2"
    ATTRS{quirks}=="0x0"
    ATTRS{authorized}=="1"
    ATTRS{manufacturer}=="Linux 2.6.31-16-generic uhci_hcd"
    ATTRS{product}=="UHCI Host Controller"
    ATTRS{serial}=="0000:00:1a.0"
    ATTRS{authorized_default}=="1"

  looking at parent device '/devices/pci0000:00/0000:00:1a.0':
    KERNELS=="0000:00:1a.0"
    SUBSYSTEMS=="pci"
    DRIVERS=="uhci_hcd"
    ATTRS{vendor}=="0x8086"
    ATTRS{device}=="0x2937"
    ATTRS{subsystem_vendor}=="0x1558"
    ATTRS{subsystem_device}=="0x0860"
    ATTRS{class}=="0x0c0300"
    ATTRS{irq}=="16"
    ATTRS{local_cpus}=="ff"
    ATTRS{local_cpulist}=="0-7"
    ATTRS{modalias}=="pci:v00008086d00002937sv00001558sd00000860bc0Csc03i00"
    ATTRS{broken_parity_status}=="0"
    ATTRS{msi_bus}==""

  looking at parent device '/devices/pci0000:00':
    KERNELS=="pci0000:00"
    SUBSYSTEMS==""
    DRIVERS==""

You can trace the device information all the way up the tree. In many cases, you’ll probably want to use attributes such as the vendor ID or product ID to uniquely identify the device.

For the NumLock problem, I’ll use the manufacturer name (“Microsoft”) and the subsystem (“input”). My keyboard is the only Microsoft input device I use, and if I ever happen to add another one (e.g., a Microsoft mouse), then running the numlockx command multiple times won’t hurt anything.

Writing the Rules

You can add new rules to the /etc/udev/rules.d directory (with root permission, of course). I created a new file called /etc/udev/rules.d/usb-keyboard.rules:

# Modify NumLock status when a keyboard is plugged/unplugged.
# by Eric Heikes
# UPDATE: This no longer works as shown here! See "Give X11 Access to Your Display" below for the new rules.

# Turn on NumLock when keyboard is plugged in.
ACTION=="add", ATTRS{manufacturer}=="Microsoft", SUBSYSTEM=="input", RUN+="/bin/sh -c 'DISPLAY=:0 /usr/bin/numlockx on'"

# Turn off NumLock when keyboard is unplugged.
ACTION=="remove", ATTRS{manufacturer}=="Microsoft", SUBSYSTEM=="input", RUN+="/bin/sh -c 'DISPLAY=:0 /usr/bin/numlockx off'"

For more explanation on writing udev rules, check out the excellent “Writing udev rules” by Daniel Drake; it will tell you everything you need to know.

Restart udev (or run sudo udevadm control reload_rules) so that the rules are reloaded. You can then use udevadm test to make sure that the rule will be called when adding and removing the device:

> udevadm test --action=add /devices/pci0000:00/0000:00:1a.0/usb3/3-2/3-2:1.1/input/input21
run_command: calling: test
udevadm_test: version 147

[...]
parse_file: reading '/etc/udev/rules.d/usb-keyboard.rules' as rules file
udev_rules_new: rules use 180864 bytes tokens (15072 * 12 bytes), 31614 bytes buffer
udev_rules_new: temporary index used 49760 bytes (2488 * 20 bytes)
udev_device_new_from_syspath: device 0x28d7d80 has devpath '/devices/pci0000:00/0000:00:1a.0/usb3/3-2/3-2:1.1/input/input21'
udev_rules_apply_to_event: RUN '/sbin/modprobe -b $env{MODALIAS}' /lib/udev/rules.d/80-drivers.rules:5
udev_rules_apply_to_event: RUN 'socket:@/org/freedesktop/hal/udev_event' /lib/udev/rules.d/90-hal.rules:2
udev_device_new_from_syspath: device 0x28d8560 has devpath '/devices/pci0000:00/0000:00:1a.0/usb3/3-2/3-2:1.1'
udev_device_new_from_syspath: device 0x28d8708 has devpath '/devices/pci0000:00/0000:00:1a.0/usb3/3-2'
udev_rules_apply_to_event: RUN '/usr/bin/numlockx on' /etc/udev/rules.d/usb-keyboard.rules:7
udevadm_test: UDEV_LOG=6
udevadm_test: DEVPATH=/devices/pci0000:00/0000:00:1a.0/usb3/3-2/3-2:1.1/input/input21
udevadm_test: PRODUCT=3/45e/db/111
udevadm_test: NAME="Microsoft Natural® Ergonomic Keyboard 4000"
udevadm_test: PHYS="usb-0000:00:1a.0-2/input1"
udevadm_test: UNIQ=""
udevadm_test: EV==10001f
udevadm_test: KEY==837fff 2c3027 bf004444 0 0 1 10f84 8a27c007 ff7f7bfa d9415fff febeffdf ffefffff ffffffff fffffffe
udevadm_test: REL==40
udevadm_test: ABS==1 0
udevadm_test: MSC==10
udevadm_test: MODALIAS=input:b0003v045Ep00DBe0111-e0,1,2,3,4,14,k71,72,73,74,75,77,79,7A,7B,7C,7D,7E,7F,80,81,82,83,84,85,86,87,88,89,8A,8B,8C,8E,90,96,98,9B,9C,9E,9F,A1,A3,A4,A5,A6,A7,A8,A9,AB,AC,AD,AE,B0,B1,B2,B3,B4,B5,B6,B8,B9,BA,BB,BC,BD,BE,BF,C0,C1,C2,CE,CF,D0,D1,D2,D5,D9,DB,DF,E2,E7,E8,E9,EA,EB,F0,100,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,r6,a20,m4,lsfw
udevadm_test: ACTION=add
udevadm_test: SUBSYSTEM=input
udevadm_test: run: '/sbin/modprobe -b input:b0003v045Ep00DBe0111-e0,1,2,3,4,14,k71,72,73,74,75,77,79,7A,7B,7C,7D,7E,7F,80,81,82,83,84,85,86,87,88,89,8A,8B,8C,8E,90,96,98,9B,9C,9E,9F,A1,A3,A4,A5,A6,A7,A8,A9,AB,AC,AD,AE,B0,B1,B2,B3,B4,B5,B6,B8,B9,BA,BB,BC,BD,BE,BF,C0,C1,C2,CE,CF,D0,D1,D2,D5,D9,DB,DF,E2,E7,E8,E9,EA,EB,F0,100,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,r6,a20,m4,lsfw'
udevadm_test: run: 'socket:@/org/freedesktop/hal/udev_event'
udevadm_test: run: '/bin/sh -c 'DISPLAY=:0 /usr/bin/numlockx on''

You should see your rule file in the output, and the run command as well. Here’s the removal test:

> udevadm test --action=remove /devices/pci0000:00/0000:00:1a.0/usb3/3-2/3-2:1.1/input/input21

run_command: calling: test
udevadm_test: version 147

[...]
parse_file: reading '/etc/udev/rules.d/usb-keyboard.rules' as rules file
udev_rules_new: rules use 180864 bytes tokens (15072 * 12 bytes), 31614 bytes buffer
udev_rules_new: temporary index used 49760 bytes (2488 * 20 bytes)
udev_device_new_from_syspath: device 0xd2fd80 has devpath '/devices/pci0000:00/0000:00:1a.0/usb3/3-2/3-2:1.1/input/input21'
udev_rules_apply_to_event: RUN 'socket:@/org/freedesktop/hal/udev_event' /lib/udev/rules.d/90-hal.rules:2
udev_device_new_from_syspath: device 0xd2fff8 has devpath '/devices/pci0000:00/0000:00:1a.0/usb3/3-2/3-2:1.1'
udev_device_new_from_syspath: device 0xd30690 has devpath '/devices/pci0000:00/0000:00:1a.0/usb3/3-2'
udev_rules_apply_to_event: RUN '/usr/bin/numlockx off' /etc/udev/rules.d/usb-keyboard.rules:10
udevadm_test: UDEV_LOG=6
udevadm_test: DEVPATH=/devices/pci0000:00/0000:00:1a.0/usb3/3-2/3-2:1.1/input/input21
udevadm_test: PRODUCT=3/45e/db/111
udevadm_test: NAME="Microsoft Natural® Ergonomic Keyboard 4000"
udevadm_test: PHYS="usb-0000:00:1a.0-2/input1"
udevadm_test: UNIQ=""
udevadm_test: EV==10001f
udevadm_test: KEY==837fff 2c3027 bf004444 0 0 1 10f84 8a27c007 ff7f7bfa d9415fff febeffdf ffefffff ffffffff fffffffe
udevadm_test: REL==40
udevadm_test: ABS==1 0
udevadm_test: MSC==10
udevadm_test: MODALIAS=input:b0003v045Ep00DBe0111-e0,1,2,3,4,14,k71,72,73,74,75,77,79,7A,7B,7C,7D,7E,7F,80,81,82,83,84,85,86,87,88,89,8A,8B,8C,8E,90,96,98,9B,9C,9E,9F,A1,A3,A4,A5,A6,A7,A8,A9,AB,AC,AD,AE,B0,B1,B2,B3,B4,B5,B6,B8,B9,BA,BB,BC,BD,BE,BF,C0,C1,C2,CE,CF,D0,D1,D2,D5,D9,DB,DF,E2,E7,E8,E9,EA,EB,F0,100,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,r6,a20,m4,lsfw
udevadm_test: ACTION=remove
udevadm_test: SUBSYSTEM=input
udevadm_test: run: 'socket:@/org/freedesktop/hal/udev_event'
udevadm_test: run: '/bin/sh -c 'DISPLAY=:0 /usr/bin/numlockx off''

Some Subtleties When Writing Udev Rules

Initially I ran into some trouble getting the rules to work. With the help of some gurus on superuser.com, I figured out the problems.

If you run the udev daemon in debug mode (udevd --debug), then you can easily see any problems that occur when the rule’s script is run.

Include Full Paths

The udev system doesn’t run in a nice shell environment like we users have. This means you have to use the full path to binaries and files in your rule — in my case, I had to specify the full paths to sh and numlockx.

X11 Programs Need a Display

Numlockx, as indicated in the name, uses the X11 session to set NumLock. Again, this is automatically set for normal users, but the udev system needs to know which session to use. This is as simple as setting the DISPLAY environment variable prior to calling numlockx. Since I am the only desktop user on my laptop, I can simply set it to :0 (the main display).

Give X11 Access to Your Display (added September 2011)

The rule described in this article stopped working when I upgraded the Ubuntu distro (sometime around 10.10). X11 now has stricter access control, so you must give your script permission to access the X11 display.

As mentioned in this forum thread, you can either include an xhost line in your script to allow access, or run the command as the desired user. In my case, I chose to run the command as myself using su:

# Turn on NumLock when keyboard is plugged in.
ACTION=="add", ATTRS{manufacturer}=="Microsoft", SUBSYSTEM=="input", RUN+="/bin/su heikese -c 'DISPLAY=:0 /usr/bin/numlockx on'"

# Turn off NumLock when keyboard is unplugged.
ACTION=="remove", ATTRS{manufacturer}=="Microsoft", SUBSYSTEM=="input", RUN+="/bin/su heikese -c 'DISPLAY=:0 /usr/bin/numlockx off'"

Startup Items

Finally, because I frequently plug and unplug the keyboard when the computer is off (and therefore udev is not running to detect the change), I created a simple Bash script to run upon system startup.

#!/bin/bash

check=`lsusb | grep -i keyboard`

if [[ $check != "" ]]
then
    /usr/bin/numlockx on
else
    /usr/bin/numlockx off
fi

Save this script somewhere (such as ~/bin/check_numlock.sh), make sure it has executable permissions, then add it to your login items (System->Preferences->Startup Applications in Ubuntu).

Screenshot of the Startup Applications window.

 

 

Leave a Reply

One Response to Turning NumLock on/off in Linux when plugging in keyboard

  1.  

    |

  2. Just wanted to thank you for your mention of numlockx. I had been very frustrated trying to get the numlock turned off so it was wonderful to find out about that very useful little program.