Skip to content

Commit 1e355f5

Browse files
committed
v1.0: functionality ready.
1 parent b3ca516 commit 1e355f5

File tree

12 files changed

+154
-150
lines changed

12 files changed

+154
-150
lines changed

inputscope/conf.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
ServerIP = 0.0.0.0
1717
1818
save() retains only the DEFAULT section, and writes only values diverging from
19-
the declared ones in source code.
19+
the declared ones in source code. File is deleted if all values are at default.
2020
2121
@author Erki Suurjaak
2222
@created 26.03.2015
23-
@modified 14.04.2015
23+
@modified 29.04.2015
2424
------------------------------------------------------------------------------
2525
"""
2626
try: import ConfigParser as configparser # Py2
@@ -36,37 +36,34 @@
3636

3737
"""Program title, version number and version date."""
3838
Title = "InputScope"
39-
Version = "0.0.1"
40-
VersionDate = "14.04.2015"
39+
Version = "1.0"
40+
VersionDate = "29.04.2015"
4141

4242
"""TCP port of the web user interface."""
4343
WebHost = "localhost"
4444
WebPort = 8099
4545
WebUrl = "http://%s:%s" % (WebHost, WebPort)
4646

47-
HomepageUrl = "https://github.com/suurjaak/%3Cspan%20class="x x-first x-last">inputscope"
47+
HomepageUrl = "https://github.com/suurjaak/%3Cspan%20class="x x-first x-last">InputScope"
4848

4949
"""Size of the heatmaps, in pixels."""
5050
MouseHeatmapSize = (640, 360)
5151
KeyboardHeatmapSize = (680, 180)
5252

53-
"""Default desktop size used for scaling, if size not available from system."""
53+
"""Default desktop size for scaling, if not available from system, in pixels."""
5454
DefaultScreenSize = (1920, 1080)
5555

5656
"""Whether mouse or keyboard logging is enabled."""
5757
MouseEnabled = True
5858
KeyboardEnabled = True
5959

60-
"""Maximum interval between key presses to count as one typing session."""
60+
"""Maximum keypress interval to count as one typing session, in seconds."""
6161
KeyboardSessionMaxDelta = 3
6262

6363
"""Physical length of a pixel, in meters."""
6464
PixelLength = 0.00024825
6565

66-
67-
"""
68-
@todo siin üks variant
69-
"""
66+
"""Key positions in keyboard heatmap."""
7067
KeyPositions = {
7168
"Escape": (12, 12),
7269
"F1": (72, 12),
@@ -223,7 +220,6 @@
223220
"""Path for application icon file."""
224221
IconPath = os.path.join(StaticPath, "icon.ico")
225222

226-
227223
"""Statements to execute in database at startup, like CREATE TABLE."""
228224
DbStatements = (
229225
"CREATE TABLE IF NOT EXISTS moves (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP, x INTEGER, y INTEGER)",

inputscope/db.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
for i in range(5): db.insert("test", [("val", "venividivici")])
77
db.insert("test", val=None)
88
db.select("test", val=None, limit=[0, 3]).fetchone()
9-
db.update("test", values=[("val", "something")], val=None)
10-
db.update("test", values=[("val", "arrivederci")], where=[("val", ("IS NOT", None))])
9+
db.update("test", values=[("val", "arrivederci")], val=None)
10+
db.update("test", values=[("val", "ciao")], where=[("val", ("IS NOT", None))])
1111
db.select("test", order=["val", ("id", "DESC")], limit=[0, 4]).fetchall()
1212
db.delete("test", val="something")
1313
db.execute("DROP TABLE test")

inputscope/listener.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
55
@author Erki Suurjaak
66
@created 06.04.2015
7-
@modified 24.04.2015
7+
@modified 29.04.2015
88
"""
99
from __future__ import print_function
10-
import collections
1110
import datetime
1211
import multiprocessing
1312
import Queue
@@ -19,6 +18,8 @@
1918
import conf
2019
import db
2120

21+
DEBUG = False
22+
2223

2324
class Listener(object):
2425
"""Runs mouse and keyboard listeners, and handles incoming commands."""
@@ -63,7 +64,7 @@ class DataHandler(threading.Thread):
6364
"""Output thread, inserts events to database and to output function."""
6465
def __init__(self, output):
6566
threading.Thread.__init__(self)
66-
self.counts = collections.defaultdict(int) # {type: count}
67+
self.counts = {} # {type: count}
6768
self.output = output
6869
self.inqueue = Queue.Queue()
6970
self.lasts = {"moves": None}
@@ -84,6 +85,7 @@ def run(self):
8485
if self.lasts[event] == pos: continue # while self.running
8586
self.lasts[event] = pos
8687

88+
if event not in self.counts: self.counts[event] = 0
8789
self.counts[event] += 1
8890
dbqueue.append((event, data))
8991
try:
@@ -124,10 +126,10 @@ def scroll(self, x, y, wheel):
124126

125127
class KeyHandler(pykeyboard.PyKeyboardEvent):
126128
"""Listens to keyboard events and forwards to output."""
127-
CONTROLCODES = {"\x00": "Nul", "\x01": "Start-Of-Header", "\x02": "Start-Of-Text", "\x03": "Break", "\x04": "End-Of-Transmission", "\x05": "Enquiry", "\x06": "Ack", "\x07": "Bell", "\x08": "Backspace", "\x09": "Tab", "\x09": "Tab", "\x0a": "Linefeed", "\x0b": "Vertical-Tab", "\x0c": "Form-Fe", "\x0d": "Enter", "\x0e": "Shift-In", "\x0f": "Shift-Out", "\x10": "Data-Link-Escape", "\x11": "Devicecontrol1", "\x12": "Devicecontrol2", "\x13": "Devicecontrol3", "\x14": "Devicecontrol4", "\x15": "Nak", "\x16": "Syn", "\x17": "End-Of-Transmission-Block", "\x18": "Break", "\x19": "End-Of-Medium", "\x1a": "Substitute", "\x1b": "Escape", "\x1c": "File-Separator", "\x1d": "Group-Separator", "\x1e": "Record-Separator", "\x1f": "Unit-Separator", "\x20": "Space", "\x7f": "Del", "\xa0": "Non-Breaking Space"}
129+
CONTROLCODES = {"\x00": "Nul", "\x01": "Start-Of-Header", "\x02": "Start-Of-Text", "\x03": "Break", "\x04": "End-Of-Transmission", "\x05": "Enquiry", "\x06": "Ack", "\x07": "Bell", "\x08": "Backspace", "\x09": "Tab", "\x0a": "Linefeed", "\x0b": "Vertical-Tab", "\x0c": "Form-Fe", "\x0d": "Enter", "\x0e": "Shift-In", "\x0f": "Shift-Out", "\x10": "Data-Link-Escape", "\x11": "Devicecontrol1", "\x12": "Devicecontrol2", "\x13": "Devicecontrol3", "\x14": "Devicecontrol4", "\x15": "Nak", "\x16": "Syn", "\x17": "End-Of-Transmission-Block", "\x18": "Break", "\x19": "End-Of-Medium", "\x1a": "Substitute", "\x1b": "Escape", "\x1c": "File-Separator", "\x1d": "Group-Separator", "\x1e": "Record-Separator", "\x1f": "Unit-Separator", "\x20": "Space", "\x7f": "Del", "\xa0": "Non-Breaking Space"}
128130
NUMPAD_SPECIALS = [("Insert", False), ("Delete", False), ("Home", False), ("End", False), ("PageUp", False), ("PageDown", False), ("Up", False), ("Down", False), ("Left", False), ("Right", False), ("Clear", False), ("Enter", True)]
129131
MODIFIERNAMES = {"Lcontrol": "Ctrl", "Rcontrol": "Ctrl", "Lshift": "Shift", "Rshift": "Shift", "Alt": "Alt", "AltGr": "Alt", "Lwin": "Win", "Rwin": "Win"}
130-
RENAMES = {"Prior": "PageUp", "Next": "PageDown", "Lmenu": "Alt", "Rmenu": "AltGr", "Apps": "Menu", "Return": "Enter", "Back": "Backspace", "Capital": "CapsLock", "Numlock": "NumLock", "Snapshot": "PrintScreen", "Scroll": "ScrollLock", "Decimal": "Numpad-Decimal", "Divide": "Numpad-Divide", "Subtract": "Numpad-Subtract", "Multiply": "Numpad-Multiply", "Add": "Numpad-Add"}
132+
RENAMES = {"Prior": "PageUp", "Next": "PageDown", "Lmenu": "Alt", "Rmenu": "AltGr", "Apps": "Menu", "Return": "Enter", "Back": "Backspace", "Capital": "CapsLock", "Numlock": "NumLock", "Snapshot": "PrintScreen", "Scroll": "ScrollLock", "Decimal": "Numpad-Decimal", "Divide": "Numpad-Divide", "Subtract": "Numpad-Subtract", "Multiply": "Numpad-Multiply", "Add": "Numpad-Add", "Cancel": "Break"}
131133
KEYS_DOWN = (0x0100, 0x0104) # [WM_KEYDOWN, WM_SYSKEYDOWN]
132134
KEYS_UP = (0x0101, 0x0105) # [WM_KEYUP, WM_SYSKEYUP]
133135
ALT_GRS = (36, 64, 91, 92, 93, 123, 124, 125, 128, 163, 208, 222, 240, 254) # $@[\]{|}€£ŠŽšž
@@ -137,9 +139,11 @@ class KeyHandler(pykeyboard.PyKeyboardEvent):
137139
def __init__(self, output):
138140
pykeyboard.PyKeyboardEvent.__init__(self)
139141
self.output = output
140-
HANDLERS = {"win32": self.handle_windows, "darwin": self.handle_mac}
141-
self.handler = HANDLERS.get(sys.platform, self.handle_linux) # Override
142-
self.modifiers = {"Ctrl": False, "Alt": False, "Shift": False, "Win": False}
142+
NAMES = {"win32": "handler", "linux2": "tap", "darwin": "keypress"}
143+
HANDLERS = {"win32": self.handle_windows, "linux2": self.handle_linux,
144+
"darwin": self.handle_mac}
145+
setattr(self, NAMES[sys.platform], HANDLERS[sys.platform])
146+
self.modifiers = dict((x, False) for x in self.MODIFIERNAMES.values())
143147
self.realmodifiers = dict((x, False) for x in self.MODIFIERNAMES)
144148
self.start()
145149

@@ -152,13 +156,11 @@ def keyname(self, key):
152156

153157
def handle_windows(self, event):
154158
"""Windows key event handler."""
155-
if event.IsInjected(): return True # Programmatically generated event
156-
157159
vkey = self.keyname(event.GetKey())
158160
if event.Message in self.KEYS_UP + self.KEYS_DOWN:
159161
if vkey in self.MODIFIERNAMES:
160-
self.modifiers[self.MODIFIERNAMES[vkey]] = event.Message in self.KEYS_DOWN
161162
self.realmodifiers[vkey] = event.Message in self.KEYS_DOWN
163+
self.modifiers[self.MODIFIERNAMES[vkey]] = self.realmodifiers[vkey]
162164
if event.Message not in self.KEYS_DOWN:
163165
return True
164166

@@ -171,18 +173,33 @@ def handle_windows(self, event):
171173
is_altgr = event.Ascii in self.ALT_GRS
172174
key = self.keyname(unichr(event.Ascii))
173175

176+
if DEBUG: print("Adding key %s (real %s)" % (key.encode("utf-8"), vkey.encode("utf-8")))
174177
self.output(type="keys", key=key, realkey=vkey)
175178

176179
if vkey not in self.MODIFIERNAMES and not is_altgr:
177-
modifier = "-".join(k for k, v in self.modifiers.items() if v)
180+
modifier = "-".join(k for k in ["Ctrl", "Alt", "Shift", "Win"]
181+
if self.modifiers[k])
178182
if modifier and modifier != "Shift": # Shift-X is not a combo
179183
if self.modifiers["Ctrl"] and event.Ascii:
180184
key = self.keyname(unichr(event.KeyID))
181185
realmodifier = "-".join(k for k, v in self.realmodifiers.items() if v)
182186
realkey = "%s-%s" % (realmodifier, key)
183187
key = "%s-%s" % (modifier, key)
188+
if DEBUG: print("Adding combo %s (real %s)" % (key.encode("utf-8"), realkey.encode("utf-8")))
184189
self.output(type="combos", key=key, realkey=realkey)
185190

191+
if DEBUG:
192+
print("CHARACTER: %r" % key)
193+
print('GetKey: {0}'.format(event.GetKey())) # Name of the virtual keycode, str
194+
print('IsAlt: {0}'.format(event.IsAlt())) # Was the alt key depressed?, bool
195+
print('IsExtended: {0}'.format(event.IsExtended())) # Is this an extended key?, bool
196+
print('IsInjected: {0}'.format(event.IsInjected())) # Was this event generated programmatically?, bool
197+
print('IsTransition: {0}'.format(event.IsTransition())) #Is this a transition from up to down or vice versa?, bool
198+
print('ASCII: {0}'.format(event.Ascii)) # ASCII value, if one exists, str
199+
print('KeyID: {0}'.format(event.KeyID)) # Virtual key code, int
200+
print('ScanCode: {0}'.format(event.ScanCode)) # Scan code, int
201+
print('Message: {0}'.format(event.Message)) # Name of the virtual keycode, str
202+
print()
186203
return True
187204

188205

@@ -201,11 +218,15 @@ def escape(self, event):
201218
return False
202219

203220

204-
205-
if "__main__" == __name__:
221+
def main():
222+
"""Entry point for stand-alone execution."""
206223
conf.init(), db.init(conf.DbPath)
207224
inqueue = multiprocessing.Queue()
208225
outqueue = type("PrintQueue", (), {"put": lambda self, x: print(x)})()
209226
if conf.MouseEnabled: inqueue.put("mouse_start")
210227
if conf.KeyboardEnabled: inqueue.put("keyboard_start")
211228
Listener(inqueue, outqueue).run()
229+
230+
231+
if "__main__" == __name__:
232+
main()

inputscope/main.py

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
@author Erki Suurjaak
77
@created 05.05.2015
8-
@modified 24.04.2015
8+
@modified 29.04.2015
99
"""
1010
import multiprocessing
1111
import multiprocessing.forking
@@ -14,12 +14,14 @@
1414
import sys
1515
import threading
1616
import webbrowser
17-
try: import Tkinter as tk # For getting screen size if wx unavailable
18-
except ImportError: tk = None
1917
try: import win32com.client # For creating startup shortcut
2018
except ImportError: pass
19+
tk = None
2120
try: import wx, wx.lib.sized_controls, wx.py.shell
22-
except ImportError: wx = None
21+
except ImportError:
22+
wx = None
23+
try: import Tkinter as tk # For getting screen size if wx unavailable
24+
except ImportError: pass
2325

2426
import conf
2527
import db
@@ -50,7 +52,6 @@ def __init__(self, messagehandler=None):
5052
self.webqueue = multiprocessing.Queue() # Out-queue to webui
5153
self.inqueue = multiprocessing.Queue() # In-queue from listener
5254

53-
5455
def toggle(self, input):
5556
if "mouse" == input:
5657
enabled = conf.MouseEnabled = not conf.MouseEnabled
@@ -59,6 +60,11 @@ def toggle(self, input):
5960
self.listenerqueue.put("%s_%s" % (input, "start" if enabled else "stop"))
6061
conf.save()
6162

63+
def stop(self):
64+
self.running = False
65+
self.listenerqueue.put("exit"), self.webqueue.put("exit")
66+
self.inqueue.put(None) # Wake up thread waiting on queue
67+
6268
def log_resolution(self, size):
6369
if size: db.insert("screen_sizes", x=size[0], y=size[1])
6470

@@ -80,57 +86,40 @@ def run(self):
8086
if not data: continue
8187
self.messagehandler and self.messagehandler(data)
8288

83-
def stop(self):
84-
self.running = False
85-
self.listenerqueue.put("exit"), self.webqueue.put("exit")
86-
self.inqueue.put(None) # Wake up thread waiting on queue
87-
8889

89-
90-
class MainWindow(getattr(wx, "Frame", object)):
91-
def __init__(self):
92-
wx.Frame.__init__(self, parent=None,
93-
title="%s %s" % (conf.Title, conf.Version))
94-
95-
handler = lambda x: wx.CallAfter(lambda: log and log.SetValue(str(x)))
96-
self.model = Model(handler)
90+
class MainApp(getattr(wx, "App", object)):
91+
def OnInit(self):
92+
self.model = Model()
9793
self.startupservice = StartupService()
9894

99-
self.frame_console = wx.py.shell.ShellFrame(self)
95+
self.frame_console = wx.py.shell.ShellFrame(None)
10096
self.trayicon = wx.TaskBarIcon()
10197

10298
if os.path.exists(conf.IconPath):
10399
icons = wx.IconBundle()
104100
icons.AddIconFromFile(conf.IconPath, wx.BITMAP_TYPE_ICO)
105-
self.SetIcons(icons)
106101
self.frame_console.SetIcons(icons)
107-
self.trayicon.SetIcon(icons.GetIconOfExactSize((16, 16)), conf.Title)
108-
109-
panel = wx.lib.sized_controls.SizedPanel(self)
110-
log = self.log = wx.TextCtrl(panel, style=wx.TE_MULTILINE)
102+
icon = (icons.GetIconOfExactSize((16, 16))
103+
if "win32" == sys.platform else icons.GetIcon((24, 24)))
104+
self.trayicon.SetIcon(icon, conf.Title)
111105

112-
log.SetEditable(False)
113-
log.BackgroundColour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
114-
log.ForegroundColour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT)
115106
self.frame_console.Title = "%s Console" % conf.Title
116107

117-
panel.SetSizerType("vertical")
118-
log.SetSizerProps(expand=True, proportion=1)
119-
120108
self.Bind(wx.EVT_CLOSE, self.OnClose)
121109
self.Bind(wx.EVT_DISPLAY_CHANGED, self.OnDisplayChanged)
122110
self.trayicon.Bind(wx.EVT_TASKBAR_LEFT_DCLICK, self.OnOpenUI)
123111
self.trayicon.Bind(wx.EVT_TASKBAR_RIGHT_DOWN, self.OnOpenMenu)
124112
self.frame_console.Bind(wx.EVT_CLOSE, self.OnToggleConsole)
125113

126-
self.Show()
114+
self.model.log_resolution(wx.GetDisplaySize())
127115
self.model.start()
116+
return True
128117

129118

130119
def OnOpenMenu(self, event):
131120
"""Creates and opens a popup menu for the tray icon."""
132121
menu = wx.Menu()
133-
item_ui = wx.MenuItem(menu, -1, "&Open user interface")
122+
item_ui = wx.MenuItem(menu, -1, "&Open statistics")
134123
item_startup = wx.MenuItem(menu, -1, "&Start with Windows",
135124
kind=wx.ITEM_CHECK) if self.startupservice.can_start() else None
136125
item_mouse = wx.MenuItem(menu, -1, "Stop &mouse logging",
@@ -143,8 +132,6 @@ def OnOpenMenu(self, event):
143132

144133
font = item_ui.Font
145134
font.SetWeight(wx.FONTWEIGHT_BOLD)
146-
font.SetFaceName(self.Font.FaceName)
147-
font.SetPointSize(self.Font.PointSize)
148135
item_ui.Font = font
149136

150137
menu.AppendItem(item_ui)
@@ -261,15 +248,14 @@ def start_listener(inqueue, outqueue):
261248
runner.run()
262249

263250

264-
265-
if "__main__" == __name__:
251+
def main():
252+
"""Entry point for stand-alone execution."""
266253
multiprocessing.freeze_support()
267254
conf.init()
268255
db.init(conf.DbPath, conf.DbStatements)
269256

270257
if wx:
271-
app = wx.App(redirect=True) # stdout and stderr redirected to wx popup
272-
app.SetTopWindow(MainWindow()) # stdout/stderr popup closes with window
258+
app = MainApp(redirect=True) # stdout and stderr redirected to wx popup
273259
app.MainLoop()
274260
else:
275261
model = Model(lambda x: sys.stderr.write("\r%s" % x))
@@ -283,3 +269,7 @@ def start_listener(inqueue, outqueue):
283269
model.run()
284270
except KeyboardInterrupt:
285271
model.stop()
272+
273+
274+
if "__main__" == __name__:
275+
main()

inputscope/static/icon.ico

-2.88 KB
Binary file not shown.

0 commit comments

Comments
 (0)