Add pyxhook with a modification
This commit is contained in:
parent
49fa1ad589
commit
0ad08ed77f
27
PYXHOOK_LICENSE
Normal file
27
PYXHOOK_LICENSE
Normal file
@ -0,0 +1,27 @@
|
||||
This license applies to all files in this repository that do not have
|
||||
another license otherwise indicated.
|
||||
|
||||
Copyright (c) 2014, Jeff Hoogland
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the <organization> nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -16,8 +16,7 @@ When you've made sure your `python` is version 2.7.x,
|
||||
|
||||
**on Windows** you can run on
|
||||
`python -m pip install pyhook`
|
||||
or **on Linux** run
|
||||
`python -m pip install pyxhook`
|
||||
**for Linux**, [pyxhook sources](pyxhook.py) are included
|
||||
|
||||
After that install pygame with
|
||||
`python -m pip install pygame`
|
||||
@ -29,8 +28,12 @@ And then you should be able to open bongocat with
|
||||
|
||||
Programming bongocat is licensed under the terms of the [MIT license][license]
|
||||
|
||||
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]
|
||||
|
||||
|
||||
[pyhook]: https://github.com/naihe2010/pyHook
|
||||
[pyxhook]: https://github.com/JeffHoogland/pyxhook
|
||||
[pyxhook-issue]: https://github.com/JeffHoogland/pyxhook/issues/25
|
||||
[pygame]: https://www.pygame.org
|
||||
[license]: LICENSE
|
||||
[license]: LICENSE
|
||||
[pyxhook-license]: PYXHOOK_LICENSE
|
@ -40,7 +40,7 @@ class InputManager:
|
||||
if shifted != None:
|
||||
shifted = shifted.lower()
|
||||
if shifted in self.currently_pressed_keys:
|
||||
self.currently_pressed_keys.remove(key)
|
||||
self.currently_pressed_keys.remove(shifted)
|
||||
self.on_update(False)
|
||||
|
||||
def left_keys_pressed(self):
|
||||
|
478
pyxhook.py
Normal file
478
pyxhook.py
Normal file
@ -0,0 +1,478 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# pyxhook -- an extension to emulate some of the PyHook library on linux.
|
||||
#
|
||||
# Copyright (C) 2008 Tim Alexander <dragonfyre13@gmail.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Thanks to Alex Badea <vamposdecampos@gmail.com> for writing the Record
|
||||
# demo for the xlib libraries. It helped me immensely working with these
|
||||
# in this library.
|
||||
#
|
||||
# Thanks to the python-xlib team. This wouldn't have been possible without
|
||||
# your code.
|
||||
#
|
||||
# This requires:
|
||||
# at least python-xlib 1.4
|
||||
# xwindows must have the "record" extension present, and active.
|
||||
#
|
||||
# This file has now been somewhat extensively modified by
|
||||
# Daniel Folkinshteyn <nanotube@users.sf.net>
|
||||
# So if there are any bugs, they are probably my fault. :)
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import re
|
||||
import time
|
||||
import threading
|
||||
|
||||
from Xlib import X, XK, display
|
||||
from Xlib.ext import record
|
||||
from Xlib.protocol import rq
|
||||
|
||||
|
||||
#######################################################################
|
||||
# #######################START CLASS DEF###############################
|
||||
#######################################################################
|
||||
|
||||
class HookManager(threading.Thread):
|
||||
""" This is the main class. Instantiate it, and you can hand it KeyDown
|
||||
and KeyUp (functions in your own code) which execute to parse the
|
||||
pyxhookkeyevent class that is returned.
|
||||
|
||||
This simply takes these two values for now:
|
||||
KeyDown : The function to execute when a key is pressed, if it
|
||||
returns anything. It hands the function an argument that
|
||||
is the pyxhookkeyevent class.
|
||||
KeyUp : The function to execute when a key is released, if it
|
||||
returns anything. It hands the function an argument that is
|
||||
the pyxhookkeyevent class.
|
||||
"""
|
||||
|
||||
def __init__(self, parameters=False):
|
||||
threading.Thread.__init__(self)
|
||||
self.finished = threading.Event()
|
||||
|
||||
# Give these some initial values
|
||||
self.mouse_position_x = 0
|
||||
self.mouse_position_y = 0
|
||||
self.ison = {"shift": False, "caps": False}
|
||||
|
||||
# Compile our regex statements.
|
||||
self.isshift = re.compile('^Shift')
|
||||
self.iscaps = re.compile('^Caps_Lock')
|
||||
self.shiftablechar = re.compile('|'.join((
|
||||
'^[a-z0-9]$',
|
||||
'^minus$',
|
||||
'^equal$',
|
||||
'^bracketleft$',
|
||||
'^bracketright$',
|
||||
'^semicolon$',
|
||||
'^backslash$',
|
||||
'^apostrophe$',
|
||||
'^comma$',
|
||||
'^period$',
|
||||
'^slash$',
|
||||
'^grave$'
|
||||
)))
|
||||
self.logrelease = re.compile('.*')
|
||||
self.isspace = re.compile('^space$')
|
||||
# Choose which type of function use
|
||||
self.parameters = parameters
|
||||
if parameters:
|
||||
self.lambda_function = lambda x, y: True
|
||||
else:
|
||||
self.lambda_function = lambda x: True
|
||||
# Assign default function actions (do nothing).
|
||||
self.KeyDown = self.lambda_function
|
||||
self.KeyUp = self.lambda_function
|
||||
self.MouseAllButtonsDown = self.lambda_function
|
||||
self.MouseAllButtonsUp = self.lambda_function
|
||||
self.MouseMovement = self.lambda_function
|
||||
|
||||
self.KeyDownParameters = {}
|
||||
self.KeyUpParameters = {}
|
||||
self.MouseAllButtonsDownParameters = {}
|
||||
self.MouseAllButtonsUpParameters = {}
|
||||
self.MouseMovementParameters = {}
|
||||
|
||||
self.contextEventMask = [X.KeyPress, X.MotionNotify]
|
||||
|
||||
# Hook to our display.
|
||||
self.local_dpy = display.Display()
|
||||
self.record_dpy = display.Display()
|
||||
|
||||
def run(self):
|
||||
# Check if the extension is present
|
||||
if not self.record_dpy.has_extension("RECORD"):
|
||||
print("RECORD extension not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
# r = self.record_dpy.record_get_version(0, 0)
|
||||
# print("RECORD extension version {major}.{minor}".format(
|
||||
# major=r.major_version,
|
||||
# minor=r.minor_version
|
||||
# ))
|
||||
|
||||
# Create a recording context; we only want key and mouse events
|
||||
self.ctx = self.record_dpy.record_create_context(
|
||||
0,
|
||||
[record.AllClients],
|
||||
[{
|
||||
'core_requests': (0, 0),
|
||||
'core_replies': (0, 0),
|
||||
'ext_requests': (0, 0, 0, 0),
|
||||
'ext_replies': (0, 0, 0, 0),
|
||||
'delivered_events': (0, 0),
|
||||
# (X.KeyPress, X.ButtonPress),
|
||||
'device_events': tuple(self.contextEventMask),
|
||||
'errors': (0, 0),
|
||||
'client_started': False,
|
||||
'client_died': False,
|
||||
}])
|
||||
|
||||
# Enable the context; this only returns after a call to
|
||||
# record_disable_context, while calling the callback function in the
|
||||
# meantime
|
||||
self.record_dpy.record_enable_context(self.ctx, self.processevents)
|
||||
# Finally free the context
|
||||
self.record_dpy.record_free_context(self.ctx)
|
||||
|
||||
def cancel(self):
|
||||
self.finished.set()
|
||||
self.local_dpy.record_disable_context(self.ctx)
|
||||
self.local_dpy.flush()
|
||||
|
||||
def printevent(self, event):
|
||||
print(event)
|
||||
|
||||
def HookKeyboard(self):
|
||||
# We don't need to do anything here anymore, since the default mask
|
||||
# is now set to contain X.KeyPress
|
||||
# self.contextEventMask[0] = X.KeyPress
|
||||
pass
|
||||
|
||||
def HookMouse(self):
|
||||
# We don't need to do anything here anymore, since the default mask
|
||||
# is now set to contain X.MotionNotify
|
||||
|
||||
# need mouse motion to track pointer position, since ButtonPress
|
||||
# events don't carry that info.
|
||||
# self.contextEventMask[1] = X.MotionNotify
|
||||
pass
|
||||
|
||||
def processhookevents(self, action_type, action_parameters, events):
|
||||
# In order to avoid duplicate code, i wrote a function that takes the
|
||||
# input value of the action function and, depending on the initialization,
|
||||
# launches it or only with the event or passes the parameter
|
||||
if self.parameters:
|
||||
action_type(events, action_parameters)
|
||||
else:
|
||||
action_type(events)
|
||||
|
||||
def processevents(self, reply):
|
||||
if reply.category != record.FromServer:
|
||||
return
|
||||
if reply.client_swapped:
|
||||
print("* received swapped protocol data, cowardly ignored")
|
||||
return
|
||||
try:
|
||||
# Get int value, python2.
|
||||
intval = ord(reply.data[0])
|
||||
except TypeError:
|
||||
# Already bytes/ints, python3.
|
||||
intval = reply.data[0]
|
||||
if (not reply.data) or (intval < 2):
|
||||
# not an event
|
||||
return
|
||||
data = reply.data
|
||||
while len(data):
|
||||
event, data = rq.EventField(None).parse_binary_value(
|
||||
data,
|
||||
self.record_dpy.display,
|
||||
None,
|
||||
None
|
||||
)
|
||||
if event.type == X.KeyPress:
|
||||
hookevent = self.keypressevent(event)
|
||||
self.processhookevents(
|
||||
self.KeyDown, self.KeyDownParameters, hookevent)
|
||||
elif event.type == X.KeyRelease:
|
||||
hookevent = self.keyreleaseevent(event)
|
||||
self.processhookevents(
|
||||
self.KeyUp, self.KeyUpParameters, hookevent)
|
||||
elif event.type == X.ButtonPress:
|
||||
hookevent = self.buttonpressevent(event)
|
||||
self.processhookevents(
|
||||
self.MouseAllButtonsDown, self.MouseAllButtonsDownParameters, hookevent)
|
||||
elif event.type == X.ButtonRelease:
|
||||
hookevent = self.buttonreleaseevent(event)
|
||||
self.processhookevents(
|
||||
self.MouseAllButtonsUp, self.MouseAllButtonsUpParameters, hookevent)
|
||||
elif event.type == X.MotionNotify:
|
||||
# use mouse moves to record mouse position, since press and
|
||||
# release events do not give mouse position info
|
||||
# (event.root_x and event.root_y have bogus info).
|
||||
hookevent = self.mousemoveevent(event)
|
||||
self.processhookevents(
|
||||
self.MouseMovement, self.MouseMovementParameters, hookevent)
|
||||
|
||||
# print("processing events...", event.type)
|
||||
|
||||
def keypressevent(self, event):
|
||||
matchto = self.lookup_keysym(
|
||||
self.local_dpy.keycode_to_keysym(event.detail, 0)
|
||||
)
|
||||
if self.shiftablechar.match(
|
||||
self.lookup_keysym(
|
||||
self.local_dpy.keycode_to_keysym(event.detail, 0))):
|
||||
# This is a character that can be typed.
|
||||
if not self.ison["shift"]:
|
||||
keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
|
||||
return self.makekeyhookevent(keysym, event)
|
||||
else:
|
||||
keysym = self.local_dpy.keycode_to_keysym(event.detail, 1)
|
||||
return self.makekeyhookevent(keysym, event)
|
||||
else:
|
||||
# Not a typable character.
|
||||
keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
|
||||
if self.isshift.match(matchto):
|
||||
self.ison["shift"] = self.ison["shift"] + 1
|
||||
elif self.iscaps.match(matchto):
|
||||
if not self.ison["caps"]:
|
||||
self.ison["shift"] = self.ison["shift"] + 1
|
||||
self.ison["caps"] = True
|
||||
if self.ison["caps"]:
|
||||
self.ison["shift"] = self.ison["shift"] - 1
|
||||
self.ison["caps"] = False
|
||||
return self.makekeyhookevent(keysym, event)
|
||||
|
||||
def keyreleaseevent(self, event):
|
||||
if self.shiftablechar.match(
|
||||
self.lookup_keysym(
|
||||
self.local_dpy.keycode_to_keysym(event.detail, 0))):
|
||||
if not self.ison["shift"]:
|
||||
keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
|
||||
else:
|
||||
keysym = self.local_dpy.keycode_to_keysym(event.detail, 1)
|
||||
else:
|
||||
keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
|
||||
matchto = self.lookup_keysym(keysym)
|
||||
if self.isshift.match(matchto):
|
||||
self.ison["shift"] = self.ison["shift"] - 1
|
||||
return self.makekeyhookevent(keysym, event)
|
||||
|
||||
def buttonpressevent(self, event):
|
||||
# self.clickx = self.rootx
|
||||
# self.clicky = self.rooty
|
||||
return self.makemousehookevent(event)
|
||||
|
||||
def buttonreleaseevent(self, event):
|
||||
# if (self.clickx == self.rootx) and (self.clicky == self.rooty):
|
||||
# # print("ButtonClock {detail} x={s.rootx y={s.rooty}}".format(
|
||||
# # detail=event.detail,
|
||||
# # s=self,
|
||||
# # ))
|
||||
# if event.detail in (1, 2, 3):
|
||||
# self.captureclick()
|
||||
# else:
|
||||
# pass
|
||||
# print("ButtonDown {detail} x={s.clickx} y={s.clicky}".format(
|
||||
# detail=event.detail,
|
||||
# s=self
|
||||
# ))
|
||||
# print("ButtonUp {detail} x={s.rootx} y={s.rooty}".format(
|
||||
# detail=event.detail,
|
||||
# s=self
|
||||
# ))
|
||||
return self.makemousehookevent(event)
|
||||
|
||||
def mousemoveevent(self, event):
|
||||
self.mouse_position_x = event.root_x
|
||||
self.mouse_position_y = event.root_y
|
||||
return self.makemousehookevent(event)
|
||||
|
||||
# need the following because XK.keysym_to_string() only does printable
|
||||
# chars rather than being the correct inverse of XK.string_to_keysym()
|
||||
def lookup_keysym(self, keysym):
|
||||
for name in dir(XK):
|
||||
if name.startswith("XK_") and getattr(XK, name) == keysym:
|
||||
return name[:3]
|
||||
return "[{}]".format(keysym)
|
||||
|
||||
def asciivalue(self, keysym):
|
||||
asciinum = XK.string_to_keysym(self.lookup_keysym(keysym))
|
||||
return asciinum % 256
|
||||
|
||||
def makekeyhookevent(self, keysym, event):
|
||||
storewm = self.xwindowinfo()
|
||||
if event.type == X.KeyPress:
|
||||
MessageName = "key down"
|
||||
elif event.type == X.KeyRelease:
|
||||
MessageName = "key up"
|
||||
return pyxhookkeyevent(
|
||||
storewm["handle"],
|
||||
storewm["name"],
|
||||
storewm["class"],
|
||||
self.lookup_keysym(keysym),
|
||||
self.asciivalue(keysym),
|
||||
False,
|
||||
event.detail,
|
||||
MessageName
|
||||
)
|
||||
|
||||
def makemousehookevent(self, event):
|
||||
storewm = self.xwindowinfo()
|
||||
if event.detail == 1:
|
||||
MessageName = "mouse left "
|
||||
elif event.detail == 3:
|
||||
MessageName = "mouse right "
|
||||
elif event.detail == 2:
|
||||
MessageName = "mouse middle "
|
||||
elif event.detail == 5:
|
||||
MessageName = "mouse wheel down "
|
||||
elif event.detail == 4:
|
||||
MessageName = "mouse wheel up "
|
||||
else:
|
||||
MessageName = "mouse {} ".format(event.detail)
|
||||
|
||||
if event.type == X.ButtonPress:
|
||||
MessageName = "{} down".format(MessageName)
|
||||
elif event.type == X.ButtonRelease:
|
||||
MessageName = "{} up".format(MessageName)
|
||||
else:
|
||||
MessageName = "mouse moved"
|
||||
return pyxhookmouseevent(
|
||||
storewm["handle"],
|
||||
storewm["name"],
|
||||
storewm["class"],
|
||||
(self.mouse_position_x, self.mouse_position_y),
|
||||
MessageName
|
||||
)
|
||||
|
||||
def xwindowinfo(self):
|
||||
try:
|
||||
windowvar = self.local_dpy.get_input_focus().focus
|
||||
wmname = windowvar.get_wm_name()
|
||||
wmclass = windowvar.get_wm_class()
|
||||
wmhandle = str(windowvar)[20:30]
|
||||
except:
|
||||
# This is to keep things running smoothly.
|
||||
# It almost never happens, but still...
|
||||
return {"name": None, "class": None, "handle": None}
|
||||
if (wmname is None) and (wmclass is None):
|
||||
try:
|
||||
windowvar = windowvar.query_tree().parent
|
||||
wmname = windowvar.get_wm_name()
|
||||
wmclass = windowvar.get_wm_class()
|
||||
wmhandle = str(windowvar)[20:30]
|
||||
except:
|
||||
# This is to keep things running smoothly.
|
||||
# It almost never happens, but still...
|
||||
return {"name": None, "class": None, "handle": None}
|
||||
if wmclass is None:
|
||||
return {"name": wmname, "class": wmclass, "handle": wmhandle}
|
||||
else:
|
||||
return {"name": wmname, "class": wmclass[0], "handle": wmhandle}
|
||||
|
||||
|
||||
class pyxhookkeyevent:
|
||||
""" This is the class that is returned with each key event.f
|
||||
It simply creates the variables below in the class.
|
||||
|
||||
Window : The handle of the window.
|
||||
WindowName : The name of the window.
|
||||
WindowProcName : The backend process for the window.
|
||||
Key : The key pressed, shifted to the correct caps value.
|
||||
Ascii : An ascii representation of the key. It returns 0 if
|
||||
the ascii value is not between 31 and 256.
|
||||
KeyID : This is just False for now. Under windows, it is the
|
||||
Virtual Key Code, but that's a windows-only thing.
|
||||
ScanCode : Please don't use this. It differs for pretty much
|
||||
every type of keyboard. X11 abstracts this
|
||||
information anyway.
|
||||
MessageName : "key down", "key up".
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, Window, WindowName, WindowProcName, Key, Ascii, KeyID,
|
||||
ScanCode, MessageName):
|
||||
self.Window = Window
|
||||
self.WindowName = WindowName
|
||||
self.WindowProcName = WindowProcName
|
||||
self.Key = Key
|
||||
self.Ascii = Ascii
|
||||
self.KeyID = KeyID
|
||||
self.ScanCode = ScanCode
|
||||
self.MessageName = MessageName
|
||||
|
||||
def __str__(self):
|
||||
return '\n'.join((
|
||||
'Window Handle: {s.Window}',
|
||||
'Window Name: {s.WindowName}',
|
||||
'Window\'s Process Name: {s.WindowProcName}',
|
||||
'Key Pressed: {s.Key}',
|
||||
'Ascii Value: {s.Ascii}',
|
||||
'KeyID: {s.KeyID}',
|
||||
'ScanCode: {s.ScanCode}',
|
||||
'MessageName: {s.MessageName}',
|
||||
)).format(s=self)
|
||||
|
||||
|
||||
class pyxhookmouseevent:
|
||||
"""This is the class that is returned with each key event.f
|
||||
It simply creates the variables below in the class.
|
||||
|
||||
Window : The handle of the window.
|
||||
WindowName : The name of the window.
|
||||
WindowProcName : The backend process for the window.
|
||||
Position : 2-tuple (x,y) coordinates of the mouse click.
|
||||
MessageName : "mouse left|right|middle down",
|
||||
"mouse left|right|middle up".
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, Window, WindowName, WindowProcName, Position, MessageName):
|
||||
self.Window = Window
|
||||
self.WindowName = WindowName
|
||||
self.WindowProcName = WindowProcName
|
||||
self.Position = Position
|
||||
self.MessageName = MessageName
|
||||
|
||||
def __str__(self):
|
||||
return '\n'.join((
|
||||
'Window Handle: {s.Window}',
|
||||
'Window\'s Process Name: {s.WindowProcName}',
|
||||
'Position: {s.Position}',
|
||||
'MessageName: {s.MessageName}',
|
||||
)).format(s=self)
|
||||
|
||||
|
||||
#######################################################################
|
||||
# ########################END CLASS DEF################################
|
||||
#######################################################################
|
||||
|
||||
if __name__ == '__main__':
|
||||
hm = HookManager()
|
||||
hm.HookKeyboard()
|
||||
hm.HookMouse()
|
||||
hm.KeyDown = hm.printevent
|
||||
hm.KeyUp = hm.printevent
|
||||
hm.MouseAllButtonsDown = hm.printevent
|
||||
hm.MouseAllButtonsUp = hm.printevent
|
||||
hm.MouseMovement = hm.printevent
|
||||
hm.start()
|
||||
time.sleep(10)
|
||||
hm.cancel()
|
Loading…
Reference in New Issue
Block a user