Search notes:

Python: Mapping my keyboard with evdev

This is a simple python script using the evdev library to

Installing

This script installs the keyboard layout remapper:
#!/bin/bash

#
# Install evdev into virtual environment
#
python3 -m venv virt-env
. ./virt-env/bin/activate
pip install evdev

sudo addgroup uinput
#
# Makeu /dev/uinput readable for users in group uinput:
#
sudo sh -c 'echo SUBSYSTEM==\"misc\", KERNEL==\"uinput\", MODE=\"0660\", GROUP=\"uinput\" > /etc/udev/rules.d/99-input.rules'

sudo usermod -a -G  input $USER
sudo usermod -a -G uinput $USER

#
# Start keyboard mapper when logging in
#
dir="$(dirname "$(readlink -f "$0")")"

mkdir -p $HOME/.config/autostart
cat <<TF > ~/.config/autostart/evdev.desktop
[Desktop Entry]
Type=Application
Name=Python evdev keyboard mapper
Version=1.5
Exec=$dir/start
Terminal=true
StartupNotify=false
TF
Github repository py-evdev-mapping, path: /_install

Source code (go.py)

go.py is the Python script that does the re-mapping of the keyboard:
# vim: foldmethod=marker foldmarker={{{,}}}
import asyncio
import atexit
import evdev
import sys

def grab_device(dev_name): # {{{
    dev_ = [ devobj for devobj in [ evdev.InputDevice(devpath) for devpath in evdev.list_devices() ] if devobj.name == dev_name ]

  #
  # Check if at least one device with the indicated name was found:
  #
    if dev_ == []:
       return None

  #
  # The device is the first (and hopefully only) element in the list:
  #
    dev = dev_[0]
  #
  # We want to handle the device's original events ourselves and
  # thus do not want the device to emit these events:
  #
    dev.grab()
  #
  # When this script stope, the device is allowed (yea, even should)
  # emit the events again:
  #
    atexit.register(dev.ungrab)
    return dev

# }}}

def grab_1st_available_device(dev_names): # {{{
    for dev_name in dev_names:
        dv = grab_device(dev_name)
        if dv != None:
           return dv

    return None

# }}}

dv_kb = grab_1st_available_device([
  'Dell Dell Wired Multimedia Keyboard',
  'LITEON Technology USB Multimedia Keyboard',
  'AT Translated Set 2 keyboard'])

dv_ms = grab_1st_available_device([
   'USB OPTICAL MOUSE ',           # Note the final space
   'SYNA8004:00 06CB:CD8B Mouse']) # 'SYNA8004:00 06CB:CD8B Touchpad'

if dv_kb == None or dv_ms == None:
   print('Either Mouse or Keyboard (or both) not found')
   sys.exit(1)

print( "") # Clear screen
print(f'Keyboard at {dv_kb.path}')
print(f'Mouse    at {dv_ms.path}')

left_alt_suppressed = False

vdev = evdev.UInput.from_device(dv_kb, dv_ms, name='virt-device', version=3)

def write_hex(keys): # {{{

    global vdev

    vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_LEFTCTRL , 1)
    vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_LEFTSHIFT, 1)
    vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_U        , 1)
    vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_U        , 0)
    vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_LEFTCTRL , 0)
    vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_LEFTSHIFT, 0)

    for key in keys:
        vdev.write(evdev.ecodes.EV_KEY, key, 1)
        vdev.write(evdev.ecodes.EV_KEY, key, 0)

    vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_SPACE    , 1)
    vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_SPACE    , 0)

# }}}

async def handle_events(dev): # {{{
    global left_alt_suppressed
    global vdev

    async for ev in dev.async_read_loop():
        if ev.type == evdev.ecodes.EV_KEY:  # {{{ Process key events.

           if   ev.code == evdev.ecodes.KEY_PAUSE and ev.value == 1:
            #
            #   Pressing pause exits immediatly.
            #
                sys.exit()

           if   ev.code == evdev.ecodes.KEY_LEFTALT and ev.value == 1:
                print(f'L-ALT')
                left_alt_suppressed = True

           elif left_alt_suppressed and ev.code == evdev.ecodes.KEY_SEMICOLON and ev.value == 1:
                print(f'     ')
                left_alt_suppressed = False
                write_hex([evdev.ecodes.KEY_F, evdev.ecodes.KEY_6]) # ö

           elif left_alt_suppressed and ev.code == evdev.ecodes.KEY_APOSTROPHE and ev.value == 1:
                print(f'     ')
                left_alt_suppressed = False
                write_hex([evdev.ecodes.KEY_E, evdev.ecodes.KEY_4]) # ä

           elif left_alt_suppressed and ev.code == evdev.ecodes.KEY_LEFTBRACE and ev.value == 1:
                print(f'     ')
                left_alt_suppressed = False
                write_hex([evdev.ecodes.KEY_F, evdev.ecodes.KEY_C]) # ü

           elif left_alt_suppressed and ev.code == evdev.ecodes.BTN_LEFT and ev.value == 1:
                vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_LEFTALT, 1)
                vdev.syn()
                left_alt_suppressed = False
                print(f'     ')
                vdev.write(ev.type, ev.code, ev.value)

           elif ev.code == evdev.ecodes.KEY_ESC:
                vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_CAPSLOCK, ev.value)

           elif ev.code == evdev.ecodes.KEY_CAPSLOCK:
                vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_ESC, ev.value)

           elif ev.code == evdev.ecodes.KEY_RIGHTMETA:
                vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_RIGHTCTRL, ev.value)

           elif ev.code == evdev.ecodes.KEY_LEFTMETA:
                vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_LEFTCTRL, ev.value)

           elif left_alt_suppressed:
                vdev.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_LEFTALT, 1)
                vdev.write(evdev.ecodes.EV_KEY, ev.code, ev.value)
                print(f'     ')
                left_alt_suppressed = False

           else:
                vdev.write(ev.type, ev.code, ev.value)
        # }}}

        else:
          # All other events (also SYNs) are passed to uinput without modification:
            vdev.write(ev.type, ev.code, ev.value)
# }}}


for dev in dv_ms, dv_kb:
    asyncio.ensure_future(handle_events(dev))

loop = asyncio.get_event_loop()
loop.run_forever()
Github repository py-evdev-mapping, path: /go.py

shell script: start

start is the shell script that activates the virtual environment into which evdev is installed and then starts go.py.
#!/bin/bash

dir="$(dirname "$(readlink -f "$0")")"

. $dir/virt-env/bin/activate
  $dir/virt-env/bin/python3 $dir/go.py
Github repository py-evdev-mapping, path: /start

History

2024-03-21 Initial version
2024-03-25 Added detection for AT Translated Set 2 keyboard and SYNA8004:00 06CB:CD8B Mouse
2024-04-02 Added function grab_1st_available_device; added keyboard device name Dell Dell Wired Multimedia Keyboard.

TODO

Can wmctrl -l -p somehow be used to identify the process which caused an event?

See also

Configuring/modifying the keyboard layout
The source code is in this Github repository.

Links

This link gave the correct instructions on how to set the /dev/uinput to permission 0660 and its group to uinput, but see also this link.
This and this link helped me figure out what I needed to write into ~/.config/autostart/evdev.desktop so that the keyboard mapper is started automatically.

Index