aa9c6e9f59
ADORE ----- * Major rearrangements were made, to reduce pinky use, and to balance out the hand usage. Tools ----- * The `hid-commands` tool will now display a notification when the **AppSel** layer is triggered. * The `log-to-heatmap.py` tool now treats the innermost keys on the bottom row as thumb keys, as far as statistics are concerned. Miscellaneous ------------- * Fixed the **Steno** toggle key. * My wife is now present on the keyboard too. Signed-off-by: Gergely Nagy <algernon@madhouse-project.org>
345 lines
12 KiB
Python
Executable file
345 lines
12 KiB
Python
Executable file
#! /usr/bin/env python3
|
|
import json
|
|
import os
|
|
import sys
|
|
import re
|
|
import argparse
|
|
import time
|
|
|
|
from math import floor
|
|
from os.path import dirname
|
|
from subprocess import Popen, PIPE, STDOUT
|
|
from blessings import Terminal
|
|
|
|
class Heatmap(object):
|
|
coords = [
|
|
[
|
|
# Row 0
|
|
[ 4, 0], [ 4, 2], [ 2, 0], [ 1, 0], [ 2, 2], [ 3, 0], [ 3, 2],
|
|
[ 3, 4], [ 3, 6], [ 2, 4], [ 1, 2], [ 2, 6], [ 4, 4], [ 4, 6],
|
|
],
|
|
[
|
|
# Row 1
|
|
[ 8, 0], [ 8, 2], [ 6, 0], [ 5, 0], [ 6, 2], [ 7, 0], [ 7, 2],
|
|
[ 7, 4], [ 7, 6], [ 6, 4], [ 5, 2], [ 6, 6], [ 8, 4], [ 8, 6],
|
|
],
|
|
[
|
|
# Row 2
|
|
[12, 0], [12, 2], [10, 0], [ 9, 0], [10, 2], [11, 0], [ ],
|
|
[ ], [11, 2], [10, 4], [ 9, 2], [10, 6], [12, 4], [12, 6],
|
|
],
|
|
[
|
|
# Row 3
|
|
[17, 0], [17, 2], [15, 0], [14, 0], [15, 2], [16, 0], [13, 0],
|
|
[13, 2], [16, 2], [15, 4], [14, 2], [15, 6], [17, 4], [17, 6],
|
|
],
|
|
[
|
|
# Row 4
|
|
[20, 0], [20, 2], [19, 0], [18, 0], [19, 2], [], [], [], [],
|
|
[19, 4], [18, 2], [19, 6], [20, 4], [20, 6], [], [], [], []
|
|
],
|
|
[
|
|
# Row 5
|
|
[ ], [23, 0], [22, 2], [22, 0], [22, 4], [21, 0], [21, 2],
|
|
[24, 0], [24, 2], [25, 0], [25, 4], [25, 2], [26, 0], [ ],
|
|
],
|
|
]
|
|
|
|
def set_attr_at(self, block, n, attr, fn, val):
|
|
blk = self.heatmap[block][n]
|
|
if attr in blk:
|
|
blk[attr] = fn(blk[attr], val)
|
|
else:
|
|
blk[attr] = fn(None, val)
|
|
|
|
def coord(self, col, row):
|
|
return self.coords[row][col]
|
|
|
|
@staticmethod
|
|
def set_attr(orig, new):
|
|
return new
|
|
|
|
def set_bg(self, coords, color):
|
|
(block, n) = coords
|
|
self.set_attr_at(block, n, "c", self.set_attr, color)
|
|
#self.set_attr_at(block, n, "g", self.set_attr, False)
|
|
|
|
def set_tap_info(self, coords, count, cap):
|
|
(block, n) = coords
|
|
def _set_tap_info(o, _count, _cap):
|
|
ns = 4 - o.count ("\n")
|
|
return o + "\n" * ns + "%.02f%%" % (float(_count) / float(_cap) * 100)
|
|
|
|
if not cap:
|
|
cap = 1
|
|
self.heatmap[block][n + 1] = _set_tap_info (self.heatmap[block][n + 1], count, cap)
|
|
|
|
@staticmethod
|
|
def heatmap_color (v):
|
|
colors = [ [0.3, 0.3, 1], [0.3, 1, 0.3], [1, 1, 0.3], [1, 0.3, 0.3]]
|
|
fb = 0
|
|
if v <= 0:
|
|
idx1, idx2 = 0, 0
|
|
elif v >= 1:
|
|
idx1, idx2 = len(colors) - 1, len(colors) - 1
|
|
else:
|
|
val = v * (len(colors) - 1)
|
|
idx1 = int(floor(val))
|
|
idx2 = idx1 + 1
|
|
fb = val - float(idx1)
|
|
|
|
r = (colors[idx2][0] - colors[idx1][0]) * fb + colors[idx1][0]
|
|
g = (colors[idx2][1] - colors[idx1][1]) * fb + colors[idx1][1]
|
|
b = (colors[idx2][2] - colors[idx1][2]) * fb + colors[idx1][2]
|
|
|
|
r, g, b = [x * 255 for x in (r, g, b)]
|
|
return "#%02x%02x%02x" % (int(r), int(g), int(b))
|
|
|
|
def __init__(self, layout):
|
|
self.log = {}
|
|
self.total = 0
|
|
self.max_cnt = 0
|
|
self.layout = layout
|
|
|
|
def update_log(self, coords):
|
|
(c, r) = coords
|
|
if not (c, r) in self.log:
|
|
self.log[(c, r)] = 0
|
|
self.log[(c, r)] = self.log[(c, r)] + 1
|
|
self.total = self.total + 1
|
|
if self.max_cnt < self.log[(c, r)]:
|
|
self.max_cnt = self.log[(c, r)]
|
|
|
|
def get_heatmap(self):
|
|
with open("%s/heatmap-layout.%s.json" % (dirname(sys.argv[0]), self.layout), "r") as f:
|
|
self.heatmap = json.load (f)
|
|
|
|
## Reset colors
|
|
for row in self.coords:
|
|
for coord in row:
|
|
if coord != []:
|
|
self.set_bg (coord, "#d9dae0")
|
|
|
|
for (c, r) in self.log:
|
|
coords = self.coord(c, r)
|
|
b, n = coords
|
|
cap = self.max_cnt
|
|
if cap == 0:
|
|
cap = 1
|
|
v = float(self.log[(c, r)]) / cap
|
|
self.set_bg (coords, self.heatmap_color (v))
|
|
self.set_tap_info (coords, self.log[(c, r)], self.total)
|
|
return self.heatmap
|
|
|
|
def get_stats(self):
|
|
usage = [
|
|
# left hand
|
|
[0, 0, 0, 0, 0],
|
|
# right hand
|
|
[0, 0, 0, 0, 0]
|
|
]
|
|
finger_map = [0, 0, 1, 2, 3, 3, 3, 1, 1, 1, 2, 3, 4, 4]
|
|
for (c, r) in self.log:
|
|
if r == 5: # thumb cluster
|
|
if c <= 6: # left side
|
|
usage[0][4] = usage[0][4] + self.log[(c, r)]
|
|
else:
|
|
usage[1][0] = usage[1][0] + self.log[(c, r)]
|
|
elif r == 4 and (c == 4 or c == 9): # bottom row thumb keys
|
|
if c <= 6: # left side
|
|
usage[0][4] = usage[0][4] + self.log[(c, r)]
|
|
else:
|
|
usage[1][0] = usage[1][0] + self.log[(c, r)]
|
|
else:
|
|
fc = c
|
|
hand = 0
|
|
if fc >= 7:
|
|
hand = 1
|
|
fm = finger_map[fc]
|
|
usage[hand][fm] = usage[hand][fm] + self.log[(c, r)]
|
|
hand_usage = [0, 0]
|
|
for f in usage[0]:
|
|
hand_usage[0] = hand_usage[0] + f
|
|
for f in usage[1]:
|
|
hand_usage[1] = hand_usage[1] + f
|
|
|
|
total = self.total
|
|
if total == 0:
|
|
total = 1
|
|
stats = {
|
|
"total-keys": total,
|
|
"hands": {
|
|
"left": {
|
|
"usage": round(float(hand_usage[0]) / total * 100, 2),
|
|
"fingers": {
|
|
"pinky": 0,
|
|
"ring": 0,
|
|
"middle": 0,
|
|
"index": 0,
|
|
"thumb": 0,
|
|
}
|
|
},
|
|
"right": {
|
|
"usage": round(float(hand_usage[1]) / total * 100, 2),
|
|
"fingers": {
|
|
"thumb": 0,
|
|
"index": 0,
|
|
"middle": 0,
|
|
"ring": 0,
|
|
"pinky": 0,
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
hmap = ['left', 'right']
|
|
fmap = ['pinky', 'ring', 'middle', 'index', 'thumb',
|
|
'thumb', 'index', 'middle', 'ring', 'pinky']
|
|
for hand_idx in range(len(usage)):
|
|
hand = usage[hand_idx]
|
|
for finger_idx in range(len(hand)):
|
|
stats['hands'][hmap[hand_idx]]['fingers'][fmap[finger_idx + hand_idx * 5]] = round(float(hand[finger_idx]) / total * 100, 2)
|
|
return stats
|
|
|
|
def dump_all(out_dir, heatmaps):
|
|
stats = {}
|
|
t = Terminal()
|
|
t.clear()
|
|
sys.stdout.write("\x1b[2J\x1b[H")
|
|
|
|
print ('{t.underline}{outdir}{t.normal}\n'.format(t=t, outdir=out_dir))
|
|
|
|
keys = list(heatmaps.keys())
|
|
keys.sort()
|
|
|
|
for layer in keys:
|
|
if len(heatmaps[layer].log) == 0:
|
|
continue
|
|
|
|
with open ("%s/%s.json" % (out_dir, layer), "w") as f:
|
|
json.dump(heatmaps[layer].get_heatmap(), f)
|
|
stats[layer] = heatmaps[layer].get_stats()
|
|
|
|
left = stats[layer]['hands']['left']
|
|
right = stats[layer]['hands']['right']
|
|
|
|
print ('{t.bold}{layer}{t.normal} ({total:,} taps):'.format(t=t, layer=layer,
|
|
total=int(stats[layer]['total-keys'] / 2)))
|
|
print (('{t.underline} | ' + \
|
|
'left ({l[usage]:6.2f}%) | ' + \
|
|
'right ({r[usage]:6.2f}%) |{t.normal}').format(t=t, l=left, r=right))
|
|
print ((' {t.bright_magenta}pinky{t.white} | {left[pinky]:6.2f}% | {right[pinky]:6.2f}% |\n' + \
|
|
' {t.bright_cyan}ring{t.white} | {left[ring]:6.2f}% | {right[ring]:6.2f}% |\n' + \
|
|
' {t.bright_blue}middle{t.white} | {left[middle]:6.2f}% | {right[middle]:6.2f}% |\n' + \
|
|
' {t.bright_green}index{t.white} | {left[index]:6.2f}% | {right[index]:6.2f}% |\n' + \
|
|
' {t.bright_red}thumb{t.white} | {left[thumb]:6.2f}% | {right[thumb]:6.2f}% |\n' + \
|
|
'').format(left=left['fingers'], right=right['fingers'], t=t))
|
|
|
|
def process_line(line, heatmaps, opts, stamped_log = None):
|
|
m = re.search ('KL: col=(\d+), row=(\d+), pressed=(\d+), layer=(.*)', line)
|
|
if not m:
|
|
return False
|
|
if stamped_log is not None:
|
|
if line.startswith("KL:"):
|
|
print ("%10.10f %s" % (time.time(), line),
|
|
file = stamped_log, end = '')
|
|
else:
|
|
print (line,
|
|
file = stamped_log, end = '')
|
|
stamped_log.flush()
|
|
|
|
(c, r, l) = (int(m.group (2)), int(m.group (1)), m.group (4))
|
|
if (c, r) not in opts.allowed_keys:
|
|
return False
|
|
|
|
heatmaps[l].update_log ((c, r))
|
|
|
|
return True
|
|
|
|
def setup_allowed_keys(opts):
|
|
if len(opts.only_key):
|
|
incmap={}
|
|
for v in opts.only_key:
|
|
m = re.search ('(\d+),(\d+)', v)
|
|
if not m:
|
|
continue
|
|
(c, r) = (int(m.group(1)), int(m.group(2)))
|
|
incmap[(c, r)] = True
|
|
else:
|
|
incmap={}
|
|
for r in range(0, 6):
|
|
for c in range(0, 14):
|
|
incmap[(c, r)] = True
|
|
|
|
for v in opts.ignore_key:
|
|
m = re.search ('(\d+),(\d+)', v)
|
|
if not m:
|
|
continue
|
|
(c, r) = (int(m.group(1)), int(m.group(2)))
|
|
del(incmap[(c, r)])
|
|
|
|
return incmap
|
|
|
|
def main(opts):
|
|
heatmaps = {"Dvorak": Heatmap("Dvorak"),
|
|
"ADORE": Heatmap("ADORE")
|
|
}
|
|
cnt = 0
|
|
out_dir = opts.outdir
|
|
|
|
if not os.path.exists(out_dir):
|
|
os.makedirs(out_dir)
|
|
|
|
opts.allowed_keys = setup_allowed_keys(opts)
|
|
|
|
if not opts.one_shot:
|
|
|
|
try:
|
|
with open("%s/stamped-log" % out_dir, "r") as f:
|
|
while True:
|
|
line = f.readline()
|
|
if not line:
|
|
break
|
|
if not process_line(line, heatmaps, opts):
|
|
continue
|
|
except:
|
|
pass
|
|
|
|
stamped_log = open ("%s/stamped-log" % (out_dir), "a+")
|
|
else:
|
|
stamped_log = None
|
|
|
|
while True:
|
|
line = sys.stdin.readline()
|
|
if not line:
|
|
break
|
|
if not process_line(line, heatmaps, opts, stamped_log):
|
|
continue
|
|
|
|
cnt = cnt + 1
|
|
|
|
if opts.dump_interval != -1 and cnt >= opts.dump_interval and not opts.one_shot:
|
|
cnt = 0
|
|
dump_all(out_dir, heatmaps)
|
|
|
|
dump_all (out_dir, heatmaps)
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser (description = "keylog to heatmap processor")
|
|
parser.add_argument ('outdir', action = 'store',
|
|
help = 'Output directory')
|
|
parser.add_argument ('--dump-interval', dest = 'dump_interval', action = 'store', type = int,
|
|
default = 100, help = 'Dump stats and heatmap at every Nth event, -1 for dumping at EOF only')
|
|
parser.add_argument ('--ignore-key', dest = 'ignore_key', action = 'append', type = str,
|
|
default = [], help = 'Ignore the key at position (x, y)')
|
|
parser.add_argument ('--only-key', dest = 'only_key', action = 'append', type = str,
|
|
default = [], help = 'Only include key at position (x, y)')
|
|
parser.add_argument ('--one-shot', dest = 'one_shot', action = 'store_true',
|
|
help = 'Do not load previous data, and do not update it, either.')
|
|
args = parser.parse_args()
|
|
if len(args.ignore_key) and len(args.only_key):
|
|
print ("--ignore-key and --only-key are mutually exclusive, please only use one of them!",
|
|
file = sys.stderr)
|
|
sys.exit(1)
|
|
main(args)
|