diff options
Diffstat (limited to 'layouts/community/ergodox/algernon/tools/log-to-heatmap.py')
| -rwxr-xr-x | layouts/community/ergodox/algernon/tools/log-to-heatmap.py | 342 |
1 files changed, 0 insertions, 342 deletions
diff --git a/layouts/community/ergodox/algernon/tools/log-to-heatmap.py b/layouts/community/ergodox/algernon/tools/log-to-heatmap.py deleted file mode 100755 index 5f52d9932..000000000 --- a/layouts/community/ergodox/algernon/tools/log-to-heatmap.py +++ /dev/null | |||
| @@ -1,342 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | import json | ||
| 3 | import os | ||
| 4 | import sys | ||
| 5 | import re | ||
| 6 | import argparse | ||
| 7 | import time | ||
| 8 | |||
| 9 | from math import floor | ||
| 10 | from os.path import dirname | ||
| 11 | from blessings import Terminal | ||
| 12 | |||
| 13 | class Heatmap(object): | ||
| 14 | coords = [ | ||
| 15 | [ | ||
| 16 | # Row 0 | ||
| 17 | [ 4, 0], [ 4, 2], [ 2, 0], [ 1, 0], [ 2, 2], [ 3, 0], [ 3, 2], | ||
| 18 | [ 3, 4], [ 3, 6], [ 2, 4], [ 1, 2], [ 2, 6], [ 4, 4], [ 4, 6], | ||
| 19 | ], | ||
| 20 | [ | ||
| 21 | # Row 1 | ||
| 22 | [ 8, 0], [ 8, 2], [ 6, 0], [ 5, 0], [ 6, 2], [ 7, 0], [ 7, 2], | ||
| 23 | [ 7, 4], [ 7, 6], [ 6, 4], [ 5, 2], [ 6, 6], [ 8, 4], [ 8, 6], | ||
| 24 | ], | ||
| 25 | [ | ||
| 26 | # Row 2 | ||
| 27 | [12, 0], [12, 2], [10, 0], [ 9, 0], [10, 2], [11, 0], [ ], | ||
| 28 | [ ], [11, 2], [10, 4], [ 9, 2], [10, 6], [12, 4], [12, 6], | ||
| 29 | ], | ||
| 30 | [ | ||
| 31 | # Row 3 | ||
| 32 | [17, 0], [17, 2], [15, 0], [14, 0], [15, 2], [16, 0], [13, 0], | ||
| 33 | [13, 2], [16, 2], [15, 4], [14, 2], [15, 6], [17, 4], [17, 6], | ||
| 34 | ], | ||
| 35 | [ | ||
| 36 | # Row 4 | ||
| 37 | [20, 0], [20, 2], [19, 0], [18, 0], [19, 2], [], [], [], [], | ||
| 38 | [19, 4], [18, 2], [19, 6], [20, 4], [20, 6], [], [], [], [] | ||
| 39 | ], | ||
| 40 | [ | ||
| 41 | # Row 5 | ||
| 42 | [ ], [23, 0], [22, 2], [22, 0], [22, 4], [21, 0], [21, 2], | ||
| 43 | [24, 0], [24, 2], [25, 0], [25, 4], [25, 2], [26, 0], [ ], | ||
| 44 | ], | ||
| 45 | ] | ||
| 46 | |||
| 47 | def set_attr_at(self, block, n, attr, fn, val): | ||
| 48 | blk = self.heatmap[block][n] | ||
| 49 | if attr in blk: | ||
| 50 | blk[attr] = fn(blk[attr], val) | ||
| 51 | else: | ||
| 52 | blk[attr] = fn(None, val) | ||
| 53 | |||
| 54 | def coord(self, col, row): | ||
| 55 | return self.coords[row][col] | ||
| 56 | |||
| 57 | @staticmethod | ||
| 58 | def set_attr(orig, new): | ||
| 59 | return new | ||
| 60 | |||
| 61 | def set_bg(self, coords, color): | ||
| 62 | (block, n) = coords | ||
| 63 | self.set_attr_at(block, n, "c", self.set_attr, color) | ||
| 64 | #self.set_attr_at(block, n, "g", self.set_attr, False) | ||
| 65 | |||
| 66 | def set_tap_info(self, coords, count, cap): | ||
| 67 | (block, n) = coords | ||
| 68 | def _set_tap_info(o, _count, _cap): | ||
| 69 | ns = 4 - o.count ("\n") | ||
| 70 | return o + "\n" * ns + "%.02f%%" % (float(_count) / float(_cap) * 100) | ||
| 71 | |||
| 72 | if not cap: | ||
| 73 | cap = 1 | ||
| 74 | self.heatmap[block][n + 1] = _set_tap_info (self.heatmap[block][n + 1], count, cap) | ||
| 75 | |||
| 76 | @staticmethod | ||
| 77 | def heatmap_color (v): | ||
| 78 | colors = [ [0.3, 0.3, 1], [0.3, 1, 0.3], [1, 1, 0.3], [1, 0.3, 0.3]] | ||
| 79 | fb = 0 | ||
| 80 | if v <= 0: | ||
| 81 | idx1, idx2 = 0, 0 | ||
| 82 | elif v >= 1: | ||
| 83 | idx1, idx2 = len(colors) - 1, len(colors) - 1 | ||
| 84 | else: | ||
| 85 | val = v * (len(colors) - 1) | ||
| 86 | idx1 = int(floor(val)) | ||
| 87 | idx2 = idx1 + 1 | ||
| 88 | fb = val - float(idx1) | ||
| 89 | |||
| 90 | r = (colors[idx2][0] - colors[idx1][0]) * fb + colors[idx1][0] | ||
| 91 | g = (colors[idx2][1] - colors[idx1][1]) * fb + colors[idx1][1] | ||
| 92 | b = (colors[idx2][2] - colors[idx1][2]) * fb + colors[idx1][2] | ||
| 93 | |||
| 94 | r, g, b = [x * 255 for x in (r, g, b)] | ||
| 95 | return "#%02x%02x%02x" % (int(r), int(g), int(b)) | ||
| 96 | |||
| 97 | def __init__(self, layout): | ||
| 98 | self.log = {} | ||
| 99 | self.total = 0 | ||
| 100 | self.max_cnt = 0 | ||
| 101 | self.layout = layout | ||
| 102 | |||
| 103 | def update_log(self, coords): | ||
| 104 | (c, r) = coords | ||
| 105 | if not (c, r) in self.log: | ||
| 106 | self.log[(c, r)] = 0 | ||
| 107 | self.log[(c, r)] = self.log[(c, r)] + 1 | ||
| 108 | self.total = self.total + 1 | ||
| 109 | if self.max_cnt < self.log[(c, r)]: | ||
| 110 | self.max_cnt = self.log[(c, r)] | ||
| 111 | |||
| 112 | def get_heatmap(self): | ||
| 113 | with open("%s/heatmap-layout.%s.json" % (dirname(sys.argv[0]), self.layout), "r") as f: | ||
| 114 | self.heatmap = json.load (f) | ||
| 115 | |||
| 116 | ## Reset colors | ||
| 117 | for row in self.coords: | ||
| 118 | for coord in row: | ||
| 119 | if coord != []: | ||
| 120 | self.set_bg (coord, "#d9dae0") | ||
| 121 | |||
| 122 | for (c, r) in self.log: | ||
| 123 | coords = self.coord(c, r) | ||
| 124 | cap = self.max_cnt | ||
| 125 | if cap == 0: | ||
| 126 | cap = 1 | ||
| 127 | v = float(self.log[(c, r)]) / cap | ||
| 128 | self.set_bg (coords, self.heatmap_color (v)) | ||
| 129 | self.set_tap_info (coords, self.log[(c, r)], self.total) | ||
| 130 | return self.heatmap | ||
| 131 | |||
| 132 | def get_stats(self): | ||
| 133 | usage = [ | ||
| 134 | # left hand | ||
| 135 | [0, 0, 0, 0, 0], | ||
| 136 | # right hand | ||
| 137 | [0, 0, 0, 0, 0] | ||
| 138 | ] | ||
| 139 | finger_map = [0, 0, 1, 2, 3, 3, 3, 1, 1, 1, 2, 3, 4, 4] | ||
| 140 | for (c, r) in self.log: | ||
| 141 | if r == 5: # thumb cluster | ||
| 142 | if c <= 6: # left side | ||
| 143 | usage[0][4] = usage[0][4] + self.log[(c, r)] | ||
| 144 | else: | ||
| 145 | usage[1][0] = usage[1][0] + self.log[(c, r)] | ||
| 146 | elif r == 4 and (c == 4 or c == 9): # bottom row thumb keys | ||
| 147 | if c <= 6: # left side | ||
| 148 | usage[0][4] = usage[0][4] + self.log[(c, r)] | ||
| 149 | else: | ||
| 150 | usage[1][0] = usage[1][0] + self.log[(c, r)] | ||
| 151 | else: | ||
| 152 | fc = c | ||
| 153 | hand = 0 | ||
| 154 | if fc >= 7: | ||
| 155 | hand = 1 | ||
| 156 | fm = finger_map[fc] | ||
| 157 | usage[hand][fm] = usage[hand][fm] + self.log[(c, r)] | ||
| 158 | hand_usage = [0, 0] | ||
| 159 | for f in usage[0]: | ||
| 160 | hand_usage[0] = hand_usage[0] + f | ||
| 161 | for f in usage[1]: | ||
| 162 | hand_usage[1] = hand_usage[1] + f | ||
| 163 | |||
| 164 | total = self.total | ||
| 165 | if total == 0: | ||
| 166 | total = 1 | ||
| 167 | stats = { | ||
| 168 | "total-keys": total, | ||
| 169 | "hands": { | ||
| 170 | "left": { | ||
| 171 | "usage": round(float(hand_usage[0]) / total * 100, 2), | ||
| 172 | "fingers": { | ||
| 173 | "pinky": 0, | ||
| 174 | "ring": 0, | ||
| 175 | "middle": 0, | ||
| 176 | "index": 0, | ||
| 177 | "thumb": 0, | ||
| 178 | } | ||
| 179 | }, | ||
| 180 | "right": { | ||
| 181 | "usage": round(float(hand_usage[1]) / total * 100, 2), | ||
| 182 | "fingers": { | ||
| 183 | "thumb": 0, | ||
| 184 | "index": 0, | ||
| 185 | "middle": 0, | ||
| 186 | "ring": 0, | ||
| 187 | "pinky": 0, | ||
| 188 | } | ||
| 189 | }, | ||
| 190 | } | ||
| 191 | } | ||
| 192 | |||
| 193 | hmap = ['left', 'right'] | ||
| 194 | fmap = ['pinky', 'ring', 'middle', 'index', 'thumb', | ||
| 195 | 'thumb', 'index', 'middle', 'ring', 'pinky'] | ||
| 196 | for hand_idx in range(len(usage)): | ||
| 197 | hand = usage[hand_idx] | ||
| 198 | for finger_idx in range(len(hand)): | ||
| 199 | stats['hands'][hmap[hand_idx]]['fingers'][fmap[finger_idx + hand_idx * 5]] = round(float(hand[finger_idx]) / total * 100, 2) | ||
| 200 | return stats | ||
| 201 | |||
| 202 | def dump_all(out_dir, heatmaps): | ||
| 203 | stats = {} | ||
| 204 | t = Terminal() | ||
| 205 | t.clear() | ||
| 206 | sys.stdout.write("\x1b[2J\x1b[H") | ||
| 207 | |||
| 208 | print ('{t.underline}{outdir}{t.normal}\n'.format(t=t, outdir=out_dir)) | ||
| 209 | |||
| 210 | keys = list(heatmaps.keys()) | ||
| 211 | keys.sort() | ||
| 212 | |||
| 213 | for layer in keys: | ||
| 214 | if len(heatmaps[layer].log) == 0: | ||
| 215 | continue | ||
| 216 | |||
| 217 | with open ("%s/%s.json" % (out_dir, layer), "w") as f: | ||
| 218 | json.dump(heatmaps[layer].get_heatmap(), f) | ||
| 219 | stats[layer] = heatmaps[layer].get_stats() | ||
| 220 | |||
| 221 | left = stats[layer]['hands']['left'] | ||
| 222 | right = stats[layer]['hands']['right'] | ||
| 223 | |||
| 224 | print ('{t.bold}{layer}{t.normal} ({total:,} taps):'.format(t=t, layer=layer, | ||
| 225 | total=int(stats[layer]['total-keys'] / 2))) | ||
| 226 | print (('{t.underline} | ' + \ | ||
| 227 | 'left ({l[usage]:6.2f}%) | ' + \ | ||
| 228 | 'right ({r[usage]:6.2f}%) |{t.normal}').format(t=t, l=left, r=right)) | ||
| 229 | print ((' {t.bright_magenta}pinky{t.white} | {left[pinky]:6.2f}% | {right[pinky]:6.2f}% |\n' + \ | ||
| 230 | ' {t.bright_cyan}ring{t.white} | {left[ring]:6.2f}% | {right[ring]:6.2f}% |\n' + \ | ||
| 231 | ' {t.bright_blue}middle{t.white} | {left[middle]:6.2f}% | {right[middle]:6.2f}% |\n' + \ | ||
| 232 | ' {t.bright_green}index{t.white} | {left[index]:6.2f}% | {right[index]:6.2f}% |\n' + \ | ||
| 233 | ' {t.bright_red}thumb{t.white} | {left[thumb]:6.2f}% | {right[thumb]:6.2f}% |\n' + \ | ||
| 234 | '').format(left=left['fingers'], right=right['fingers'], t=t)) | ||
| 235 | |||
| 236 | def process_line(line, heatmaps, opts, stamped_log = None): | ||
| 237 | m = re.search ('KL: col=(\d+), row=(\d+), pressed=(\d+), layer=(.*)', line) | ||
| 238 | if not m: | ||
| 239 | return False | ||
| 240 | if stamped_log is not None: | ||
| 241 | if line.startswith("KL:"): | ||
| 242 | print ("%10.10f %s" % (time.time(), line), | ||
| 243 | file = stamped_log, end = '') | ||
| 244 | else: | ||
| 245 | print (line, | ||
| 246 | file = stamped_log, end = '') | ||
| 247 | stamped_log.flush() | ||
| 248 | |||
| 249 | (c, r, l) = (int(m.group (2)), int(m.group (1)), m.group (4)) | ||
| 250 | if (c, r) not in opts.allowed_keys: | ||
| 251 | return False | ||
| 252 | |||
| 253 | heatmaps[l].update_log ((c, r)) | ||
| 254 | |||
| 255 | return True | ||
| 256 | |||
| 257 | def setup_allowed_keys(opts): | ||
| 258 | if len(opts.only_key): | ||
| 259 | incmap={} | ||
| 260 | for v in opts.only_key: | ||
| 261 | m = re.search ('(\d+),(\d+)', v) | ||
| 262 | if not m: | ||
| 263 | continue | ||
| 264 | (c, r) = (int(m.group(1)), int(m.group(2))) | ||
| 265 | incmap[(c, r)] = True | ||
| 266 | else: | ||
| 267 | incmap={} | ||
| 268 | for r in range(0, 6): | ||
| 269 | for c in range(0, 14): | ||
| 270 | incmap[(c, r)] = True | ||
| 271 | |||
| 272 | for v in opts.ignore_key: | ||
| 273 | m = re.search ('(\d+),(\d+)', v) | ||
| 274 | if not m: | ||
| 275 | continue | ||
| 276 | (c, r) = (int(m.group(1)), int(m.group(2))) | ||
| 277 | del(incmap[(c, r)]) | ||
| 278 | |||
| 279 | return incmap | ||
| 280 | |||
| 281 | def main(opts): | ||
| 282 | heatmaps = {"Dvorak": Heatmap("Dvorak"), | ||
| 283 | "ADORE": Heatmap("ADORE") | ||
| 284 | } | ||
| 285 | cnt = 0 | ||
| 286 | out_dir = opts.outdir | ||
| 287 | |||
| 288 | if not os.path.exists(out_dir): | ||
| 289 | os.makedirs(out_dir) | ||
| 290 | |||
| 291 | opts.allowed_keys = setup_allowed_keys(opts) | ||
| 292 | |||
| 293 | if not opts.one_shot: | ||
| 294 | |||
| 295 | try: | ||
| 296 | with open("%s/stamped-log" % out_dir, "r") as f: | ||
| 297 | while True: | ||
| 298 | line = f.readline() | ||
| 299 | if not line: | ||
| 300 | break | ||
| 301 | if not process_line(line, heatmaps, opts): | ||
| 302 | continue | ||
| 303 | except Exception: | ||
| 304 | pass | ||
| 305 | |||
| 306 | stamped_log = open ("%s/stamped-log" % (out_dir), "a+") | ||
| 307 | else: | ||
| 308 | stamped_log = None | ||
| 309 | |||
| 310 | while True: | ||
| 311 | line = sys.stdin.readline() | ||
| 312 | if not line: | ||
| 313 | break | ||
| 314 | if not process_line(line, heatmaps, opts, stamped_log): | ||
| 315 | continue | ||
| 316 | |||
| 317 | cnt = cnt + 1 | ||
| 318 | |||
| 319 | if opts.dump_interval != -1 and cnt >= opts.dump_interval and not opts.one_shot: | ||
| 320 | cnt = 0 | ||
| 321 | dump_all(out_dir, heatmaps) | ||
| 322 | |||
| 323 | dump_all (out_dir, heatmaps) | ||
| 324 | |||
| 325 | if __name__ == "__main__": | ||
| 326 | parser = argparse.ArgumentParser (description = "keylog to heatmap processor") | ||
| 327 | parser.add_argument ('outdir', action = 'store', | ||
| 328 | help = 'Output directory') | ||
| 329 | parser.add_argument ('--dump-interval', dest = 'dump_interval', action = 'store', type = int, | ||
| 330 | default = 100, help = 'Dump stats and heatmap at every Nth event, -1 for dumping at EOF only') | ||
| 331 | parser.add_argument ('--ignore-key', dest = 'ignore_key', action = 'append', type = str, | ||
| 332 | default = [], help = 'Ignore the key at position (x, y)') | ||
| 333 | parser.add_argument ('--only-key', dest = 'only_key', action = 'append', type = str, | ||
| 334 | default = [], help = 'Only include key at position (x, y)') | ||
| 335 | parser.add_argument ('--one-shot', dest = 'one_shot', action = 'store_true', | ||
| 336 | help = 'Do not load previous data, and do not update it, either.') | ||
| 337 | args = parser.parse_args() | ||
| 338 | if len(args.ignore_key) and len(args.only_key): | ||
| 339 | print ("--ignore-key and --only-key are mutually exclusive, please only use one of them!", | ||
| 340 | file = sys.stderr) | ||
| 341 | sys.exit(1) | ||
| 342 | main(args) | ||
