2

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()
simonzack
  • 19,729
  • 13
  • 73
  • 118

1 Answers1

2

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()
simonzack
  • 19,729
  • 13
  • 73
  • 118
  • You can improve the code in your answer by removing the unnecessary `while True:` loop. If you want to fix the window receiving the second `a` you can move the lines assigning `disp`, `window`, and `root` out of the handler function (don't forget to rename root to avoid conflicts). The question which remains open is: how to stop a keystroke from being propagated to the currently focused window, so that you can translate a keystroke to another keystroke and not only add an additional one. – Claudio Jun 07 '23 at 14:24
  • Thanks for the suggestions. I removed `while True:`. Moving `disp` and `root` appears to break some things. – simonzack Jun 07 '23 at 15:09
  • Test it yourself if you can confirm that the `display.record_enable_context(ctx, handler)` line is the last line executed in the Python script. The line `display.record_free_context(ctx)` and the line `event = root.display.next_event()` are never reached, so you can remove them like you have removed the loop. – Claudio Jun 07 '23 at 19:59
  • By the way: `dir(display)` does not list the `display.record_enable_context()` and `display.record_create_context()` methods. How do you know about them and how to get other ones available, but not listed by dir(display)? – Claudio Jun 07 '23 at 20:04
  • I suppose you need to call the `record_free_context()` in some kind of onExit() function to achieve a clean exit of the script. Or you can use the Python atexit module for this purpose. – Claudio Jun 07 '23 at 20:09
  • I found them from https://stackoverflow.com/a/22368676. – simonzack Jun 08 '23 at 13:30