Optimizing Your Keyboard Layout
28 Oct 2017 —I’ve recently gotten into the fabulous world of mechanical keyboards. Other than the Aesthetics and the fact that building working electronics is cool, the main draw for me was the ability to completely customize how your board works. Just imagine all the chances for increased productivity!
Of course, good cognitive scientist as I am, I know that optimal performance requires a good grip of the statistics of the environment you’re working in. For us, that’s going to be our typing patterns! Here, I’ll show you how I was able to capture a huge amount of data on how I type, how to play around with it, and how to do the same for your own keyboard.
My set up
This tutorial/demonstration is for those who know something about mechanical keyboards, and just a little bit about the QMK firmware that powers most custom boards. I’m relatively new to QMK myself, but I’m trying to make things a little bit easier for the next guy. However, I’m not a developer for this project, and my free time is limited, so this will be specific to my experience and setup, etc.
After suffering through the hell that was the Massdrop group buy for Jack Humbert’s Planck mechanical keyboard1, I’ve finally got a fully programmable keyboard of my own. This guide will be for that rev4 Planck. I use an old Macbook Pro with OSX 10.9.5 (I really need to get around to updating!) and R
version 3.3.1 (more shame!).
I can tell you right now, following this tutorial on a PC is going to be challenging. Making and flashing custom QMK layouts to your keyboard is way easier on Unix-based systems, and most of the cool open-source stuff is for Unix as well. Now is the perfect time to get in on that Linux action if you haven’t already. Installing a virtual machine on your PC is a commitment-free way of playing around with Linux, and I’d recommend that.
Getting your typing data
Alright folks, this here’s the secret I learned: download selfspy. Selfspy is basically spyware you use on yourself–it collects enormous amounts of information on your activity on your computer. It records every keystroke, every name of every window you open, and your mouse movements as well. Unlike spyware, it doesn’t send this data to anybody else, it just saves all of it in an Sqlite database on your computer.
Installing selfspy
Heads up: this might not that be easy to do. I kinda blacked out from the stress of trying to get it installed right, so my memory about what I did to get it to work is a little hazy. Follow the instructions on the Github page as well as you can, but IIRC I had to install the exact versions of some of the stuff listed in the osx_requirements.txt
file. In particular, I think I had to make sure I was using the 3.0.4 version of pyobjc
in the end (I used the pip
installer).
Looking back, you might be able to just update the pyobjc requirements in osx_requirements.txt
to whatever you have currently, but I’m not positive about that.
Analyzing your data
Open up a terminal, and run selfspy
to have it start collecting your data. A word to the wise: unless you sometimes pause it, it will record EVERYTHING. All your passwords, the names of all those dirty, dirty sites you visit (you filthy animal!)–everything.
This isn’t a such a big deal, however, because you can have it encrypt the data it stores in the database on your computer. If you’re fine with somewhat surface-level analyses of your data, this is fine–selfspy
comes with a program called selfstats
that lets you print out some basic analyses of your data, including key frequencies. If you want to see how often you’ve pressed each key while selfspy
has been running, you can do it with selfstats
easy-peasy.
But if you want to manipulate all that juicy, succulent data yourself (you little pervert!), the fact that it’s all encrypted is a bit of problem. At least it was for me, who doesn’t know how to decrypt it with the tools I had on hand. Instead, I opted to just leave everything unencrypted and pray to God that the Russians don’t get a hold of my computer2. The R
code that I’ve written to analyze the data at a more fine-grain level only works when it’s unecrypted, so either figure out how to decrypt Blowfish in R
or Python
, or let your freak flag fly.
Walking through my code
I do a lot of R
coding and use Hadley Wickham’s beautiful R
babies all the time. One of his packages, dplyr
, has3 the capability of manipulating SQL data, so that’s what I use to access the data stored in ~/.selfspy/selfspy.sqlite
. I’ll run you through some of it so you can get a sense of how it works. You can download it for yourself in the link at the end of the post–it comes with (hopefully) helpful comments and multiple examples for you to acquaint yourself with different possibilities.
You can see that there are five tables in the database: click
(mouse data?), geometry
(window size data?), keys
(key press data), process
(the names of the programs), window
(data on what windows have been active). For this excursion, we’ll only care about keys
and process
.
Note: For this demonstration, I’m using a smaller copy of one of my databases. This is because: 1) I accidentally locked myself out of my month-long database and 2) I don’t want any weird stuff being coming up in this tutorial!
See the Source: query [?? x 10]
at the top? As it currently stands, key_db
isn’t a data frame or a tbl_df
–it’s just a query to the database–it doesn’t know how many rows there are yet. When dealing with SQL, dplyr
evaluates things lazily, meaning it won’t actually fetch all the data from the database unless you demand it.
You can see that there’s a lot of info in key_db
: in addition to timing information for the key presses, it has the IDs of all the applications and windows that were active while you were typing. Today, I’ll only be focusing on the key presses and the names of the applications.
Let’s look at some of the functions I’ve written to help you analyze your data.
Getting the key presses in human-readable formats
In order analyze the key presses, we have to get them to a point where we can work with them. For this, I’ve written the function getPresses()
. There are a lot of optional arguments, but they’re detailed in the code. The basic functionality is like this:
For those of you not used to R
and those R
users not used to the tidyverse
, the %>%
operator pipes the output of everything on the left of it (getPresses(key_db)
) into the function on the right of it as the first argument (or, wherever you put a .
). Thus, what is above is equivalent to select(getPresses(key_db), cleanStrings)
. My irrational commitment to never declare new variables might make some of the code seem a little weird to some of you–each function is basically a single “flow”.
Understanding the output: basic
The output from getPresses()
is essentially a table where every row is a single key press (kind of). The keypresses are saved as strings with the “cleaned up” version of each press in the cleanStrings
column. A row with “a” in this column means you typed that character–unfortunately, selfspy
includes the presence of the Shift key in this value, so “A” actually means Shift + 'a'
. For modifier combos and certain other key presses, such as Command + Tab
or Backspace
, the key press is saved in brackets like <[Cmd: Tab]>
. Technically speaking, these probably aren’t “key presses”–selfspy
doesn’t record pressing the Command, Alt, or Ctrl keys unless they are pressed along with some other key.
By itself, the output of getPresses
is pretty useless. If we want to analyze the data, maybe we’d want to start looking at which keys are pressed the most often, or what shortcuts are used the most. That way, we could decide how accessible each key should be on our keyboard, or what shortcut macros might help the most.
To get a sense of this, I’ve created a function called getStats()
:
You can see from the output that there are three columns: the names of the key presses, areModsPressed
(a column indicating whether functional/modifier keys were pressed down, excluding Shift), and n
(the number of presses).
It seems that so far, I press “Down” and “Backspace” the most, followed by “Space” (annoyingly represented as actual whitespace) and then some really common letters. (You can show all the rows by piping the output into as.data.frame()
.) Not super surprising. But we set the break_multitaps
to TRUE
–what was up with that?
Well, selfspy
automatically records repeated “bracketed” key presses with a single entry. So 15 Command + Tab
s in a row will be recorded as <[Command + Tab]x15>
. By default, getStats()
treats this as a single key press. This is useful if you want to consider making macros for, say, something that presses Backspace three times in a row. Note that only “bracketed” key presses are recorded this way. By setting break_multitaps=TRUE
, we treat these repeated presses individually.
Understanding the output: intermediate
But what if you’re not satisfied with something that you could basically do with the selfstats
command? For example, what if you wanted to look at how you typed differently in different applications (perhaps for creating a Python
layer or an R
layer), or if you wanted to look at multi-press patterns?
I’ve made getPresses()
capable of separating key presses by application name, so that they can be analyzed independently in parallel. Here we use our old friend process_db
from earlier. Because the key_db
only has numbers for each process, if we want to see the names those numbers refer to, we have to pass in what is essentially a number-to-name dictionary (process_db
, after we call collect()
on it).
A marvel of the tidyverse
is that it can store entire data frames as single cells in bigger data frames. Each cell in the data
column above is a data frame of all the key presses for the application in the process_id
column.
We can analyze each of these data frames the same as we would a single data frame with Hadley Wickham’s purrr
package. Having started my programming journey in Scheme
, I love me some functional programming. The map
function in purrr
lets us apply the getStats
function to the inside of each data frame in the data
column.
Yeah, most of my typing is typing English words, separated by spaces, so you can see similar statistics for the top key presses across applications.
There are differences, however. I threw together this quick-and-dirty little visualization that compares the highest key presses across applications.
It’s a pretty boring plot for the most part (sue me!), but you can see some differences emerging across applications. I press the Enter
key a lot less in Microsoft Word, than I do when I’m coding in R
, or browsing the web. Presumably, that’s because I type in paragraphs in Word. If I want to optimize my key layout, I might be ok with making a layer for Word where the Enter
key is less accessible.
I urge you to really play around with your data at this point. Really get in there and see what’s up. You can do a lot of comparisons between applications, visualize it, etc.–I’ve really only shown the most boring possibilities above. The differences get much more interesting once you get past the alpha keys.
Understanding the output: advanced
What would a rational keyboard layout look like? I’m tempted to start prattling on about what that could be, bringing in concepts from information theory or psycholinguistics, but to spare you my philosophizing, I’ll just settle on a super basic definition: a rational keyboard layout is one that minimizes the number of necessary key strokes for the statistics of the output and the constraints of the board4.
Ugh, it’s too hard to stop myself from going into lecture mode, so I’ll just say this–if you type the same multi-key patterns again and again, why not reduce them to fewer key presses? For example, I type the %>%
operator a lot in R
–maybe I want to make this a macro that I can press with a single button? Well, if you want to start looking at key press n-grams, I wrote some rudimentary code for that as well!
The function nGramPresses()
basically does the same thing as getPresses()
, but it clusters subsequent key-presses into groups of n. Let’s look at what happens when we analyze the frequency of 4-gram key presses that use modifier keys, grouping them by application.
Look at that! Now we’re getting somewhere! You can get a sense of what I’ve been doing the most from these n-grams! I’ve been using git
a lot (e.g., <[Enter]>-g-i-t
), navigating with the terminal (with ls
and cd
), and yes, it looks like I do use the %>%
operator (followed by a return) a lot! Hilariously, you can also see my reddit obsession: I just type r-e-d
to get the autocomplete suggestions, and then press Enter
!
Other layouts
Hopefully, I’ll be getting around to writing a newbie’s introduction to QMK, but for those of you with no C++ experience, check out kbfirmware.com for an easy way to make basic layers and layouts. For those who are just starting out on their QMK journey, my advice is to copy the hell out of other people’s code and layouts–there are loads of examples for most keyboards in the firmware!
There are also great ideas to draw inspiration from. For example, my current layout (which you can follow along with here) draws a lot of inspiration from Noah Frederick’s set up. This person also has a bunch of crazy layouts and code that beginners might find helpful.
If this helps at all, or if you have any questions (please don’t be about installing selfspy please don’t be about installing selfspy), feel free to drop a comment here or on reddit!
Source Code:
The code that I banged out for this project. Although it’s using some outdated package versions, I added a lot of comments and usage examples.
The R Markdown file this blog post is generated from, if you want to know what R code I used for the plot, etc.
Footnotes
-
None of the trainwreck was Jack’s fault–Massdrop is just a horrible company. ↩
-
You can leave your data unencrypted with:
selfspy -p ""
. I also learned that you can completely screw up your database by trying to change the password, so my advice is to pick if you want it encrypted or not and stay with that decision. ↩ -
Technically it “had” the ability. The latest release of
dplyr
has separated this functionality into thedbplyr
package. I haven’t yet made the leap to the newestdplyr
though (even though it’s way better), and my code reflects that. The code I wrote should be similar to what you’d use if you use the new package though. ↩ -
Hhhnnnngggh, that’s totally wrong by the way, and way too simple to be interesting, but if I start thinking about it anymore I won’t be able to
quell my raging science bonerget any work done. I actually think an optimal keyboard layout would probably end up looking something like a stenography board for most users, but the specificity needed for activities like coding would make the trade-off with speed interesting. ↩