Listening for global key-combinations in python on Linux
Asked Answered
C

3

6

I just wrote a little program which downloads a new wallpaper from flickr every few minutes.

Now I want to add the capability to "like" a wallpaper, so it will occur more often than non-liked or disliked wallpapers.

I'd like to assign a global keyboard-shortcut to this function.

For example: If I press ctrl+7, it would execute some kind of "like" function in Python.

Are there any libraries for this (in JavaScript for example there's a library where I can define shortcuts with shortcut("ctrl-b", someFunction);)

Otherwise, how would I go round doing this? I've seen this similar SO question, but it's old.

Christman answered 13/3, 2014 at 1:29 Comment(0)
T
7

I do not know of any libraries that are designed to be extended. However as your link stated the backend of pykeylogger gives an example of how to do it, but it does seem a little overcomplicated for you would need.

pykeylogger uses the python-xlib module to capture keypresses on the X display. Someone has already created a lighter example of how to go through this on pastebin. Below is the source from it copied here as-is.

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

disp = None

def handler(reply):
    """ This function is called when a xlib event is fired """
    data = reply.data
    while len(data):
        event, data = rq.EventField(None).parse_binary_value(data, disp.display, None, None)

        # KEYCODE IS FOUND USERING event.detail
        print(event.detail)

        if event.type == X.KeyPress:
            # BUTTON PRESSED
            print("pressed")
        elif event.type == X.KeyRelease:
            # BUTTON RELEASED
            print("released")

# get current display
disp = Display()
root = disp.screen().root

# Monitor keypress and button press
ctx = disp.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,
            }])
disp.record_enable_context(ctx, handler)
disp.record_free_context(ctx)

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

You will have to extend the handler to fit your needs for instead of just printing to screen, and then make it into a separate thread.

The (painful) alternative is to listen to the keyboard directly, without relying on external libraries or the X session. In linux everything is a file and your keyboard input will be in /dev/input that you could read as a file, for example open('/dev/input/even2', 'rb'), in the background. This is not suggested as it requires escalated permissions, figuring out which device is the keyboard, and then create your own keymapping. Just wanted to let you know what's possible if necessary.

Edit: Also found Global keybinding on X using Python gtk3 which seems to have more example goodness.

Tideway answered 13/3, 2014 at 3:48 Comment(0)
C
5

For those wanting to see the code I ended up using, I made a little gist right here.

Keycodes might differ a bit per computer. Also, the keylistener class I quickly made up isn't protected against all kinds of problems that can occur. Then again, it's working pretty well for me.

Christman answered 18/3, 2014 at 1:23 Comment(0)
B
1

Here is a cleaner version which also decodes and prints the key names, instead of just numbers:

#!/usr/bin/env python2

# Captures all keyboard and mouse events, including modifiers
# Adapted from http://stackoverflow.com/questions/22367358/
# Requires python-xlib

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


class Listener:
  def __init__(self):
    self.disp = None
    self.keys_down = set()

  def keycode_to_key(self, keycode, state):
    i = 0
    if state & X.ShiftMask:
      i += 1
    if state & X.Mod1Mask:
      i += 2
    return self.disp.keycode_to_keysym(keycode, i)

  def key_to_string(self, key):
    keys = []
    for name in dir(XK):
      if name.startswith("XK_") and getattr(XK, name) == key:
        keys.append(name.lstrip("XK_").replace("_L", "").replace("_R", ""))
    if keys:
      return " or ".join(keys)
    return "[%d]" % key

  def keycode_to_string(self, keycode, state):
    return self.key_to_string(self.keycode_to_key(keycode, state))

  def mouse_to_string(self, code):
    if code == X.Button1:
      return "Button1"
    elif code == X.Button2:
      return "Button2"
    elif code == X.Button3:
      return "Button3"
    elif code == X.Button4:
      return "Button4"
    elif code == X.Button5:
      return "Button5"
    else:
      return "{%d}" % code

  def down(self, key):
    self.keys_down.add(key)
    self.print_keys()

  def up(self, key):
    if key in self.keys_down:
      self.keys_down.remove(key)
      self.print_keys()

  def print_keys(self):
    keys = list(self.keys_down)
    print "Currently pressed:", ", ".join(keys)

  def event_handler(self, reply):
    data = reply.data
    while data:
      event, data = rq.EventField(None).parse_binary_value(data, self.disp.display, None, None)
      if event.type == X.KeyPress:
        self.down(self.keycode_to_string(event.detail, event.state))
      elif event.type == X.KeyRelease:
        self.up(self.keycode_to_string(event.detail, event.state))
      elif event.type == X.ButtonPress:
        self.down(self.mouse_to_string(event.detail))
      elif event.type == X.ButtonRelease:
        self.up(self.mouse_to_string(event.detail))

  def run(self):
    self.disp = Display()
    XK.load_keysym_group('xf86')
    root = self.disp.screen().root
    ctx = self.disp.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,
                                          }])
    self.disp.record_enable_context(ctx, lambda reply: self.event_handler(reply))
    self.disp.record_free_context(ctx)
    while True:
      event = root.display.next_event()


if __name__ == "__main__":
  Listener().run()

The output looks like this:

Currently pressed: Alt
Currently pressed:
Currently pressed: Alt
Currently pressed: Alt, Tab
Currently pressed: Alt
Currently pressed: Alt, Tab
Currently pressed: Alt
Currently pressed: Alt, Tab
Currently pressed: Alt
Currently pressed: Alt, Tab
Currently pressed: Alt
Currently pressed: Alt, Tab
Currently pressed: Alt
Currently pressed:
Currently pressed: Control
Currently pressed: Control, a
Currently pressed: Control
Currently pressed: Control, Shift
Currently pressed: Control, Shift, A
Currently pressed: Control, Shift
Banyan answered 9/5, 2017 at 22:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.