aboutsummaryrefslogtreecommitdiff
path: root/layouts/community/ergodox/algernon/tools/log-to-heatmap.py
diff options
context:
space:
mode:
Diffstat (limited to 'layouts/community/ergodox/algernon/tools/log-to-heatmap.py')
-rwxr-xr-xlayouts/community/ergodox/algernon/tools/log-to-heatmap.py342
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
2import json
3import os
4import sys
5import re
6import argparse
7import time
8
9from math import floor
10from os.path import dirname
11from blessings import Terminal
12
13class 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
202def 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
236def 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
257def 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
281def 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
325if __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)