|
| 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