Browse Source

Add pyxhook with a modification

Teascade 8 months ago
parent
commit
0ad08ed77f
4 changed files with 512 additions and 4 deletions
  1. 27
    0
      PYXHOOK_LICENSE
  2. 6
    3
      README.md
  3. 1
    1
      input_manager.py
  4. 478
    0
      pyxhook.py

+ 27
- 0
PYXHOOK_LICENSE View File

@@ -0,0 +1,27 @@
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 <organization> 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 <COPYRIGHT HOLDER> 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.

+ 6
- 3
README.md View File

@@ -16,8 +16,7 @@ When you've made sure your `python` is version 2.7.x,
16 16
 
17 17
 **on Windows** you can run on
18 18
 `python -m pip install pyhook`  
19
-or **on Linux** run
20
-`python -m pip install pyxhook`
19
+**for Linux**, [pyxhook sources](pyxhook.py) are included
21 20
 
22 21
 After that install pygame with
23 22
 `python -m pip install pygame`
@@ -29,8 +28,12 @@ And then you should be able to open bongocat with
29 28
 
30 29
 Programming bongocat is licensed under the terms of the [MIT license][license]
31 30
 
31
+This repository also contains the source code of [pyxhook][pyxhook], which is licensed under a [BSD license][license], with some modifications to fix [issue 25][pyxhook-issue]
32
+
32 33
 
33 34
 [pyhook]: https://github.com/naihe2010/pyHook
34 35
 [pyxhook]: https://github.com/JeffHoogland/pyxhook
36
+[pyxhook-issue]: https://github.com/JeffHoogland/pyxhook/issues/25
35 37
 [pygame]: https://www.pygame.org
36
-[license]: LICENSE
38
+[license]: LICENSE
39
+[pyxhook-license]: PYXHOOK_LICENSE

+ 1
- 1
input_manager.py View File

@@ -40,7 +40,7 @@ class InputManager:
40 40
         if shifted != None:
41 41
             shifted = shifted.lower()
42 42
         if shifted in self.currently_pressed_keys:
43
-            self.currently_pressed_keys.remove(key)
43
+            self.currently_pressed_keys.remove(shifted)
44 44
             self.on_update(False)
45 45
 
46 46
     def left_keys_pressed(self):

+ 478
- 0
pyxhook.py View File

@@ -0,0 +1,478 @@
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
+
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
+    def processevents(self, reply):
186
+        if reply.category != record.FromServer:
187
+            return
188
+        if reply.client_swapped:
189
+            print("* received swapped protocol data, cowardly ignored")
190
+            return
191
+        try:
192
+            # Get int value, python2.
193
+            intval = ord(reply.data[0])
194
+        except TypeError:
195
+            # Already bytes/ints, python3.
196
+            intval = reply.data[0]
197
+        if (not reply.data) or (intval < 2):
198
+            # not an event
199
+            return
200
+        data = reply.data
201
+        while len(data):
202
+            event, data = rq.EventField(None).parse_binary_value(
203
+                data,
204
+                self.record_dpy.display,
205
+                None,
206
+                None
207
+            )
208
+            if event.type == X.KeyPress:
209
+                hookevent = self.keypressevent(event)
210
+                self.processhookevents(
211
+                    self.KeyDown, self.KeyDownParameters, hookevent)
212
+            elif event.type == X.KeyRelease:
213
+                hookevent = self.keyreleaseevent(event)
214
+                self.processhookevents(
215
+                    self.KeyUp, self.KeyUpParameters, hookevent)
216
+            elif event.type == X.ButtonPress:
217
+                hookevent = self.buttonpressevent(event)
218
+                self.processhookevents(
219
+                    self.MouseAllButtonsDown, self.MouseAllButtonsDownParameters, hookevent)
220
+            elif event.type == X.ButtonRelease:
221
+                hookevent = self.buttonreleaseevent(event)
222
+                self.processhookevents(
223
+                    self.MouseAllButtonsUp, self.MouseAllButtonsUpParameters, hookevent)
224
+            elif event.type == X.MotionNotify:
225
+                # use mouse moves to record mouse position, since press and
226
+                # release events do not give mouse position info
227
+                # (event.root_x and event.root_y have bogus info).
228
+                hookevent = self.mousemoveevent(event)
229
+                self.processhookevents(
230
+                    self.MouseMovement, self.MouseMovementParameters, hookevent)
231
+
232
+        # print("processing events...", event.type)
233
+
234
+    def keypressevent(self, event):
235
+        matchto = self.lookup_keysym(
236
+            self.local_dpy.keycode_to_keysym(event.detail, 0)
237
+        )
238
+        if self.shiftablechar.match(
239
+                self.lookup_keysym(
240
+                    self.local_dpy.keycode_to_keysym(event.detail, 0))):
241
+            # This is a character that can be typed.
242
+            if not self.ison["shift"]:
243
+                keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
244
+                return self.makekeyhookevent(keysym, event)
245
+            else:
246
+                keysym = self.local_dpy.keycode_to_keysym(event.detail, 1)
247
+                return self.makekeyhookevent(keysym, event)
248
+        else:
249
+            # Not a typable character.
250
+            keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
251
+            if self.isshift.match(matchto):
252
+                self.ison["shift"] = self.ison["shift"] + 1
253
+            elif self.iscaps.match(matchto):
254
+                if not self.ison["caps"]:
255
+                    self.ison["shift"] = self.ison["shift"] + 1
256
+                    self.ison["caps"] = True
257
+                if self.ison["caps"]:
258
+                    self.ison["shift"] = self.ison["shift"] - 1
259
+                    self.ison["caps"] = False
260
+            return self.makekeyhookevent(keysym, event)
261
+
262
+    def keyreleaseevent(self, event):
263
+        if self.shiftablechar.match(
264
+                self.lookup_keysym(
265
+                    self.local_dpy.keycode_to_keysym(event.detail, 0))):
266
+            if not self.ison["shift"]:
267
+                keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
268
+            else:
269
+                keysym = self.local_dpy.keycode_to_keysym(event.detail, 1)
270
+        else:
271
+            keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
272
+        matchto = self.lookup_keysym(keysym)
273
+        if self.isshift.match(matchto):
274
+            self.ison["shift"] = self.ison["shift"] - 1
275
+        return self.makekeyhookevent(keysym, event)
276
+
277
+    def buttonpressevent(self, event):
278
+        # self.clickx = self.rootx
279
+        # self.clicky = self.rooty
280
+        return self.makemousehookevent(event)
281
+
282
+    def buttonreleaseevent(self, event):
283
+        # if (self.clickx == self.rootx) and (self.clicky == self.rooty):
284
+        #     # print("ButtonClock {detail} x={s.rootx y={s.rooty}}".format(
285
+        #     #    detail=event.detail,
286
+        #     #    s=self,
287
+        #     # ))
288
+        #     if event.detail in (1, 2, 3):
289
+        #         self.captureclick()
290
+        # else:
291
+        #     pass
292
+        #     print("ButtonDown {detail} x={s.clickx} y={s.clicky}".format(
293
+        #         detail=event.detail,
294
+        #         s=self
295
+        #     ))
296
+        #     print("ButtonUp {detail} x={s.rootx} y={s.rooty}".format(
297
+        #         detail=event.detail,
298
+        #         s=self
299
+        #     ))
300
+        return self.makemousehookevent(event)
301
+
302
+    def mousemoveevent(self, event):
303
+        self.mouse_position_x = event.root_x
304
+        self.mouse_position_y = event.root_y
305
+        return self.makemousehookevent(event)
306
+
307
+    # need the following because XK.keysym_to_string() only does printable
308
+    # chars rather than being the correct inverse of XK.string_to_keysym()
309
+    def lookup_keysym(self, keysym):
310
+        for name in dir(XK):
311
+            if name.startswith("XK_") and getattr(XK, name) == keysym:
312
+                return name[:3]
313
+        return "[{}]".format(keysym)
314
+
315
+    def asciivalue(self, keysym):
316
+        asciinum = XK.string_to_keysym(self.lookup_keysym(keysym))
317
+        return asciinum % 256
318
+
319
+    def makekeyhookevent(self, keysym, event):
320
+        storewm = self.xwindowinfo()
321
+        if event.type == X.KeyPress:
322
+            MessageName = "key down"
323
+        elif event.type == X.KeyRelease:
324
+            MessageName = "key up"
325
+        return pyxhookkeyevent(
326
+            storewm["handle"],
327
+            storewm["name"],
328
+            storewm["class"],
329
+            self.lookup_keysym(keysym),
330
+            self.asciivalue(keysym),
331
+            False,
332
+            event.detail,
333
+            MessageName
334
+        )
335
+
336
+    def makemousehookevent(self, event):
337
+        storewm = self.xwindowinfo()
338
+        if event.detail == 1:
339
+            MessageName = "mouse left "
340
+        elif event.detail == 3:
341
+            MessageName = "mouse right "
342
+        elif event.detail == 2:
343
+            MessageName = "mouse middle "
344
+        elif event.detail == 5:
345
+            MessageName = "mouse wheel down "
346
+        elif event.detail == 4:
347
+            MessageName = "mouse wheel up "
348
+        else:
349
+            MessageName = "mouse {} ".format(event.detail)
350
+
351
+        if event.type == X.ButtonPress:
352
+            MessageName = "{} down".format(MessageName)
353
+        elif event.type == X.ButtonRelease:
354
+            MessageName = "{} up".format(MessageName)
355
+        else:
356
+            MessageName = "mouse moved"
357
+        return pyxhookmouseevent(
358
+            storewm["handle"],
359
+            storewm["name"],
360
+            storewm["class"],
361
+            (self.mouse_position_x, self.mouse_position_y),
362
+            MessageName
363
+        )
364
+
365
+    def xwindowinfo(self):
366
+        try:
367
+            windowvar = self.local_dpy.get_input_focus().focus
368
+            wmname = windowvar.get_wm_name()
369
+            wmclass = windowvar.get_wm_class()
370
+            wmhandle = str(windowvar)[20:30]
371
+        except:
372
+            # This is to keep things running smoothly.
373
+            # It almost never happens, but still...
374
+            return {"name": None, "class": None, "handle": None}
375
+        if (wmname is None) and (wmclass is None):
376
+            try:
377
+                windowvar = windowvar.query_tree().parent
378
+                wmname = windowvar.get_wm_name()
379
+                wmclass = windowvar.get_wm_class()
380
+                wmhandle = str(windowvar)[20:30]
381
+            except:
382
+                # This is to keep things running smoothly.
383
+                # It almost never happens, but still...
384
+                return {"name": None, "class": None, "handle": None}
385
+        if wmclass is None:
386
+            return {"name": wmname, "class": wmclass, "handle": wmhandle}
387
+        else:
388
+            return {"name": wmname, "class": wmclass[0], "handle": wmhandle}
389
+
390
+
391
+class pyxhookkeyevent:
392
+    """ This is the class that is returned with each key event.f
393
+        It simply creates the variables below in the class.
394
+
395
+        Window         : The handle of the window.
396
+        WindowName     : The name of the window.
397
+        WindowProcName : The backend process for the window.
398
+        Key            : The key pressed, shifted to the correct caps value.
399
+        Ascii          : An ascii representation of the key. It returns 0 if
400
+                         the ascii value is not between 31 and 256.
401
+        KeyID          : This is just False for now. Under windows, it is the
402
+                         Virtual Key Code, but that's a windows-only thing.
403
+        ScanCode       : Please don't use this. It differs for pretty much
404
+                         every type of keyboard. X11 abstracts this
405
+                         information anyway.
406
+        MessageName    : "key down", "key up".
407
+    """
408
+
409
+    def __init__(
410
+            self, Window, WindowName, WindowProcName, Key, Ascii, KeyID,
411
+            ScanCode, MessageName):
412
+        self.Window = Window
413
+        self.WindowName = WindowName
414
+        self.WindowProcName = WindowProcName
415
+        self.Key = Key
416
+        self.Ascii = Ascii
417
+        self.KeyID = KeyID
418
+        self.ScanCode = ScanCode
419
+        self.MessageName = MessageName
420
+
421
+    def __str__(self):
422
+        return '\n'.join((
423
+            'Window Handle: {s.Window}',
424
+            'Window Name: {s.WindowName}',
425
+            'Window\'s Process Name: {s.WindowProcName}',
426
+            'Key Pressed: {s.Key}',
427
+            'Ascii Value: {s.Ascii}',
428
+            'KeyID: {s.KeyID}',
429
+            'ScanCode: {s.ScanCode}',
430
+            'MessageName: {s.MessageName}',
431
+        )).format(s=self)
432
+
433
+
434
+class pyxhookmouseevent:
435
+    """This is the class that is returned with each key event.f
436
+    It simply creates the variables below in the class.
437
+
438
+        Window         : The handle of the window.
439
+        WindowName     : The name of the window.
440
+        WindowProcName : The backend process for the window.
441
+        Position       : 2-tuple (x,y) coordinates of the mouse click.
442
+        MessageName    : "mouse left|right|middle down",
443
+                         "mouse left|right|middle up".
444
+    """
445
+
446
+    def __init__(
447
+            self, Window, WindowName, WindowProcName, Position, MessageName):
448
+        self.Window = Window
449
+        self.WindowName = WindowName
450
+        self.WindowProcName = WindowProcName
451
+        self.Position = Position
452
+        self.MessageName = MessageName
453
+
454
+    def __str__(self):
455
+        return '\n'.join((
456
+            'Window Handle: {s.Window}',
457
+            'Window\'s Process Name: {s.WindowProcName}',
458
+            'Position: {s.Position}',
459
+            'MessageName: {s.MessageName}',
460
+        )).format(s=self)
461
+
462
+
463
+#######################################################################
464
+# ########################END CLASS DEF################################
465
+#######################################################################
466
+
467
+if __name__ == '__main__':
468
+    hm = HookManager()
469
+    hm.HookKeyboard()
470
+    hm.HookMouse()
471
+    hm.KeyDown = hm.printevent
472
+    hm.KeyUp = hm.printevent
473
+    hm.MouseAllButtonsDown = hm.printevent
474
+    hm.MouseAllButtonsUp = hm.printevent
475
+    hm.MouseMovement = hm.printevent
476
+    hm.start()
477
+    time.sleep(10)
478
+    hm.cancel()