Skip to content

Commit b3ca516

Browse files
committed
First commit with basic functionality:
- logs mouse and keyboard activity - runs a web server for seeing statistics - opens a tray icon and a small overview window
0 parents  commit b3ca516

File tree

17 files changed

+3603
-0
lines changed

17 files changed

+3603
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.pyc

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
InputOut
2+
========
3+
4+
Mouse and keyboard input visualizer.
5+
6+
Three components:
7+
* main - wxPython desktop tray program, runs listener and webui
8+
* listener - listens and logs mouse and keyboard input
9+
* webui - web frontend for statistics and heatmaps
10+
11+
Listener and web-UI components can be run separately, or launched from main.
12+
13+
[screenshot @todo mouse]
14+
[screenshot @todo keyboard]
15+
16+
17+
Dependencies
18+
------------
19+
20+
* Python 2.7
21+
* bottle
22+
* PyUserInput
23+
* wxPython (optional)
24+
25+
26+
Attribution
27+
-----------
28+
29+
Heatmaps are drawn with heatmap.js,
30+
(c) 2014 Patrick Wied, http://www.patrick-wied.at/static/heatmapjs/.
31+
32+
Icon from Paomedia small-n-flat iconset,
33+
released under Creative Commons (Attribution 3.0 Unported),
34+
https://www.iconfinder.com/icons/285642/monitor_icon.

inputscope/conf.py

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Configuration settings. Can read additional/overridden options from INI file,
4+
supporting any JSON-serializable datatype.
5+
6+
INI file can contain a [DEFAULT] section for default settings, and additional
7+
sections overriding the default for different environments. Example:
8+
-----------------
9+
[DEFAULT]
10+
# single-line comments can start with # or ;
11+
ServerIP = my.server.domain
12+
ServerPort = 80
13+
SampleJSON = {"a": false, "b": [0.1, 0.2]}
14+
15+
[DEV]
16+
ServerIP = 0.0.0.0
17+
18+
save() retains only the DEFAULT section, and writes only values diverging from
19+
the declared ones in source code.
20+
21+
@author Erki Suurjaak
22+
@created 26.03.2015
23+
@modified 14.04.2015
24+
------------------------------------------------------------------------------
25+
"""
26+
try: import ConfigParser as configparser # Py2
27+
except ImportError: import configparser # Py3
28+
try: import cStringIO as StringIO # Py2
29+
except ImportError: import io as StringIO # Py3
30+
import datetime
31+
import json
32+
import logging
33+
import os
34+
import re
35+
import sys
36+
37+
"""Program title, version number and version date."""
38+
Title = "InputScope"
39+
Version = "0.0.1"
40+
VersionDate = "14.04.2015"
41+
42+
"""TCP port of the web user interface."""
43+
WebHost = "localhost"
44+
WebPort = 8099
45+
WebUrl = "http://%s:%s" % (WebHost, WebPort)
46+
47+
HomepageUrl = "https://github.com/suurjaak/inputscope"
48+
49+
"""Size of the heatmaps, in pixels."""
50+
MouseHeatmapSize = (640, 360)
51+
KeyboardHeatmapSize = (680, 180)
52+
53+
"""Default desktop size used for scaling, if size not available from system."""
54+
DefaultScreenSize = (1920, 1080)
55+
56+
"""Whether mouse or keyboard logging is enabled."""
57+
MouseEnabled = True
58+
KeyboardEnabled = True
59+
60+
"""Maximum interval between key presses to count as one typing session."""
61+
KeyboardSessionMaxDelta = 3
62+
63+
"""Physical length of a pixel, in meters."""
64+
PixelLength = 0.00024825
65+
66+
67+
"""
68+
@todo siin üks variant
69+
"""
70+
KeyPositions = {
71+
"Escape": (12, 12),
72+
"F1": (72, 12),
73+
"F2": (102, 12),
74+
"F3": (132, 12),
75+
"F4": (162, 12),
76+
"F5": (206, 12),
77+
"F6": (236, 12),
78+
"F7": (266, 12),
79+
"F8": (296, 12),
80+
"F9": (338, 12),
81+
"F10": (368, 12),
82+
"F11": (398, 12),
83+
"F12": (428, 12),
84+
"PrintScreen": (472, 12),
85+
"ScrollLock": (502, 12),
86+
"Pause": (532, 12),
87+
"Break": (532, 12),
88+
89+
"Oem_7": (12, 56),
90+
"1": (44, 56),
91+
"2": (74, 56),
92+
"3": (104, 56),
93+
"4": (134, 56),
94+
"5": (164, 56),
95+
"6": (192, 56),
96+
"7": (222, 56),
97+
"8": (252, 56),
98+
"9": (281, 56),
99+
"0": (311, 56),
100+
"Oem_Minus": (340, 56),
101+
"Oem_Plus": (371, 56),
102+
"Backspace": (414, 56),
103+
104+
"Tab": (24, 84),
105+
"Q": (60, 84),
106+
"W": (90, 84),
107+
"E": (120, 84),
108+
"R": (150, 84),
109+
"T": (180, 84),
110+
"Y": (210, 84),
111+
"U": (240, 84),
112+
"I": (270, 84),
113+
"O": (300, 84),
114+
"P": (330, 84),
115+
"Oem_3": (370, 84),
116+
"Oem_4": (400, 84),
117+
"Enter": (426, 96),
118+
119+
"CapsLock": (25, 111),
120+
"A": (68, 111),
121+
"S": (98, 111),
122+
"D": (128, 111),
123+
"F": (158, 111),
124+
"G": (188, 111),
125+
"H": (218, 111),
126+
"J": (248, 111),
127+
"K": (278, 111),
128+
"L": (308, 111),
129+
"Oem_1": (338, 111),
130+
"Oem_2": (368, 111),
131+
"Oem_5": (394, 111),
132+
133+
"Lshift": (19, 138),
134+
"Oem_102": (50, 138),
135+
"Z": (80, 138),
136+
"X": (110, 138),
137+
"C": (140, 138),
138+
"V": (170, 138),
139+
"B": (200, 138),
140+
"N": (230, 138),
141+
"M": (260, 138),
142+
"Oem_Comma": (290, 138),
143+
"Oem_Period": (320, 138),
144+
"Oem_6": (350, 138),
145+
"Rshift": (404, 138),
146+
147+
"Lcontrol": (19, 166),
148+
"Lwin": (54, 166),
149+
"Alt": (89, 166),
150+
"Space": (201, 166),
151+
"AltGr": (315, 166),
152+
"Rwin": (350, 166),
153+
"Menu": (384, 166),
154+
"Rcontrol": (424, 166),
155+
156+
"Up": (504, 138),
157+
"Left": (474, 166),
158+
"Down": (504, 166),
159+
"Right": (534, 166),
160+
161+
"Insert": (474, 56),
162+
"Home": (504, 56),
163+
"PageUp": (534, 56),
164+
"Delete": (474, 84),
165+
"End": (504, 84),
166+
"PageDown": (534, 84),
167+
168+
"NumLock": (576, 56),
169+
"Numpad-Divide": (605, 56),
170+
"Numpad-Multiply": (634, 56),
171+
"Numpad-Subtract": (664, 56),
172+
"Numpad-Add": (664, 98),
173+
"Numpad-Enter": (664, 152),
174+
"Numpad0": (590, 166),
175+
"Numpad1": (576, 138),
176+
"Numpad2": (605, 138),
177+
"Numpad3": (634, 138),
178+
"Numpad4": (576, 111),
179+
"Numpad5": (605, 111),
180+
"Numpad6": (634, 111),
181+
"Numpad7": (576, 84),
182+
"Numpad8": (605, 84),
183+
"Numpad9": (634, 84),
184+
"Numpad-Insert": (590, 166),
185+
"Numpad-Decimal": (634, 166),
186+
"Numpad-Delete": (634, 166),
187+
"Numpad-End": (576, 138),
188+
"Numpad-Down": (605, 138),
189+
"Numpad-PageDown": (634, 138),
190+
"Numpad-Left": (576, 111),
191+
"Numpad-Clear": (605, 111),
192+
"Numpad-Right": (634, 111),
193+
"Numpad-Home": (576, 84),
194+
"Numpad-Up": (605, 84),
195+
"Numpad-PageUp": (634, 84),
196+
}
197+
198+
"""Whether web modules and templates are automatically reloaded on change."""
199+
WebAutoReload = False
200+
201+
"""Whether web server is quiet or echoes access log."""
202+
WebQuiet = True
203+
204+
if getattr(sys, "frozen", False): # Running as a pyinstaller executable
205+
ExecutablePath = ShortcutIconPath = os.path.abspath(sys.executable)
206+
ApplicationPath = os.path.dirname(ExecutablePath)
207+
RootPath = os.path.join(os.environ.get("_MEIPASS2", getattr(sys, "_MEIPASS", "")))
208+
DbPath = os.path.join(ApplicationPath, "%s.db" % Title.lower())
209+
ConfigPath = os.path.join(ApplicationPath, "%s.ini" % Title.lower())
210+
else:
211+
RootPath = ApplicationPath = os.path.dirname(os.path.abspath(__file__))
212+
ExecutablePath = os.path.join(RootPath, "main.py")
213+
ShortcutIconPath = os.path.join(RootPath, "static", "icon.ico")
214+
DbPath = os.path.join(RootPath, "var", "%s.db" % Title.lower())
215+
ConfigPath = os.path.join(ApplicationPath, "var", "%s.ini" % Title.lower())
216+
217+
"""Path for static web content, like images and JavaScript files."""
218+
StaticPath = os.path.join(RootPath, "static")
219+
220+
"""Path for HTML templates."""
221+
TemplatePath = os.path.join(RootPath, "views")
222+
223+
"""Path for application icon file."""
224+
IconPath = os.path.join(StaticPath, "icon.ico")
225+
226+
227+
"""Statements to execute in database at startup, like CREATE TABLE."""
228+
DbStatements = (
229+
"CREATE TABLE IF NOT EXISTS moves (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP, x INTEGER, y INTEGER)",
230+
"CREATE TABLE IF NOT EXISTS clicks (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP, x INTEGER, y INTEGER, button INTEGER)",
231+
"CREATE TABLE IF NOT EXISTS scrolls (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP, x INTEGER, y INTEGER, wheel INTEGER)",
232+
"CREATE TABLE IF NOT EXISTS keys (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP, key TEXT, realkey TEXT)",
233+
"CREATE TABLE IF NOT EXISTS combos (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP, key TEXT, realkey TEXT)",
234+
"CREATE TABLE IF NOT EXISTS app_events (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP DEFAULT (DATETIME('now', 'localtime')), type TEXT)",
235+
"CREATE TABLE IF NOT EXISTS screen_sizes (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP DEFAULT (DATETIME('now', 'localtime')), x INTEGER, y INTEGER)",
236+
)
237+
238+
239+
def init(filename=ConfigPath):
240+
"""Loads INI configuration into this module's attributes."""
241+
section, parts = "DEFAULT", filename.rsplit(":", 1)
242+
if len(parts) > 1 and os.path.isfile(parts[0]): filename, section = parts
243+
if not os.path.isfile(filename): return
244+
245+
vardict, parser = globals(), configparser.RawConfigParser()
246+
parser.optionxform = str # Force case-sensitivity on names
247+
try:
248+
def parse_value(raw):
249+
try: return json.loads(raw) # Try to interpret as JSON
250+
except ValueError: return raw # JSON failed, fall back to raw
251+
txt = open(filename).read() # Add DEFAULT section if none present
252+
if not re.search("\\[\\w+\\]", txt): txt = "[DEFAULT]\n" + txt
253+
parser.readfp(StringIO.StringIO(txt), filename)
254+
for k, v in parser.items(section): vardict[k] = parse_value(v)
255+
except Exception:
256+
logging.warn("Error reading config from %s.", filename, exc_info=True)
257+
258+
259+
def save(filename=ConfigPath):
260+
"""Saves this module's changed attributes to INI configuration."""
261+
global DefaultValues
262+
parser = configparser.RawConfigParser()
263+
parser.optionxform = str # Force case-sensitivity on names
264+
try:
265+
save_types = basestring, int, float, tuple, list, dict, type(None)
266+
for k, v in sorted(globals().items()):
267+
if not isinstance(v, save_types) or k.startswith("_") \
268+
or DefaultValues.get(k, parser) == v: continue # for k, v
269+
try: parser.set("DEFAULT", k, json.dumps(v))
270+
except Exception: pass
271+
if parser.defaults():
272+
with open(filename, "wb") as f:
273+
f.write("# %s %s configuration written on %s.\n" % (Title, Version,
274+
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
275+
parser.write(f)
276+
else: # Nothing to write: delete configuration file
277+
try: os.unlink(filename)
278+
except Exception: pass
279+
except Exception:
280+
logging.warn("Error writing config to %s.", filename, exc_info=True)
281+
282+
283+
def register_defaults(values={}):
284+
"""Returns a once-assembled dict of this module's storable attributes."""
285+
if values: return values
286+
save_types = basestring, int, float, tuple, list, dict, type(None)
287+
for k, v in globals().items():
288+
if isinstance(v, save_types) and not k.startswith("_"): values[k] = v
289+
values["DefaultValues"] = values
290+
return values
291+
292+
293+
DefaultValues = register_defaults() # Store initial values to compare on saving

0 commit comments

Comments
 (0)