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