__init__.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. """
  2. rescriptoon
  3. Copyright (C) 2018-2019 Fabian Peter Hammerle <fabian@hammerle.me>
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. """
  15. import copy
  16. import logging
  17. import os
  18. import select
  19. import time
  20. import typing
  21. import Xlib.display
  22. from Xlib import XK, X, Xatom
  23. from rescriptoon._actions import (
  24. LowThrowAction,
  25. RewriteKeyEventAction,
  26. SelectGagAction,
  27. ToggleOverlayAction,
  28. )
  29. from rescriptoon._keys import keysym_to_label
  30. EXTENDED_CONTROLS_DEFAULT_TOGGLE_KEYSYM_NAME = "grave"
  31. TOONTOWN_WINDOW_NAME = "Toontown Rewritten"
  32. _KEYSYM_ACTION_MAPPINGS = {
  33. # pylint: disable=no-member; false positive for XK.*
  34. XK.XK_w: RewriteKeyEventAction(keysym=XK.XK_Up, target_engine_index=0),
  35. XK.XK_a: RewriteKeyEventAction(keysym=XK.XK_Left, target_engine_index=0),
  36. XK.XK_s: RewriteKeyEventAction(keysym=XK.XK_Down, target_engine_index=0),
  37. XK.XK_d: RewriteKeyEventAction(keysym=XK.XK_Right, target_engine_index=0),
  38. XK.XK_Control_L: RewriteKeyEventAction(
  39. keysym=XK.XK_Control_L, target_engine_index=0
  40. ),
  41. XK.XK_v: LowThrowAction(target_engine_index=0),
  42. XK.XK_o: RewriteKeyEventAction(keysym=XK.XK_Up, target_engine_index=1),
  43. XK.XK_k: RewriteKeyEventAction(keysym=XK.XK_Left, target_engine_index=1),
  44. XK.XK_l: RewriteKeyEventAction(keysym=XK.XK_Down, target_engine_index=1),
  45. XK.XK_semicolon: RewriteKeyEventAction(keysym=XK.XK_Right, target_engine_index=1),
  46. XK.XK_slash: RewriteKeyEventAction(keysym=XK.XK_Control_L, target_engine_index=1),
  47. XK.XK_n: LowThrowAction(target_engine_index=1),
  48. XK.XK_space: RewriteKeyEventAction(keysym=XK.XK_Control_L),
  49. # TODO replace gag_name with enum
  50. XK.XK_e: SelectGagAction(
  51. gag_name="elephant trunk",
  52. target_engine_index=0,
  53. column_index=4,
  54. factor_y=-0.047,
  55. ),
  56. XK.XK_i: SelectGagAction(
  57. gag_name="elephant trunk",
  58. target_engine_index=1,
  59. column_index=4,
  60. factor_y=-0.047,
  61. ),
  62. XK.XK_f: SelectGagAction(
  63. gag_name="foghorn", target_engine_index=0, column_index=5, factor_y=-0.047
  64. ),
  65. XK.XK_j: SelectGagAction(
  66. gag_name="foghorn", target_engine_index=1, column_index=5, factor_y=-0.047
  67. ),
  68. }
  69. def _x_walk_children_windows(
  70. parent_window: "Xlib.display.Window",
  71. ) -> typing.Iterator["Xlib.display.Window"]:
  72. yield parent_window
  73. for child_window in parent_window.query_tree().children:
  74. for subchild_window in _x_walk_children_windows(child_window):
  75. yield subchild_window
  76. def _x_wait_for_event(xdisplay, timeout_seconds):
  77. """ Wait up to `timeout_seconds` seconds for a xevent.
  78. Return True, if a xevent is available.
  79. Return False, if the timeout was reached. """
  80. rlist = select.select(
  81. [xdisplay.display.socket], # rlist
  82. [], # wlist
  83. [], # xlist
  84. timeout_seconds, # timeout [seconds]
  85. )[0]
  86. return len(rlist) > 0
  87. class Overlay:
  88. def __init__(
  89. self, toggle_keysym_name=EXTENDED_CONTROLS_DEFAULT_TOGGLE_KEYSYM_NAME,
  90. ):
  91. self._xdisplay = Xlib.display.Display()
  92. self._toggle_keysym = XK.string_to_keysym(toggle_keysym_name)
  93. if self._toggle_keysym == X.NoSymbol:
  94. raise ValueError(
  95. "rescriptoon controls toggle:"
  96. + " unknown keysym name '{}'".format(toggle_keysym_name)
  97. )
  98. self._keysym_mappings = copy.deepcopy(_KEYSYM_ACTION_MAPPINGS)
  99. if self._toggle_keysym in self._keysym_mappings:
  100. logging.warning("ignoring mapping for toggle key %s", toggle_keysym_name)
  101. self._keysym_mappings[self._toggle_keysym] = ToggleOverlayAction()
  102. self._active_key_registry = {}
  103. self._enabled = False
  104. self._engine_windows = None
  105. @property
  106. def xdisplay(self):
  107. return self._xdisplay
  108. @property
  109. def engine_windows(self) -> typing.List["Xlib.display.Window"]:
  110. return self._engine_windows
  111. @property
  112. def _engine_windows_open(self) -> bool:
  113. for window in self._engine_windows:
  114. try:
  115. window.get_wm_state()
  116. except Xlib.error.BadWindow:
  117. logging.info("engine window %x is no longer available", window.id)
  118. return False
  119. return True
  120. def run(self) -> None:
  121. self._engine_windows = self._find_engine_windows()
  122. logging.debug("engine window ids %r", [hex(w.id) for w in self._engine_windows])
  123. if not self._engine_windows:
  124. raise Exception("no toontown window found")
  125. self._grab_key(self.xdisplay.keysym_to_keycode(self._toggle_keysym),)
  126. print("key bindings:")
  127. for keysym, action in self._keysym_mappings.items():
  128. print("{}: {}".format(keysym_to_label(keysym), action.description))
  129. self.enable()
  130. while self._engine_windows_open:
  131. while self.xdisplay.pending_events():
  132. self._handle_xevent(self.xdisplay.next_event())
  133. self._check_active_key_registry()
  134. # keep timeout low for _check_active_key_registry()
  135. # to be called frequently
  136. _x_wait_for_event(self.xdisplay, timeout_seconds=0.05)
  137. self._disable()
  138. def _handle_xevent(self, xevent: Xlib.protocol.rq.Event) -> None:
  139. if isinstance(
  140. xevent, (Xlib.protocol.event.KeyPress, Xlib.protocol.event.KeyRelease)
  141. ):
  142. self._handle_xkeyevent(xevent)
  143. def _handle_xkeyevent(
  144. self,
  145. xkeyevent: typing.Union[
  146. Xlib.protocol.event.KeyPress, Xlib.protocol.event.KeyRelease
  147. ],
  148. ) -> None:
  149. self._update_active_key_registry(xkeyevent)
  150. keysym_in = self.xdisplay.keycode_to_keysym(xkeyevent.detail, index=0,)
  151. try:
  152. action = self._keysym_mappings[keysym_in]
  153. except KeyError:
  154. logging.warning("received key event of unmapped keysym %d", keysym_in)
  155. return
  156. action.execute(self, xkeyevent)
  157. @property
  158. def _toggle_keysym_name(self) -> str:
  159. return XK.keysym_to_string(self._toggle_keysym)
  160. def enable(self):
  161. for keysym in self._keysym_mappings.keys():
  162. if keysym != self._toggle_keysym:
  163. self._grab_key(self.xdisplay.keysym_to_keycode(keysym),)
  164. self._enabled = True
  165. logging.info(
  166. "rescriptoon is now enabled. press %s to disable.",
  167. self._toggle_keysym_name,
  168. )
  169. def _disable(self):
  170. for keysym in self._keysym_mappings.keys():
  171. if keysym != self._toggle_keysym:
  172. self._ungrab_key(self.xdisplay.keysym_to_keycode(keysym),)
  173. self._enabled = False
  174. def disable(self):
  175. self._disable()
  176. logging.info(
  177. "rescriptoon is now disabled. press %s to enable.",
  178. self._toggle_keysym_name,
  179. )
  180. @property
  181. def enabled(self):
  182. return self._enabled
  183. def toggle(self):
  184. if self.enabled:
  185. self.disable()
  186. else:
  187. self.enable()
  188. def _grab_key(self, keycode):
  189. for window in self._engine_windows:
  190. window.grab_key(
  191. keycode,
  192. X.AnyModifier,
  193. # owner_events
  194. # https://stackoverflow.com/questions/32122360/x11-will-xgrabpointer-prevent-other-apps-from-any-mouse-event
  195. # False,
  196. True,
  197. X.GrabModeAsync,
  198. X.GrabModeAsync,
  199. )
  200. def _ungrab_key(self, keycode):
  201. for window in self._engine_windows:
  202. window.ungrab_key(keycode, X.AnyModifier)
  203. def _find_engine_windows(self) -> typing.List["Xlib.display.Window"]:
  204. return list(
  205. filter(
  206. lambda w: w.get_wm_name() == TOONTOWN_WINDOW_NAME,
  207. _x_walk_children_windows(self.xdisplay.screen().root),
  208. )
  209. )
  210. def _update_active_key_registry(self, xkeyevent):
  211. # see self._check_active_key_registry
  212. keycode = xkeyevent.detail
  213. if isinstance(xkeyevent, Xlib.protocol.event.KeyPress):
  214. self._active_key_registry[keycode] = xkeyevent
  215. elif keycode in self._active_key_registry:
  216. del self._active_key_registry[keycode]
  217. def _check_active_key_registry(self):
  218. """
  219. WORKAROUND
  220. For an unknown reason some key release events don't get queued
  221. when multiple keys are being released simultaneously.
  222. So we keep a hashmap of supposedly currently pressed keys
  223. and periodically compare it with xdispaly.query_keymap().
  224. ref: https://stackoverflow.com/q/18160792/5894777
  225. """
  226. # https://tronche.com/gui/x/xlib/input/XQueryKeymap.html
  227. keymap = self.xdisplay.query_keymap()
  228. missed_releases = []
  229. for keycode, press_event in self._active_key_registry.items():
  230. byte_index = keycode >> 3
  231. bit_index = keycode & ((1 << 3) - 1)
  232. if not keymap[byte_index] & (1 << bit_index):
  233. print("DEBUG missed release event of key {}".format(keycode))
  234. missed_releases.append(
  235. Xlib.protocol.event.KeyRelease(
  236. window=press_event.window,
  237. detail=press_event.detail,
  238. state=press_event.state,
  239. root_x=press_event.root_x,
  240. root_y=press_event.root_y,
  241. event_x=press_event.event_x,
  242. event_y=press_event.event_y,
  243. child=press_event.child,
  244. root=press_event.root,
  245. time=X.CurrentTime,
  246. same_screen=press_event.same_screen,
  247. )
  248. )
  249. for release_event in missed_releases:
  250. self._handle_xkeyevent(release_event)