pythoninputkeyboardx11xlib

How to send a key press after capturing another key event in X11


I'm trying to implement a Python script in Linux to capture the key press a send the key press a again (so it sends a twice).

Through some code at https://github.com/PeterHo/Linalfred/blob/master/src/globalhotkey.py I arrived at the following.

The key is captured fine, but sending the key press event does nothing. What am I missing here?

I also looked at the question globally capture, ignore and send keyevents with python xlib, recognize fake input. The posted solution doesn't even appear to receive the events (after porting the print to Python 3).

import time

from Xlib import X, protocol
from Xlib.display import Display
from Xlib.ext import record

display = None
root = None

def handler(reply):
    data = reply.data
    while len(data):
        event, data = protocol.rq.EventField(None).parse_binary_value(data, display.display, None, None)
        if event.type == X.KeyPress:
            keycode = event.detail
            print(keycode)
            if keycode == 38:
                window = Display().get_input_focus().focus
                event = protocol.event.KeyPress(
                    time=int(time.time()),
                    root=root,
                    window=window,
                    same_screen=0, child=X.NONE,
                    root_x=0, root_y=0, event_x=0, event_y=0,
                    state=0,
                    detail=keycode
                )
                window.send_event(event, propagate=True)
                event = protocol.event.KeyRelease(
                    time=int(time.time()),
                    root=root,
                    window=window,
                    same_screen=0, child=X.NONE,
                    root_x=0, root_y=0, event_x=0, event_y=0,
                    state=0,
                    detail=keycode
                )
                window.send_event(event, propagate=True)


def main():
    global display, root
    display = Display()
    root = display.screen().root

    ctx = display.record_create_context(
        0,
        [record.AllClients],
        [{
            'core_requests': (0, 0),
            'core_replies': (0, 0),
            'ext_requests': (0, 0, 0, 0),
            'ext_replies': (0, 0, 0, 0),
            'delivered_events': (0, 0),
            'device_events': (X.KeyReleaseMask, X.ButtonReleaseMask),
            'errors': (0, 0),
            'client_started': False,
            'client_died': False,
        }]
    )
    display.record_enable_context(ctx, handler)
    display.record_free_context(ctx)

    while True:
        # Infinite wait, doesn't do anything as no events are grabbed.
        event = root.display.next_event()


if __name__ == '__main__':
    main()

Solution

  • I googled quite a bit more today, and came across Sending key presses to specific windows in X, which worked for me.

    The key idea is to use display instead of window to send the events, and to sync the display. Without syncing the display, the key press won't be registered at all.

    disp.send_event(window, event, propagate=True)
    disp.sync()
    

    Applied that to our script above, we have the following working version:

    import time
    
    from Xlib import X, protocol
    from Xlib.display import Display
    from Xlib.ext import record
    
    display = None
    root = None
    
    def handler(reply):
        data = reply.data
        while len(data):
            event, data = protocol.rq.EventField(None).parse_binary_value(data, display.display, None, None)
            if event.type == X.KeyPress:
                keycode = event.detail
                print(keycode)
                if keycode == 38:
                    disp = Display()
                    window = disp.get_input_focus().focus
                    root = disp.screen().root
                    event = protocol.event.KeyPress(
                        time=0,
                        root=root, window=window, same_screen=0, child=X.NONE,
                        root_x=0, root_y=0, event_x=0, event_y=0,
                        state=0, detail=keycode
                    )
                    disp.send_event(window, event, propagate=True)
                    disp.sync()
                    event = protocol.event.KeyRelease(
                        time=0,
                        root=root, window=window, same_screen=0, child=X.NONE,
                        root_x=0, root_y=0, event_x=0, event_y=0,
                        state=0, detail=keycode
                    )
                    disp.send_event(window, event, propagate=True)
                    disp.sync()
    
    
    def main():
        global display, root
        display = Display()
        root = display.screen().root
    
        ctx = display.record_create_context(
            0,
            [record.AllClients],
            [{
                'core_requests': (0, 0),
                'core_replies': (0, 0),
                'ext_requests': (0, 0, 0, 0),
                'ext_replies': (0, 0, 0, 0),
                'delivered_events': (0, 0),
                'device_events': (X.KeyReleaseMask, X.ButtonReleaseMask),
                'errors': (0, 0),
                'client_started': False,
                'client_died': False,
            }]
        )
        display.record_enable_context(ctx, handler)
    
    
    if __name__ == '__main__':
        main()