Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A thoroughly annotated example on keyboard remapping with evdev/uinput.
#!/usr/bin/python3
# CC0, originally written by t184256.
# This is an example Python program for Linux that remaps a keyboard.
# The events (key presses releases and repeats), are captured with evdev,
# and then injected back with uinput.
# This approach should work in X, Wayland, anywhere!
# Also it is not limited to keyboards, may be adapted to any input devices.
# The program should be easily portable to other languages or extendable to
# run really any code in 'macros', e.g., fetching and typing current weather.
# The ones eager to do it in C can take a look at (overengineered) caps2esc:
# https://github.com/oblitum/caps2esc
# Import necessary libraries.
import atexit
# You need to install evdev with a package manager or pip3.
import evdev # (sudo pip3 install evdev)
# Define an example dictionary describing the remaps.
REMAP_TABLE = {
# Let's swap A and B...
evdev.ecodes.KEY_A: evdev.ecodes.KEY_B,
evdev.ecodes.KEY_B: evdev.ecodes.KEY_A,
# ... and make the right Shift into a second Space.
evdev.ecodes.KEY_RIGHTSHIFT: evdev.ecodes.KEY_SPACE,
# We'll also remap CapsLock to Control when held ...
evdev.ecodes.KEY_CAPSLOCK: evdev.ecodes.KEY_LEFTCTRL,
# ... but to Esc when pressed solo, xcape style! See below.
}
# The names can be found with evtest or in evdev docs.
# The keyboard name we will intercept the events for. Obtainable with evtest.
MATCH = 'AT Translated Set 2 keyboard'
# Find all input devices.
devices = [evdev.InputDevice(fn) for fn in evdev.list_devices()]
# Limit the list to those containing MATCH and pick the first one.
kbd = [d for d in devices if MATCH in d.name][0]
atexit.register(kbd.ungrab) # Don't forget to ungrab the keyboard on exit!
kbd.grab() # Grab, i.e. prevent the keyboard from emitting original events.
soloing_caps = False # A flag needed for CapsLock example later.
# Create a new keyboard mimicking the original one.
with evdev.UInput.from_device(kbd, name='kbdremap') as ui:
for ev in kbd.read_loop(): # Read events from original keyboard.
if ev.type == evdev.ecodes.EV_KEY: # Process key events.
if ev.code == evdev.ecodes.KEY_PAUSE and ev.value == 1:
# Exit on pressing PAUSE.
# Useful if that is your only keyboard. =)
# Also if you bind that script to PAUSE, it'll be a toggle.
break
elif ev.code in REMAP_TABLE:
# Lookup the key we want to press/release instead...
remapped_code = REMAP_TABLE[ev.code]
# And do it.
ui.write(evdev.ecodes.EV_KEY, remapped_code, ev.value)
# Also, remap a 'solo CapsLock' into an Escape as promised.
if ev.code == evdev.ecodes.KEY_CAPSLOCK and ev.value == 0:
if soloing_caps:
# Single-press Escape.
ui.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_ESC, 1)
ui.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_ESC, 0)
else:
# Passthrough other key events unmodified.
ui.write(evdev.ecodes.EV_KEY, ev.code, ev.value)
# If we just pressed (or held) CapsLock, remember it.
# Other keys will reset this flag.
soloing_caps = (ev.code == evdev.ecodes.KEY_CAPSLOCK and ev.value)
else:
# Passthrough other events unmodified (e.g. SYNs).
ui.write(ev.type, ev.code, ev.value)
@parkerlreed
Copy link

parkerlreed commented Nov 22, 2019

So I have an old PS/2 keyboard (shows up as same name thankfully) that I want to use this to bypass X11's/libinput's key repeat rate. The keyboard itself sends raw rate repeats that are configurable via hotkey. This script works GREAT except for when it gets passed back to uinput, the key repeat delay is just added back in, thus negating the desired effect.

Is there any way to basically just bypass they key repeat on the tail end? Maybe using something other than uniput. xtest directly perhaps.

@t184256
Copy link
Author

t184256 commented Nov 22, 2019

@parkerlreed: 1) sure, you can use anything, you'll just be bound to X with xtest 2) did I understand correctly that the keyboard has hardware-generated repeats, you strip them, but something else down the line adds software-generated ones? Maybe all you need is a simple xset r rate ... or an equivalent in your DE/compositor settings.

@parkerlreed
Copy link

parkerlreed commented Nov 22, 2019

The xset rate sets a hard rate and works but still doesn't respect the raw evdev rate. It's always constant.

X11/libinput itself is capping it at whatever is set.

And yeah I know it's less agnostic at that point. I've been poking at xtest/xlib without any luck. I'm sure this can be done but still trying to find out how.

@parkerlreed
Copy link

parkerlreed commented Nov 22, 2019

Also with your script I believe it's reading at the evdev rate which is great, but python-evdev creating the uinput node is just passing back into libinput which negates everything as I mentioned. Yeah the keyboard itself has it's own rate in hardware which is certainly odd. I don't think many if any newer hardware does this. I'm using an AT keyboard passively converted to PS/2.

https://deskthority.net/wiki/Focus_FK-5001

@parkerlreed
Copy link

parkerlreed commented Nov 22, 2019

The keyboard can switch on the fly what rate sends, but the software end (X11/libinput) is basically acting as a bottleneck at that point. This is why I'm attempting to use this to bypass that completely. python-xlib is newer and has a package and python-xtest is older and does not but seems more likely to work. I'll have to mess around and see what I can come up with.

Example of the raw events https://cdn.discordapp.com/attachments/318262911846055936/647244328904949760/simplescreenrecorder-2019-11-21_20.17.13.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment