globally capture, ignore and send keyevents with python xlib, recognize fake input
Asked Answered
M

1

8

i want to implement key chording on a normal keyboard and i thought i use python xlib. for this to work the program has to globally swallow all keyevents and only later allow them to go through.

my current test just grabs the "1" key. if this key is pressed it calls a handler which sends "x" to the focused window via xtest.fake_input. because im only grabbing the "1"-key, there shouldn't be a problem, right? but somehow the handler gets called again, because "x" was pressed. in fact, the moment i type "1" the program is listening to all keys. this could have something to do with calling

display.allow_events(X.ReplayKeyboard, X.CurrentTime)

after handling an event, but if i don't do this, everything freezes.

for the final program the change in the listening behaviour is not really relevant, but i have to be able to distinguish the fake events from the user events. to do this i'm just fast forwarding display.next_event(), but this isnt ideal, because the user could be typing at that exact moment and than those keystrokes would be lost.

i tried releasing the keygrab during sending and emptying the eventqueue via

display.flush()
display.sync()

but that doesn't do anything.

so, any idea how to recognize or ignore fake input events and why i'm suddenly listening to all keypresses (and releases)?

xlib is very frustrating.

from Xlib.display import Display
import Xlib
from Xlib import X
import Xlib.XK
import sys
import signal 

display = None
root = None

def handle_event(aEvent):
    print "handle!"
    send_key("x")

def send_key(emulated_key):

    global display,root
    print "send key"
    # ungrabbing doesnt help
    root.ungrab_key(10,X.AnyModifier) 
    window = display.get_input_focus()._data["focus"]
    # Generate the correct keycode
    keysym = Xlib.XK.string_to_keysym(emulated_key)
    keycode = display.keysym_to_keycode(keysym)
    # Send a fake keypress via xtestaaa
    Xlib.ext.xtest.fake_input(window, Xlib.X.KeyPress, keycode)
    Xlib.ext.xtest.fake_input(window, Xlib.X.KeyRelease, keycode)
    display.flush()
    display.sync()
    # fast forward those two events,this seems a bit hacky, 
    # what if theres another keyevent coming in at that exact time?
    while display.pending_events():
        display.next_event()
    #
    root.grab_key(10, X.AnyModifier, True,X.GrabModeSync, X.GrabModeSync)

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

    # we tell the X server we want to catch keyPress event
    root.change_attributes(event_mask = X.KeyPressMask)
    # just grab the "1"-key for now
    root.grab_key(10, X.AnyModifier, True,X.GrabModeSync, X.GrabModeSync)
    # while experimenting everything could freeze, so exit after 10 seconds
    signal.signal(signal.SIGALRM, lambda a,b:sys.exit(1))
    signal.alarm(10)
    while 1:
        event = display.next_event()
        print "event"
        #if i dont call this, the whole thing freezes
        display.allow_events(X.ReplayKeyboard, X.CurrentTime)
        handle_event(event)

if __name__ == '__main__':
    main()
Milfordmilhaud answered 4/8, 2013 at 1:39 Comment(0)
M
6

i found the problem. im almost certain that xtest.fake_input does something weird, because when i send keypresses and -releases manually (with some code i found), it works

here is an example, that swallows only the "1"-key on keypress and sends "x" on keyrelease to the focused window:

from Xlib.display import Display
import Xlib
from Xlib import X
import Xlib.XK
import sys
import signal 
import time
display = None
root = None

def handle_event(event):
    print "handle!"
    if (event.type == X.KeyRelease):
        send_key("x")

# from http://shallowsky.com/software/crikey/pykey-0.1 
def send_key(emulated_key):
    shift_mask = 0 # or Xlib.X.ShiftMask
    window = display.get_input_focus()._data["focus"]
    keysym = Xlib.XK.string_to_keysym(emulated_key)
    keycode = display.keysym_to_keycode(keysym)
    event = Xlib.protocol.event.KeyPress(
        time = int(time.time()),
        root = root,
        window = window,
        same_screen = 0, child = Xlib.X.NONE,
        root_x = 0, root_y = 0, event_x = 0, event_y = 0,
        state = shift_mask,
        detail = keycode
        )
    window.send_event(event, propagate = True)
    event = Xlib.protocol.event.KeyRelease(
        time = int(time.time()),
        root = display.screen().root,
        window = window,
        same_screen = 0, child = Xlib.X.NONE,
        root_x = 0, root_y = 0, event_x = 0, event_y = 0,
        state = shift_mask,
        detail = keycode
        )
    window.send_event(event, propagate = True)

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

    # we tell the X server we want to catch keyPress event
    root.change_attributes(event_mask = X.KeyPressMask|X.KeyReleaseMask)
    # just grab the "1"-key for now
    root.grab_key(10, 0, True,X.GrabModeSync, X.GrabModeSync)

    signal.signal(signal.SIGALRM, lambda a,b:sys.exit(1))
    signal.alarm(10)
    while 1:
        event = display.next_event()
        print "event"
        handle_event(event)
        display.allow_events(X.AsyncKeyboard, X.CurrentTime)            

if __name__ == '__main__':
    main()
Milfordmilhaud answered 5/8, 2013 at 15:50 Comment(4)
This seems to crash silently after some time or after mashing 1 and other keys with key repeats (can't tell which). Another user also reported this, but his comment was deleted because it was posted as an answer.Italy
@Italy I have the same issue. I also did not figure out which key combination. But I had it fail after 20 minutes and after a minute and a half. Seems to be either random or dependent on keyboard/mouse usage.Minter
Replacing time=int(time.time()) by time = X.CurrentTime resolved the issue for me.Roberge
This keygrabbing code works for me: larsen-b.com/Article/184.htmlBrooking

© 2022 - 2024 — McMap. All rights reserved.