2025, Oct 03 09:00
evdev/uinput Unicode input fix: declare modifier keys in your virtual keyboard to avoid literal 'u0985'
Troubleshooting evdev/uinput Unicode input on Linux: why Ctrl+Shift+U prints 'u0985' and how declaring modifier keys in virtual keyboard capabilities fixes it.
Remapping a physical key to emit a different Unicode character through a virtual keyboard looks straightforward with evdev and uinput, until the text field stubbornly shows the literal “u0985” instead of the character behind that code point. If you simulate Ctrl+Shift+U and then type the hex digits, but your target app receives plain letters and digits, the problem is typically not in the sequence itself.
Problem statement
The goal is to press a real key, intercept it, and make a virtual keyboard inject the Unicode input sequence Ctrl+Shift+U 0985 Enter so the focused application receives the actual character instead of the raw characters “u0985”. The observed behavior: the injected sequence appears as “u0985” in the focused element, not as the corresponding Unicode character.
Reproducible code showing the issue
The following snippet reads from a physical input device, grabs it, and uses a virtual uinput device to send Ctrl+Shift+U, the digits 0, 9, 8, 5, and Enter. The virtual device declares a limited set of keys in its capabilities, and that’s where the subtle error appears.
import evdev
from evdev import UInput, ecodes as codes
import time
# Bind to the physical keyboard-like device
src_dev = evdev.InputDevice('/dev/input/event2')
src_dev.grab()
# Virtual device with incomplete capabilities (problematic)
virt_caps = {codes.EV_KEY: [
    codes.KEY_A,
    codes.KEY_B,
    codes.KEY_G,
    codes.KEY_U,
    codes.KEY_0,
    codes.KEY_9,
    codes.KEY_8,
    codes.KEY_5,
    codes.KEY_ENTER,
]}
vkb = UInput(virt_caps, name='virtual_kbd_demo')
for evt in src_dev.read_loop():
    if evt.type == codes.EV_KEY:
        key_info = evdev.categorize(evt)
        if key_info.keystate == evdev.KeyEvent.key_down:
            # Start Unicode input mode
            vkb.write(evt.type, codes.KEY_LEFTCTRL, 1)
            vkb.write(evt.type, codes.KEY_LEFTSHIFT, 1)
            vkb.write(evt.type, codes.KEY_U, 1)
            vkb.write(evt.type, codes.KEY_U, 0)
            vkb.write(evt.type, codes.KEY_LEFTSHIFT, 0)
            vkb.write(evt.type, codes.KEY_LEFTCTRL, 0)
            vkb.syn()
            time.sleep(0.1)
            # Send hex digits
            vkb.write(evt.type, codes.KEY_0, 1)
            vkb.write(evt.type, codes.KEY_0, 0)
            vkb.write(evt.type, codes.KEY_9, 1)
            vkb.write(evt.type, codes.KEY_9, 0)
            vkb.write(evt.type, codes.KEY_8, 1)
            vkb.write(evt.type, codes.KEY_8, 0)
            vkb.write(evt.type, codes.KEY_5, 1)
            vkb.write(evt.type, codes.KEY_5, 0)
            vkb.syn()
            time.sleep(0.1)
            # Confirm with Enter
            vkb.write(evt.type, codes.KEY_ENTER, 1)
            vkb.write(evt.type, codes.KEY_ENTER, 0)
            vkb.syn()
        elif key_info.keystate == evdev.KeyEvent.key_up:
            pass
    else:
        # Forward non-key events to keep timing/flow sane
        vkb.write(evt.type, evt.code, evt.value)
# Cleanup
src_dev.ungrab()
vkb.close()
What’s really happening
The injected sequence is correct, but the virtual device does not advertise that it supports the modifier keys it tries to emit. When the uinput device is created without KEY_LEFTCTRL and KEY_LEFTSHIFT in its capabilities, those key presses are not accepted from that device. As a consequence, the system sees only “u” and the digits, which explains why the focused element receives literal text like “u0985” instead of switching to Unicode input mode and composing the target character.
The fix
Declare every key the virtual device will ever send. In this case, add KEY_LEFTCTRL and KEY_LEFTSHIFT to the EV_KEY capability set. With this change, the same injection logic yields the expected Unicode character.
import evdev
from evdev import UInput, ecodes as codes
import time
# Bind to the physical keyboard-like device
src_dev = evdev.InputDevice('/dev/input/event2')
src_dev.grab()
# Virtual device with complete capabilities for the sequence
virt_caps = {codes.EV_KEY: [
    codes.KEY_LEFTCTRL,   # added
    codes.KEY_LEFTSHIFT,  # added
    codes.KEY_U,
    codes.KEY_0,
    codes.KEY_9,
    codes.KEY_8,
    codes.KEY_5,
    codes.KEY_ENTER,
]}
vkb = UInput(virt_caps, name='virtual_kbd_demo')
for evt in src_dev.read_loop():
    if evt.type == codes.EV_KEY:
        key_info = evdev.categorize(evt)
        if key_info.keystate == evdev.KeyEvent.key_down:
            # Start Unicode input mode
            vkb.write(evt.type, codes.KEY_LEFTCTRL, 1)
            vkb.write(evt.type, codes.KEY_LEFTSHIFT, 1)
            vkb.write(evt.type, codes.KEY_U, 1)
            vkb.write(evt.type, codes.KEY_U, 0)
            vkb.write(evt.type, codes.KEY_LEFTSHIFT, 0)
            vkb.write(evt.type, codes.KEY_LEFTCTRL, 0)
            vkb.syn()
            time.sleep(0.1)
            # Send hex digits
            vkb.write(evt.type, codes.KEY_0, 1)
            vkb.write(evt.type, codes.KEY_0, 0)
            vkb.write(evt.type, codes.KEY_9, 1)
            vkb.write(evt.type, codes.KEY_9, 0)
            vkb.write(evt.type, codes.KEY_8, 1)
            vkb.write(evt.type, codes.KEY_8, 0)
            vkb.write(evt.type, codes.KEY_5, 1)
            vkb.write(evt.type, codes.KEY_5, 0)
            vkb.syn()
            time.sleep(0.1)
            # Confirm with Enter
            vkb.write(evt.type, codes.KEY_ENTER, 1)
            vkb.write(evt.type, codes.KEY_ENTER, 0)
            vkb.syn()
        elif key_info.keystate == evdev.KeyEvent.key_up:
            pass
    else:
        # Forward non-key events to keep timing/flow sane
        vkb.write(evt.type, evt.code, evt.value)
# Cleanup
src_dev.ungrab()
vkb.close()
Why it’s important
When building virtual keyboards or remappers with uinput, the capability set is a contract. The input stack relies on it to decide which events a device may legitimately produce. If a key is not declared, the corresponding event can be ignored or mishandled by higher layers, leading to confusing outcomes like literal “u0985” instead of the intended character. This detail is easy to overlook because everything else about the sequence looks correct and debuggers show your code firing the right calls.
Takeaways
If you plan to emit a key from a virtual device, include it in the EV_KEY capability list up front. That applies especially to modifiers such as Ctrl and Shift, which gate behaviors like Unicode input mode. Once the capability set contains every key you send, the Unicode composition flow behaves as expected and the focused application receives the actual character rather than the raw keystrokes.