import copy import select import time from tooncher.actions import * try: import psutil except ImportError: psutil = False try: import Xlib.display from Xlib import X, XK except ImportError: Xlib = False EXTENDED_CONTROLS_DEFAULT_TOGGLE_KEYSYM_NAME = 'grave' if Xlib: EXTENDED_CONTROLS_DEFAULT_KEYSYM_MAPPINGS = { XK.XK_w: RewriteKeyEventAction(keysym=XK.XK_Up), XK.XK_a: RewriteKeyEventAction(keysym=XK.XK_Left), XK.XK_s: RewriteKeyEventAction(keysym=XK.XK_Down), XK.XK_d: RewriteKeyEventAction(keysym=XK.XK_Right), XK.XK_f: SelectFogAction(), } def x_find_window(parent_window, filter_callback): matching = [] for child_window in parent_window.query_tree().children: if filter_callback(child_window): matching.append(child_window) matching += x_find_window(child_window, filter_callback) return matching def x_find_window_by_pid(display, pid): pid_prop = display.intern_atom('_NET_WM_PID') def filter_callback(window): prop = window.get_full_property(pid_prop, X.AnyPropertyType) return prop and prop.value.tolist() == [pid] return x_find_window(display.screen().root, filter_callback) def x_wait_for_event(xdisplay, timeout_seconds): """ Wait up to `timeout_seconds` seconds for a xevent. Return True, if a xevent is available. Return False, if the timeout was reached. """ rlist = select.select( [xdisplay.display.socket], # rlist [], # wlist [], # xlist timeout_seconds, # timeout [seconds] )[0] return len(rlist) > 0 class ExtendedControls: def __init__(self, engine_pid, toggle_keysym_name=EXTENDED_CONTROLS_DEFAULT_TOGGLE_KEYSYM_NAME): if not psutil: raise Exception('\n'.join([ 'Extended keyboard controls require the python lib psutil to be installed.', 'Depending on your system run', '\t$ sudo apt-get install python3-psutil', 'or', '\t$ pip3 install --user psutil', ])) if not Xlib: raise Exception('\n'.join([ 'Extended keyboard controls require xlib for python to be installed.', 'Depending on your system run', '\t$ sudo apt-get install python3-xlib', 'or', '\t$ pip3 install --user xlib', ])) self._engine_pid = engine_pid self._xdisplay = Xlib.display.Display() self._toggle_keysym = XK.string_to_keysym(toggle_keysym_name) if self._toggle_keysym == X.NoSymbol: raise Exception("Extended keyboard controls toggle:" + " Unknown keysym name '{}'".format(toggle_keysym_name)) self._keysym_mappings = copy.deepcopy( EXTENDED_CONTROLS_DEFAULT_KEYSYM_MAPPINGS, ) if self._toggle_keysym in self._keysym_mappings: print("INFO Extended Controls:" + " Ignoring mapping for toggle key '{}'".format(toggle_keysym_name)) self._keysym_mappings[self._toggle_keysym] \ = ToggleExtendedControlsAction() self._default_action = ForwardKeyEventAction() self._engine_window = None self._enabled = False @property def engine_running(self): return psutil.pid_exists(self._engine_pid) @property def xdisplay(self): return self._xdisplay @property def engine_window(self): return self._engine_window def _wait_for_engine_window(self, timeout_seconds=20, search_interval_seconds=2): start_epoch = time.time() while self.engine_running and (time.time() - start_epoch) <= timeout_seconds: windows = x_find_window_by_pid( self._xdisplay, self._engine_pid, ) assert len(windows) <= 1 if len(windows) == 1: return windows[0] time.sleep(search_interval_seconds) return None def run(self): self._engine_window = self._wait_for_engine_window() if not self._engine_window: raise Exception('Could not find the game\'s window.') self._grab_key( self._xdisplay.keysym_to_keycode(self._toggle_keysym), ) if not self.enabled: keysym_name = XK.keysym_to_string(self._toggle_keysym) print("INFO Extended Controls are currently disabled." + " Press key '{}' to enable.".format(keysym_name)) while self.engine_running: while self.xdisplay.pending_events(): self._handle_xevent(self._xdisplay.next_event()) x_wait_for_event(self.xdisplay, timeout_seconds=1) def _handle_xevent(self, xevent): # TODO investigate why some release events get lost # https://stackoverflow.com/q/18160792/5894777 if isinstance(xevent, Xlib.protocol.event.KeyPress) \ or isinstance(xevent, Xlib.protocol.event.KeyRelease): self._handle_xkeyevent(xevent) def _handle_xkeyevent(self, xkeyevent): keysym_in = self._xdisplay.keycode_to_keysym( xkeyevent.detail, index=0, ) if keysym_in in self._keysym_mappings: action = self._keysym_mappings[keysym_in] else: action = self._default_action action.execute(self, xkeyevent) def enable(self): for keysym in self._keysym_mappings.keys(): if keysym != self._toggle_keysym: self._grab_key( self._xdisplay.keysym_to_keycode(keysym), ) self._enabled = True print("INFO Enabled Extended Controls") def disable(self): for keysym in self._keysym_mappings.keys(): if keysym != self._toggle_keysym: self._ungrab_key( self._xdisplay.keysym_to_keycode(keysym), ) self._enabled = False print("INFO Disabled Extended Controls") @property def enabled(self): return self._enabled def toggle(self): if self.enabled: self.disable() else: self.enable() def _grab_key(self, keycode): self._engine_window.grab_key( keycode, X.AnyModifier, # owner_events # https://stackoverflow.com/questions/32122360/x11-will-xgrabpointer-prevent-other-apps-from-any-mouse-event # False, True, X.GrabModeAsync, X.GrabModeAsync, ) def _ungrab_key(self, keycode): self._engine_window.ungrab_key(keycode, X.AnyModifier)