eatabrick.org

For to be to make you smarter. For to be to get you dead.

Keyboard 2014-11-05

I have recently been investigating building or purchasing a 60% keyboard. Before committing to this investment, I wanted to get an idea of how much I actually use the keys outside of the 60% layout. There may have been some kind of tool to show me this but I decided to whip up a very rudimentary keylogger and let it run for a few hours while I used my computer.

#!/bin/sh

id=$(xinput --list |grep 'USB Keyboard' |head -1 |grep -oP '(?<=id=)\d+')
xinput --test $id >> ~/Documents/keylog.txt

I did say it was rudimentary. Better data might be attained with a better method of logging keystrokes, as xinput seems to regard holding a key down as a long series of press and release events rather than a single one. After collecting the data for a few hours I created a script to put the data into a useful format for me to digest.

#!/usr/bin/env perl

package KeyMap;

use 5.010;
use strict;
use warnings;

use SVG;

sub new {
  my ($class, @args) = @_;

  my $self = bless {
    presses => [],
    total   => 0,
    svg     => SVG->new(
      width  => 940,
      height => 300,
      @args,
    ),
  }, $class;
}

sub svg { shift->{svg} }

sub make_key {
  my ($self, $x, $y, $w, $h, $text, $color) = @_;

  my ($r, $g, $b) = @$color;
  my $tcol = $color->[3] ? '#fff' : '#000';

  $self->svg->rectangle(
    x      => $x + 2,
    y      => $y + 2,
    width  => $w - 4,
    height => $h - 4,
    rx     => 5,
    ry     => 5,
    style  => {
      stroke => '#000',
      fill   => "rgb($r,$g,$b)",
    });

  $self->svg->text(
    x     => $x + $w / 2,
    y     => $y + $h / 2 + 3,
    style => {
      'fill'        => $tcol,
      'text-align'  => 'center',
      'text-anchor' => 'middle',
      'font-family' => 'Noto Sans',
      'font-size'   => '10px',
    },
  )->cdata($text);
}

sub make_row {
  my ($self, $x, $y, @keys) = @_;

  foreach (@keys) {
    $_ = [ $_, 1, 1 ] unless ref $_;
    my ($code, $w, $h) = @$_;
    $w ||= 1;
    $h ||= 1;

    $self->make_key($x, $y, $w * 40, $h * 40,
      sprintf('%0.2f', $self->percent($code)),
      $self->color($code));
    $x += $w * 40;
  }
}

sub add_key_press {
  my ($self, $code) = @_;

  $self->{presses}[$code]++;
  $self->{total}++;

  delete $self->{max_percent};
}

sub presses { shift->{presses}[shift] || 0 }
sub total { shift->{total} }

sub max_percent {
  my ($self) = @_;

  return $self->{max_percent} if exists $self->{max_percent};

  my $max = 0;
  for (@{ $self->{presses} }) {
    $max = $_ if $_ and $_ > $max;
  }

  return $self->{max_percent} = $max / $self->total * 100;
}

sub percent {
  my ($self, $code) = @_;

  return $self->presses($code) / $self->total * 100;
}

sub color {
  my ($self, $code) = @_;

  # Crazy exponential scaling gotten experimentally
  my $scale = ($self->percent($code) / $self->max_percent) ** 0.3;

  my ($r, $gb, $t);

  if ($scale > 0.66) {
    $r = 255 - int(128 * ($scale - 0.66) / 0.33);
    $gb = 0;
    $t = 1;
  } else {
    $r = 255;
    $gb = 255 - int(255 * $scale / 0.66);
    $t = 0;
  }

  return [ $r, $gb, $gb, $t ];
}

sub render {
  my ($self) = @_;

  # base keys
  $self->make_row(20, 80, 49, 10 .. 21, [ 22, 2 ]);
  $self->make_row(20, 120, [ 23, 1.5 ],  24 .. 35, [ 51, 1.5 ]);
  $self->make_row(20, 160, [ 66, 1.75 ], 38 .. 48, [ 36, 2.25 ]);
  $self->make_row(20, 200, [ 50, 2.25 ], 52 .. 61, [ 62, 2.75 ]);
  $self->make_row(
    20,
    240,
    [ 37,  1.25 ],
    [ 133, 1.25 ],
    [ 64,  1.25 ],
    [ 65,  6.25 ],
    [ 108, 1.25 ],
    [ 134, 1.25 ],
    [ 135, 1.25 ],
    [ 105, 1.25 ]);

  # function keys
  $self->make_row(20,  20, 9);
  $self->make_row(100, 20, 67 .. 70);
  $self->make_row(280, 20, 71 .. 74);
  $self->make_row(460, 20, 75, 76, 95, 96);

  # navigation
  $self->make_row(630, 20,  107, 78,  127);
  $self->make_row(630, 80,  118, 110, 112);
  $self->make_row(630, 120, 119, 115, 117);
  $self->make_row(670, 200, 111);
  $self->make_row(630, 240, 113, 116, 114);

  # number pad
  $self->make_row(760, 80, 77, 106, 63, 82);
  $self->make_row(760, 120, 79 .. 81, [ 86,  1, 2 ]);
  $self->make_row(760, 160, 83 .. 85);
  $self->make_row(760, 200, 87 .. 89, [ 104, 1, 2 ]);
  $self->make_row(760, 240, [ 90, 2 ], 91);
}

sub xmlify {
  my ($self, @args) = @_;

  return $self->svg->xmlify(@args);
}

package main;

use strict;
use warnings;
use autodie;

my $INPUT  = "$ENV{HOME}/Documents/keylog.txt";
my $OUTPUT = "$ENV{HOME}/Documents/keyfreq.svg";

open my $output, '>', $OUTPUT;
open my $input, '<', $INPUT;

my $map = KeyMap->new();
while (<$input>) {
  $map->add_key_press($1) if /^key release\s+(\d+)\s+$/;
}

close $input;

$map->render;
print $output $map->xmlify;

close $output;

This script reads the keylog and creates a "heat map" of which keys I actually pressed. Here is the generated image:

keyboard heat map

Update: I have regenerated the map with a few days of data to give a better view of my usage.

Some notes before interpreting the data:

I have my caps lock mapped to super (windows key) because it is the modifier I use for my window manager functions. I do not actually turn caps lock on and off that frequently.

I have my escape and tilde (left of number 1) keys swapped. I'm not sure I like this just yet as I type ~ more often than I had realized.

I only had the keylogger running while I was doing work-type things. I boot into Windows if I am going to play games and I did not log any data during those times although I'm not sure the differences would be significant.

All in all I think this really shows how little keys are used outside of the 60% layout area (at least by me during the time of this experiment). In any case, this has strengthened my resolve to acquire a 60% keyboard.