pyxhook.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. #!/usr/bin/python
  2. #
  3. # pyxhook -- an extension to emulate some of the PyHook library on linux.
  4. #
  5. # Copyright (C) 2008 Tim Alexander <dragonfyre13@gmail.com>
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program; if not, write to the Free Software
  19. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  20. #
  21. # Thanks to Alex Badea <vamposdecampos@gmail.com> for writing the Record
  22. # demo for the xlib libraries. It helped me immensely working with these
  23. # in this library.
  24. #
  25. # Thanks to the python-xlib team. This wouldn't have been possible without
  26. # your code.
  27. #
  28. # This requires:
  29. # at least python-xlib 1.4
  30. # xwindows must have the "record" extension present, and active.
  31. #
  32. # This file has now been somewhat extensively modified by
  33. # Daniel Folkinshteyn <nanotube@users.sf.net>
  34. # So if there are any bugs, they are probably my fault. :)
  35. from __future__ import print_function
  36. import sys
  37. import re
  38. import time
  39. import threading
  40. from Xlib import X, XK, display
  41. from Xlib.ext import record
  42. from Xlib.protocol import rq
  43. #######################################################################
  44. # #######################START CLASS DEF###############################
  45. #######################################################################
  46. class HookManager(threading.Thread):
  47. """ This is the main class. Instantiate it, and you can hand it KeyDown
  48. and KeyUp (functions in your own code) which execute to parse the
  49. pyxhookkeyevent class that is returned.
  50. This simply takes these two values for now:
  51. KeyDown : The function to execute when a key is pressed, if it
  52. returns anything. It hands the function an argument that
  53. is the pyxhookkeyevent class.
  54. KeyUp : The function to execute when a key is released, if it
  55. returns anything. It hands the function an argument that is
  56. the pyxhookkeyevent class.
  57. """
  58. def __init__(self, parameters=False):
  59. threading.Thread.__init__(self)
  60. self.finished = threading.Event()
  61. # Give these some initial values
  62. self.mouse_position_x = 0
  63. self.mouse_position_y = 0
  64. self.ison = {"shift": False, "caps": False}
  65. # Compile our regex statements.
  66. self.isshift = re.compile('^Shift')
  67. self.iscaps = re.compile('^Caps_Lock')
  68. self.shiftablechar = re.compile('|'.join((
  69. '^[a-z0-9]$',
  70. '^minus$',
  71. '^equal$',
  72. '^bracketleft$',
  73. '^bracketright$',
  74. '^semicolon$',
  75. '^backslash$',
  76. '^apostrophe$',
  77. '^comma$',
  78. '^period$',
  79. '^slash$',
  80. '^grave$'
  81. )))
  82. self.logrelease = re.compile('.*')
  83. self.isspace = re.compile('^space$')
  84. # Choose which type of function use
  85. self.parameters = parameters
  86. if parameters:
  87. self.lambda_function = lambda x, y: True
  88. else:
  89. self.lambda_function = lambda x: True
  90. # Assign default function actions (do nothing).
  91. self.KeyDown = self.lambda_function
  92. self.KeyUp = self.lambda_function
  93. self.MouseAllButtonsDown = self.lambda_function
  94. self.MouseAllButtonsUp = self.lambda_function
  95. self.MouseMovement = self.lambda_function
  96. self.KeyDownParameters = {}
  97. self.KeyUpParameters = {}
  98. self.MouseAllButtonsDownParameters = {}
  99. self.MouseAllButtonsUpParameters = {}
  100. self.MouseMovementParameters = {}
  101. self.contextEventMask = [X.KeyPress, X.MotionNotify]
  102. # Hook to our display.
  103. self.local_dpy = display.Display()
  104. self.record_dpy = display.Display()
  105. def run(self):
  106. # Check if the extension is present
  107. if not self.record_dpy.has_extension("RECORD"):
  108. print("RECORD extension not found", file=sys.stderr)
  109. sys.exit(1)
  110. # r = self.record_dpy.record_get_version(0, 0)
  111. # print("RECORD extension version {major}.{minor}".format(
  112. # major=r.major_version,
  113. # minor=r.minor_version
  114. # ))
  115. # Create a recording context; we only want key and mouse events
  116. self.ctx = self.record_dpy.record_create_context(
  117. 0,
  118. [record.AllClients],
  119. [{
  120. 'core_requests': (0, 0),
  121. 'core_replies': (0, 0),
  122. 'ext_requests': (0, 0, 0, 0),
  123. 'ext_replies': (0, 0, 0, 0),
  124. 'delivered_events': (0, 0),
  125. # (X.KeyPress, X.ButtonPress),
  126. 'device_events': tuple(self.contextEventMask),
  127. 'errors': (0, 0),
  128. 'client_started': False,
  129. 'client_died': False,
  130. }])
  131. # Enable the context; this only returns after a call to
  132. # record_disable_context, while calling the callback function in the
  133. # meantime
  134. self.record_dpy.record_enable_context(self.ctx, self.processevents)
  135. # Finally free the context
  136. self.record_dpy.record_free_context(self.ctx)
  137. def cancel(self):
  138. self.finished.set()
  139. self.local_dpy.record_disable_context(self.ctx)
  140. self.local_dpy.flush()
  141. def printevent(self, event):
  142. print(event)
  143. def HookKeyboard(self):
  144. # We don't need to do anything here anymore, since the default mask
  145. # is now set to contain X.KeyPress
  146. # self.contextEventMask[0] = X.KeyPress
  147. pass
  148. def HookMouse(self):
  149. # We don't need to do anything here anymore, since the default mask
  150. # is now set to contain X.MotionNotify
  151. # need mouse motion to track pointer position, since ButtonPress
  152. # events don't carry that info.
  153. # self.contextEventMask[1] = X.MotionNotify
  154. pass
  155. def processhookevents(self, action_type, action_parameters, events):
  156. # In order to avoid duplicate code, i wrote a function that takes the
  157. # input value of the action function and, depending on the initialization,
  158. # launches it or only with the event or passes the parameter
  159. if self.parameters:
  160. action_type(events, action_parameters)
  161. else:
  162. action_type(events)
  163. def processevents(self, reply):
  164. if reply.category != record.FromServer:
  165. return
  166. if reply.client_swapped:
  167. print("* received swapped protocol data, cowardly ignored")
  168. return
  169. try:
  170. # Get int value, python2.
  171. intval = ord(reply.data[0])
  172. except TypeError:
  173. # Already bytes/ints, python3.
  174. intval = reply.data[0]
  175. if (not reply.data) or (intval < 2):
  176. # not an event
  177. return
  178. data = reply.data
  179. while len(data):
  180. event, data = rq.EventField(None).parse_binary_value(
  181. data,
  182. self.record_dpy.display,
  183. None,
  184. None
  185. )
  186. if event.type == X.KeyPress:
  187. hookevent = self.keypressevent(event)
  188. self.processhookevents(
  189. self.KeyDown, self.KeyDownParameters, hookevent)
  190. elif event.type == X.KeyRelease:
  191. hookevent = self.keyreleaseevent(event)
  192. self.processhookevents(
  193. self.KeyUp, self.KeyUpParameters, hookevent)
  194. elif event.type == X.ButtonPress:
  195. hookevent = self.buttonpressevent(event)
  196. self.processhookevents(
  197. self.MouseAllButtonsDown, self.MouseAllButtonsDownParameters, hookevent)
  198. elif event.type == X.ButtonRelease:
  199. hookevent = self.buttonreleaseevent(event)
  200. self.processhookevents(
  201. self.MouseAllButtonsUp, self.MouseAllButtonsUpParameters, hookevent)
  202. elif event.type == X.MotionNotify:
  203. # use mouse moves to record mouse position, since press and
  204. # release events do not give mouse position info
  205. # (event.root_x and event.root_y have bogus info).
  206. hookevent = self.mousemoveevent(event)
  207. self.processhookevents(
  208. self.MouseMovement, self.MouseMovementParameters, hookevent)
  209. # print("processing events...", event.type)
  210. def keypressevent(self, event):
  211. matchto = self.lookup_keysym(
  212. self.local_dpy.keycode_to_keysym(event.detail, 0)
  213. )
  214. if self.shiftablechar.match(
  215. self.lookup_keysym(
  216. self.local_dpy.keycode_to_keysym(event.detail, 0))):
  217. # This is a character that can be typed.
  218. if not self.ison["shift"]:
  219. keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
  220. return self.makekeyhookevent(keysym, event)
  221. else:
  222. keysym = self.local_dpy.keycode_to_keysym(event.detail, 1)
  223. return self.makekeyhookevent(keysym, event)
  224. else:
  225. # Not a typable character.
  226. keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
  227. if self.isshift.match(matchto):
  228. self.ison["shift"] = self.ison["shift"] + 1
  229. elif self.iscaps.match(matchto):
  230. if not self.ison["caps"]:
  231. self.ison["shift"] = self.ison["shift"] + 1
  232. self.ison["caps"] = True
  233. if self.ison["caps"]:
  234. self.ison["shift"] = self.ison["shift"] - 1
  235. self.ison["caps"] = False
  236. return self.makekeyhookevent(keysym, event)
  237. def keyreleaseevent(self, event):
  238. if self.shiftablechar.match(
  239. self.lookup_keysym(
  240. self.local_dpy.keycode_to_keysym(event.detail, 0))):
  241. if not self.ison["shift"]:
  242. keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
  243. else:
  244. keysym = self.local_dpy.keycode_to_keysym(event.detail, 1)
  245. else:
  246. keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
  247. matchto = self.lookup_keysym(keysym)
  248. if self.isshift.match(matchto):
  249. self.ison["shift"] = self.ison["shift"] - 1
  250. return self.makekeyhookevent(keysym, event)
  251. def buttonpressevent(self, event):
  252. # self.clickx = self.rootx
  253. # self.clicky = self.rooty
  254. return self.makemousehookevent(event)
  255. def buttonreleaseevent(self, event):
  256. # if (self.clickx == self.rootx) and (self.clicky == self.rooty):
  257. # # print("ButtonClock {detail} x={s.rootx y={s.rooty}}".format(
  258. # # detail=event.detail,
  259. # # s=self,
  260. # # ))
  261. # if event.detail in (1, 2, 3):
  262. # self.captureclick()
  263. # else:
  264. # pass
  265. # print("ButtonDown {detail} x={s.clickx} y={s.clicky}".format(
  266. # detail=event.detail,
  267. # s=self
  268. # ))
  269. # print("ButtonUp {detail} x={s.rootx} y={s.rooty}".format(
  270. # detail=event.detail,
  271. # s=self
  272. # ))
  273. return self.makemousehookevent(event)
  274. def mousemoveevent(self, event):
  275. self.mouse_position_x = event.root_x
  276. self.mouse_position_y = event.root_y
  277. return self.makemousehookevent(event)
  278. # need the following because XK.keysym_to_string() only does printable
  279. # chars rather than being the correct inverse of XK.string_to_keysym()
  280. def lookup_keysym(self, keysym):
  281. for name in dir(XK):
  282. if name.startswith("XK_") and getattr(XK, name) == keysym:
  283. return name[3:]
  284. return "[{}]".format(keysym)
  285. def asciivalue(self, keysym):
  286. asciinum = XK.string_to_keysym(self.lookup_keysym(keysym))
  287. return asciinum % 256
  288. def makekeyhookevent(self, keysym, event):
  289. storewm = self.xwindowinfo()
  290. if event.type == X.KeyPress:
  291. MessageName = "key down"
  292. elif event.type == X.KeyRelease:
  293. MessageName = "key up"
  294. return pyxhookkeyevent(
  295. storewm["handle"],
  296. storewm["name"],
  297. storewm["class"],
  298. self.lookup_keysym(keysym),
  299. self.asciivalue(keysym),
  300. False,
  301. event.detail,
  302. MessageName
  303. )
  304. def makemousehookevent(self, event):
  305. storewm = self.xwindowinfo()
  306. if event.detail == 1:
  307. MessageName = "mouse left "
  308. elif event.detail == 3:
  309. MessageName = "mouse right "
  310. elif event.detail == 2:
  311. MessageName = "mouse middle "
  312. elif event.detail == 5:
  313. MessageName = "mouse wheel down "
  314. elif event.detail == 4:
  315. MessageName = "mouse wheel up "
  316. else:
  317. MessageName = "mouse {} ".format(event.detail)
  318. if event.type == X.ButtonPress:
  319. MessageName = "{} down".format(MessageName)
  320. elif event.type == X.ButtonRelease:
  321. MessageName = "{} up".format(MessageName)
  322. else:
  323. MessageName = "mouse moved"
  324. return pyxhookmouseevent(
  325. storewm["handle"],
  326. storewm["name"],
  327. storewm["class"],
  328. (self.mouse_position_x, self.mouse_position_y),
  329. MessageName
  330. )
  331. def xwindowinfo(self):
  332. try:
  333. windowvar = self.local_dpy.get_input_focus().focus
  334. wmname = windowvar.get_wm_name()
  335. wmclass = windowvar.get_wm_class()
  336. wmhandle = str(windowvar)[20:30]
  337. except:
  338. # This is to keep things running smoothly.
  339. # It almost never happens, but still...
  340. return {"name": None, "class": None, "handle": None}
  341. if (wmname is None) and (wmclass is None):
  342. try:
  343. windowvar = windowvar.query_tree().parent
  344. wmname = windowvar.get_wm_name()
  345. wmclass = windowvar.get_wm_class()
  346. wmhandle = str(windowvar)[20:30]
  347. except:
  348. # This is to keep things running smoothly.
  349. # It almost never happens, but still...
  350. return {"name": None, "class": None, "handle": None}
  351. if wmclass is None:
  352. return {"name": wmname, "class": wmclass, "handle": wmhandle}
  353. else:
  354. return {"name": wmname, "class": wmclass[0], "handle": wmhandle}
  355. class pyxhookkeyevent:
  356. """ This is the class that is returned with each key event.f
  357. It simply creates the variables below in the class.
  358. Window : The handle of the window.
  359. WindowName : The name of the window.
  360. WindowProcName : The backend process for the window.
  361. Key : The key pressed, shifted to the correct caps value.
  362. Ascii : An ascii representation of the key. It returns 0 if
  363. the ascii value is not between 31 and 256.
  364. KeyID : This is just False for now. Under windows, it is the
  365. Virtual Key Code, but that's a windows-only thing.
  366. ScanCode : Please don't use this. It differs for pretty much
  367. every type of keyboard. X11 abstracts this
  368. information anyway.
  369. MessageName : "key down", "key up".
  370. """
  371. def __init__(
  372. self, Window, WindowName, WindowProcName, Key, Ascii, KeyID,
  373. ScanCode, MessageName):
  374. self.Window = Window
  375. self.WindowName = WindowName
  376. self.WindowProcName = WindowProcName
  377. self.Key = Key
  378. self.Ascii = Ascii
  379. self.KeyID = KeyID
  380. self.ScanCode = ScanCode
  381. self.MessageName = MessageName
  382. def __str__(self):
  383. return '\n'.join((
  384. 'Window Handle: {s.Window}',
  385. 'Window Name: {s.WindowName}',
  386. 'Window\'s Process Name: {s.WindowProcName}',
  387. 'Key Pressed: {s.Key}',
  388. 'Ascii Value: {s.Ascii}',
  389. 'KeyID: {s.KeyID}',
  390. 'ScanCode: {s.ScanCode}',
  391. 'MessageName: {s.MessageName}',
  392. )).format(s=self)
  393. class pyxhookmouseevent:
  394. """This is the class that is returned with each key event.f
  395. It simply creates the variables below in the class.
  396. Window : The handle of the window.
  397. WindowName : The name of the window.
  398. WindowProcName : The backend process for the window.
  399. Position : 2-tuple (x,y) coordinates of the mouse click.
  400. MessageName : "mouse left|right|middle down",
  401. "mouse left|right|middle up".
  402. """
  403. def __init__(
  404. self, Window, WindowName, WindowProcName, Position, MessageName):
  405. self.Window = Window
  406. self.WindowName = WindowName
  407. self.WindowProcName = WindowProcName
  408. self.Position = Position
  409. self.MessageName = MessageName
  410. def __str__(self):
  411. return '\n'.join((
  412. 'Window Handle: {s.Window}',
  413. 'Window\'s Process Name: {s.WindowProcName}',
  414. 'Position: {s.Position}',
  415. 'MessageName: {s.MessageName}',
  416. )).format(s=self)
  417. #######################################################################
  418. # ########################END CLASS DEF################################
  419. #######################################################################
  420. if __name__ == '__main__':
  421. hm = HookManager()
  422. hm.HookKeyboard()
  423. hm.HookMouse()
  424. hm.KeyDown = hm.printevent
  425. hm.KeyUp = hm.printevent
  426. hm.MouseAllButtonsDown = hm.printevent
  427. hm.MouseAllButtonsUp = hm.printevent
  428. hm.MouseMovement = hm.printevent
  429. hm.start()
  430. time.sleep(10)
  431. hm.cancel()