├── .gitignore ├── README.md ├── example.py ├── example_with_parameters.py ├── license.txt └── pyxhook.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pyxhook is an implementation of pyhook (http://sourceforge.net/projects/pyhook/) that works on Linux 2 | 3 | It is 100% desktop and GUI toolkit independent. It is taken from: http://sourceforge.net/projects/pykeylogger/ 4 | 5 | The purpose of this repository is to provide a central location for obtaining just pyxhook.py library and provide a clear example of its usage 6 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple example of hooking the keyboard on Linux using pyxhook 3 | 4 | Any key pressed prints out the keys values, program terminates when spacebar 5 | is pressed. 6 | """ 7 | from __future__ import print_function 8 | 9 | # Libraries we need 10 | import pyxhook 11 | import time 12 | 13 | 14 | # This function is called every time a key is presssed 15 | def kbevent(event): 16 | global running 17 | # print key info 18 | print(event) 19 | 20 | # If the ascii value matches spacebar, terminate the while loop 21 | if event.Ascii == 32: 22 | running = False 23 | 24 | 25 | # Create hookmanager 26 | hookman = pyxhook.HookManager() 27 | # Define our callback to fire when a key is pressed down 28 | hookman.KeyDown = kbevent 29 | # Hook the keyboard 30 | hookman.HookKeyboard() 31 | # Start our listener 32 | hookman.start() 33 | 34 | # Create a loop to keep the application running 35 | running = True 36 | while running: 37 | time.sleep(0.1) 38 | 39 | # Close the listener when we are done 40 | hookman.cancel() -------------------------------------------------------------------------------- /example_with_parameters.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple example of hooking the keyboard on Linux using pyxhook 3 | 4 | Any key pressed prints out the keys values, program terminates when spacebar 5 | is pressed. 6 | """ 7 | from __future__ import print_function 8 | 9 | # Libraries we need 10 | import pyxhook 11 | import time 12 | 13 | 14 | # This function is called every time a key is presssed 15 | def kbevent(event, params): 16 | # print key info 17 | print(event) 18 | # If the ascii value matches spacebar, terminate the while loop 19 | if event.Ascii == 32: 20 | params['running'] = False 21 | 22 | parameters={'running':True} 23 | # Create hookmanager 24 | hookman = pyxhook.HookManager(parameters=True) 25 | # Define our callback to fire when a key is pressed down 26 | hookman.KeyDown = kbevent 27 | # Define our parameters for callback function 28 | hookman.KeyDownParameters = parameters 29 | # Hook the keyboard 30 | hookman.HookKeyboard() 31 | # Start our listener 32 | hookman.start() 33 | 34 | # Create a loop to keep the application running 35 | while parameters['running']: 36 | time.sleep(0.1) 37 | # Close the listener when we are done 38 | hookman.cancel() 39 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | This license applies to all files in this repository that do not have 2 | another license otherwise indicated. 3 | 4 | Copyright (c) 2014, Jeff Hoogland 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | * Neither the name of the nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /pyxhook.py: -------------------------------------------------------------------------------- 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 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 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 34 | # So if there are any bugs, they are probably my fault. :) 35 | from __future__ import print_function 36 | 37 | import sys 38 | import re 39 | import time 40 | import threading 41 | 42 | from Xlib import X, XK, display 43 | from Xlib.ext import record 44 | from Xlib.protocol import rq 45 | 46 | 47 | ####################################################################### 48 | # #######################START CLASS DEF############################### 49 | ####################################################################### 50 | 51 | class HookManager(threading.Thread): 52 | """ This is the main class. Instantiate it, and you can hand it KeyDown 53 | and KeyUp (functions in your own code) which execute to parse the 54 | pyxhookkeyevent class that is returned. 55 | 56 | This simply takes these two values for now: 57 | KeyDown : The function to execute when a key is pressed, if it 58 | returns anything. It hands the function an argument that 59 | is the pyxhookkeyevent class. 60 | KeyUp : The function to execute when a key is released, if it 61 | returns anything. It hands the function an argument that is 62 | the pyxhookkeyevent class. 63 | """ 64 | 65 | def __init__(self,parameters=False): 66 | threading.Thread.__init__(self) 67 | self.finished = threading.Event() 68 | 69 | # Give these some initial values 70 | self.mouse_position_x = 0 71 | self.mouse_position_y = 0 72 | self.ison = {"shift": False, "caps": False} 73 | 74 | # Compile our regex statements. 75 | self.isshift = re.compile('^Shift') 76 | self.iscaps = re.compile('^Caps_Lock') 77 | self.shiftablechar = re.compile('|'.join(( 78 | '^[a-z0-9]$', 79 | '^minus$', 80 | '^equal$', 81 | '^bracketleft$', 82 | '^bracketright$', 83 | '^semicolon$', 84 | '^backslash$', 85 | '^apostrophe$', 86 | '^comma$', 87 | '^period$', 88 | '^slash$', 89 | '^grave$' 90 | ))) 91 | self.logrelease = re.compile('.*') 92 | self.isspace = re.compile('^space$') 93 | # Choose which type of function use 94 | self.parameters=parameters 95 | if parameters: 96 | self.lambda_function=lambda x,y: True 97 | else: 98 | self.lambda_function=lambda x: True 99 | # Assign default function actions (do nothing). 100 | self.KeyDown = self.lambda_function 101 | self.KeyUp = self.lambda_function 102 | self.MouseAllButtonsDown = self.lambda_function 103 | self.MouseAllButtonsUp = self.lambda_function 104 | self.MouseMovement = self.lambda_function 105 | 106 | self.KeyDownParameters = {} 107 | self.KeyUpParameters = {} 108 | self.MouseAllButtonsDownParameters = {} 109 | self.MouseAllButtonsUpParameters= {} 110 | self.MouseMovementParameters = {} 111 | 112 | self.contextEventMask = [X.KeyPress, X.MotionNotify] 113 | 114 | # Hook to our display. 115 | self.local_dpy = display.Display() 116 | self.record_dpy = display.Display() 117 | 118 | def run(self): 119 | # Check if the extension is present 120 | if not self.record_dpy.has_extension("RECORD"): 121 | print("RECORD extension not found", file=sys.stderr) 122 | sys.exit(1) 123 | # r = self.record_dpy.record_get_version(0, 0) 124 | # print("RECORD extension version {major}.{minor}".format( 125 | # major=r.major_version, 126 | # minor=r.minor_version 127 | # )) 128 | 129 | # Create a recording context; we only want key and mouse events 130 | self.ctx = self.record_dpy.record_create_context( 131 | 0, 132 | [record.AllClients], 133 | [{ 134 | 'core_requests': (0, 0), 135 | 'core_replies': (0, 0), 136 | 'ext_requests': (0, 0, 0, 0), 137 | 'ext_replies': (0, 0, 0, 0), 138 | 'delivered_events': (0, 0), 139 | # (X.KeyPress, X.ButtonPress), 140 | 'device_events': tuple(self.contextEventMask), 141 | 'errors': (0, 0), 142 | 'client_started': False, 143 | 'client_died': False, 144 | }]) 145 | 146 | # Enable the context; this only returns after a call to 147 | # record_disable_context, while calling the callback function in the 148 | # meantime 149 | self.record_dpy.record_enable_context(self.ctx, self.processevents) 150 | # Finally free the context 151 | self.record_dpy.record_free_context(self.ctx) 152 | 153 | def cancel(self): 154 | self.finished.set() 155 | self.local_dpy.record_disable_context(self.ctx) 156 | self.local_dpy.flush() 157 | 158 | def printevent(self, event): 159 | print(event) 160 | 161 | def HookKeyboard(self): 162 | # We don't need to do anything here anymore, since the default mask 163 | # is now set to contain X.KeyPress 164 | # self.contextEventMask[0] = X.KeyPress 165 | pass 166 | 167 | def HookMouse(self): 168 | # We don't need to do anything here anymore, since the default mask 169 | # is now set to contain X.MotionNotify 170 | 171 | # need mouse motion to track pointer position, since ButtonPress 172 | # events don't carry that info. 173 | # self.contextEventMask[1] = X.MotionNotify 174 | pass 175 | 176 | def processhookevents(self,action_type,action_parameters,events): 177 | # In order to avoid duplicate code, i wrote a function that takes the 178 | # input value of the action function and, depending on the initialization, 179 | # launches it or only with the event or passes the parameter 180 | if self.parameters: 181 | action_type(events,action_parameters) 182 | else: 183 | action_type(events) 184 | 185 | 186 | def processevents(self, reply): 187 | if reply.category != record.FromServer: 188 | return 189 | if reply.client_swapped: 190 | print("* received swapped protocol data, cowardly ignored") 191 | return 192 | try: 193 | # Get int value, python2. 194 | intval = ord(reply.data[0]) 195 | except TypeError: 196 | # Already bytes/ints, python3. 197 | intval = reply.data[0] 198 | if (not reply.data) or (intval < 2): 199 | # not an event 200 | return 201 | data = reply.data 202 | while len(data): 203 | event, data = rq.EventField(None).parse_binary_value( 204 | data, 205 | self.record_dpy.display, 206 | None, 207 | None 208 | ) 209 | if event.type == X.KeyPress: 210 | hookevent = self.keypressevent(event) 211 | self.processhookevents(self.KeyDown,self.KeyDownParameters,hookevent) 212 | elif event.type == X.KeyRelease: 213 | hookevent = self.keyreleaseevent(event) 214 | self.processhookevents(self.KeyUp,self.KeyUpParameters,hookevent) 215 | elif event.type == X.ButtonPress: 216 | hookevent = self.buttonpressevent(event) 217 | self.processhookevents(self.MouseAllButtonsDown,self.MouseAllButtonsDownParameters,hookevent) 218 | elif event.type == X.ButtonRelease: 219 | hookevent = self.buttonreleaseevent(event) 220 | self.processhookevents(self.MouseAllButtonsUp,self.MouseAllButtonsUpParameters,hookevent) 221 | elif event.type == X.MotionNotify: 222 | # use mouse moves to record mouse position, since press and 223 | # release events do not give mouse position info 224 | # (event.root_x and event.root_y have bogus info). 225 | hookevent = self.mousemoveevent(event) 226 | self.processhookevents(self.MouseMovement,self.MouseMovementParameters,hookevent) 227 | 228 | # print("processing events...", event.type) 229 | 230 | def keypressevent(self, event): 231 | matchto = self.lookup_keysym( 232 | self.local_dpy.keycode_to_keysym(event.detail, 0) 233 | ) 234 | if self.shiftablechar.match( 235 | self.lookup_keysym( 236 | self.local_dpy.keycode_to_keysym(event.detail, 0))): 237 | # This is a character that can be typed. 238 | if not self.ison["shift"]: 239 | keysym = self.local_dpy.keycode_to_keysym(event.detail, 0) 240 | return self.makekeyhookevent(keysym, event) 241 | else: 242 | keysym = self.local_dpy.keycode_to_keysym(event.detail, 1) 243 | return self.makekeyhookevent(keysym, event) 244 | else: 245 | # Not a typable character. 246 | keysym = self.local_dpy.keycode_to_keysym(event.detail, 0) 247 | if self.isshift.match(matchto): 248 | self.ison["shift"] = self.ison["shift"] + 1 249 | elif self.iscaps.match(matchto): 250 | if not self.ison["caps"]: 251 | self.ison["shift"] = self.ison["shift"] + 1 252 | self.ison["caps"] = True 253 | if self.ison["caps"]: 254 | self.ison["shift"] = self.ison["shift"] - 1 255 | self.ison["caps"] = False 256 | return self.makekeyhookevent(keysym, event) 257 | 258 | def keyreleaseevent(self, event): 259 | if self.shiftablechar.match( 260 | self.lookup_keysym( 261 | self.local_dpy.keycode_to_keysym(event.detail, 0))): 262 | if not self.ison["shift"]: 263 | keysym = self.local_dpy.keycode_to_keysym(event.detail, 0) 264 | else: 265 | keysym = self.local_dpy.keycode_to_keysym(event.detail, 1) 266 | else: 267 | keysym = self.local_dpy.keycode_to_keysym(event.detail, 0) 268 | matchto = self.lookup_keysym(keysym) 269 | if self.isshift.match(matchto): 270 | self.ison["shift"] = self.ison["shift"] - 1 271 | return self.makekeyhookevent(keysym, event) 272 | 273 | def buttonpressevent(self, event): 274 | # self.clickx = self.rootx 275 | # self.clicky = self.rooty 276 | return self.makemousehookevent(event) 277 | 278 | def buttonreleaseevent(self, event): 279 | # if (self.clickx == self.rootx) and (self.clicky == self.rooty): 280 | # # print("ButtonClock {detail} x={s.rootx y={s.rooty}}".format( 281 | # # detail=event.detail, 282 | # # s=self, 283 | # # )) 284 | # if event.detail in (1, 2, 3): 285 | # self.captureclick() 286 | # else: 287 | # pass 288 | # print("ButtonDown {detail} x={s.clickx} y={s.clicky}".format( 289 | # detail=event.detail, 290 | # s=self 291 | # )) 292 | # print("ButtonUp {detail} x={s.rootx} y={s.rooty}".format( 293 | # detail=event.detail, 294 | # s=self 295 | # )) 296 | return self.makemousehookevent(event) 297 | 298 | def mousemoveevent(self, event): 299 | self.mouse_position_x = event.root_x 300 | self.mouse_position_y = event.root_y 301 | return self.makemousehookevent(event) 302 | 303 | # need the following because XK.keysym_to_string() only does printable 304 | # chars rather than being the correct inverse of XK.string_to_keysym() 305 | def lookup_keysym(self, keysym): 306 | for name in dir(XK): 307 | if name.startswith("XK_") and getattr(XK, name) == keysym: 308 | return name.lstrip("XK_") 309 | return "[{}]".format(keysym) 310 | 311 | def asciivalue(self, keysym): 312 | asciinum = XK.string_to_keysym(self.lookup_keysym(keysym)) 313 | return asciinum % 256 314 | 315 | def makekeyhookevent(self, keysym, event): 316 | storewm = self.xwindowinfo() 317 | if event.type == X.KeyPress: 318 | MessageName = "key down" 319 | elif event.type == X.KeyRelease: 320 | MessageName = "key up" 321 | return pyxhookkeyevent( 322 | storewm["handle"], 323 | storewm["name"], 324 | storewm["class"], 325 | self.lookup_keysym(keysym), 326 | self.asciivalue(keysym), 327 | False, 328 | event.detail, 329 | MessageName 330 | ) 331 | 332 | def makemousehookevent(self, event): 333 | storewm = self.xwindowinfo() 334 | if event.detail == 1: 335 | MessageName = "mouse left " 336 | elif event.detail == 3: 337 | MessageName = "mouse right " 338 | elif event.detail == 2: 339 | MessageName = "mouse middle " 340 | elif event.detail == 5: 341 | MessageName = "mouse wheel down " 342 | elif event.detail == 4: 343 | MessageName = "mouse wheel up " 344 | else: 345 | MessageName = "mouse {} ".format(event.detail) 346 | 347 | if event.type == X.ButtonPress: 348 | MessageName = "{} down".format(MessageName) 349 | elif event.type == X.ButtonRelease: 350 | MessageName = "{} up".format(MessageName) 351 | else: 352 | MessageName = "mouse moved" 353 | return pyxhookmouseevent( 354 | storewm["handle"], 355 | storewm["name"], 356 | storewm["class"], 357 | (self.mouse_position_x, self.mouse_position_y), 358 | MessageName 359 | ) 360 | 361 | def xwindowinfo(self): 362 | try: 363 | windowvar = self.local_dpy.get_input_focus().focus 364 | wmname = windowvar.get_wm_name() 365 | wmclass = windowvar.get_wm_class() 366 | wmhandle = str(windowvar)[20:30] 367 | except: 368 | # This is to keep things running smoothly. 369 | # It almost never happens, but still... 370 | return {"name": None, "class": None, "handle": None} 371 | if (wmname is None) and (wmclass is None): 372 | try: 373 | windowvar = windowvar.query_tree().parent 374 | wmname = windowvar.get_wm_name() 375 | wmclass = windowvar.get_wm_class() 376 | wmhandle = str(windowvar)[20:30] 377 | except: 378 | # This is to keep things running smoothly. 379 | # It almost never happens, but still... 380 | return {"name": None, "class": None, "handle": None} 381 | if wmclass is None: 382 | return {"name": wmname, "class": wmclass, "handle": wmhandle} 383 | else: 384 | return {"name": wmname, "class": wmclass[0], "handle": wmhandle} 385 | 386 | 387 | class pyxhookkeyevent: 388 | """ This is the class that is returned with each key event.f 389 | It simply creates the variables below in the class. 390 | 391 | Window : The handle of the window. 392 | WindowName : The name of the window. 393 | WindowProcName : The backend process for the window. 394 | Key : The key pressed, shifted to the correct caps value. 395 | Ascii : An ascii representation of the key. It returns 0 if 396 | the ascii value is not between 31 and 256. 397 | KeyID : This is just False for now. Under windows, it is the 398 | Virtual Key Code, but that's a windows-only thing. 399 | ScanCode : Please don't use this. It differs for pretty much 400 | every type of keyboard. X11 abstracts this 401 | information anyway. 402 | MessageName : "key down", "key up". 403 | """ 404 | 405 | def __init__( 406 | self, Window, WindowName, WindowProcName, Key, Ascii, KeyID, 407 | ScanCode, MessageName): 408 | self.Window = Window 409 | self.WindowName = WindowName 410 | self.WindowProcName = WindowProcName 411 | self.Key = Key 412 | self.Ascii = Ascii 413 | self.KeyID = KeyID 414 | self.ScanCode = ScanCode 415 | self.MessageName = MessageName 416 | 417 | def __str__(self): 418 | return '\n'.join(( 419 | 'Window Handle: {s.Window}', 420 | 'Window Name: {s.WindowName}', 421 | 'Window\'s Process Name: {s.WindowProcName}', 422 | 'Key Pressed: {s.Key}', 423 | 'Ascii Value: {s.Ascii}', 424 | 'KeyID: {s.KeyID}', 425 | 'ScanCode: {s.ScanCode}', 426 | 'MessageName: {s.MessageName}', 427 | )).format(s=self) 428 | 429 | 430 | class pyxhookmouseevent: 431 | """This is the class that is returned with each key event.f 432 | It simply creates the variables below in the class. 433 | 434 | Window : The handle of the window. 435 | WindowName : The name of the window. 436 | WindowProcName : The backend process for the window. 437 | Position : 2-tuple (x,y) coordinates of the mouse click. 438 | MessageName : "mouse left|right|middle down", 439 | "mouse left|right|middle up". 440 | """ 441 | 442 | def __init__( 443 | self, Window, WindowName, WindowProcName, Position, MessageName): 444 | self.Window = Window 445 | self.WindowName = WindowName 446 | self.WindowProcName = WindowProcName 447 | self.Position = Position 448 | self.MessageName = MessageName 449 | 450 | def __str__(self): 451 | return '\n'.join(( 452 | 'Window Handle: {s.Window}', 453 | 'Window\'s Process Name: {s.WindowProcName}', 454 | 'Position: {s.Position}', 455 | 'MessageName: {s.MessageName}', 456 | )).format(s=self) 457 | 458 | 459 | ####################################################################### 460 | # ########################END CLASS DEF################################ 461 | ####################################################################### 462 | 463 | if __name__ == '__main__': 464 | hm = HookManager() 465 | hm.HookKeyboard() 466 | hm.HookMouse() 467 | hm.KeyDown = hm.printevent 468 | hm.KeyUp = hm.printevent 469 | hm.MouseAllButtonsDown = hm.printevent 470 | hm.MouseAllButtonsUp = hm.printevent 471 | hm.MouseMovement = hm.printevent 472 | hm.start() 473 | time.sleep(10) 474 | hm.cancel() 475 | --------------------------------------------------------------------------------