This post is a summary of the tools that I'm fiddling with to tweak my keyboard layout every now and then. I recently changed from Dvorak to the less standard Workman Layout with a bit more exotic features that led me to look into options for implementing and customizing the layout.


For my Macbook, new layouts can be added by creating a bundle in ~/Library/Keyboard Layouts. There is a Workman bundle with several layout variations available at deekayen/workman. I'm using a variation of Workman Dead, which trades the number of key presses against the distance traveled. Additionally, you don't have to press a modifier key to get to the symbols. Symbols that are usually available via the number row, are accessible on home row after pressing the dead key. Pressing the dead key, in my case the comma key, will remap several keys. It changes the layout from normal Workman:

workman layer.

to a more symbol focused one:

symbol layer.

For example, the key { is accessible by pressing ,s in succession, rather than shift + [, the key ( is ,h rather than shift + 9 and so on.

The symbol layer is different from the one included in the bundle at deekayen/workman, more customized to my habits. For example, rather than splitting parentheses across both hands, I moved them to the left hand. This means that the right hand can stay on top of the dead key in case I want to insert a pair. I also traded numbers for more symbols and added ~ close to the dead key so that I can type ~/ conveniently in a rolling motion.

The customization is quite straight forward with Ukelele. You can open the main bundle, select the layout you want to modify and enter the symbol layer to modify just that part of it. Ukelele then updates the file that describes the layout (*.keylayout in Workman.bundle/Contents/Resources). Much more convenient than editing the XML by hand :)

For some specific key remapping on my Macbook I use Karabiner and Seil, rather than the layout. For example, capslock and return are both mapped to control when pressed in combination with another key, otherwise to their original meaning. The combination of Karabiner and Seil allows all sorts of remappings. For example, in the firmware version I mapped backslash to the capslock key, as I don't really use capslock. I use Seil to map capslock to backslash and Karabiner to map backslash to control when pressed in combination with another key.

You can do this for any other key combo: Identify the key's code via: Karabiner > Misc & Uninstall > Launch EventViewer and use Seil to map the capslock key to that code. Then add your own configuration to Karabiner, similar to the following:

  <?xml version="1.0"?>
      <name>Change backslash to left control key.</name>
      <appendix>(Send an backslash key event when backslash key is pressed alone.)</appendix>

        <!-- from -->
        <!-- to -->
        <!-- alone -->

The software support that OSX offers is quite convenient and switching between layouts is fast, in case someone else needs to type. But there are some shortcomings:

  • OSX defaults back to QWERTY when requesting an admin password or when logging out.
  • There are some issues with the symbol layers when using sites like keybr.com or typing.io for practicing. Not sure where the key presses are lost or whether they are translated incorrectly.
  • When pairing with other developers, I fall back to Qwerty as Workman is still quite niche and few systems support it out of the box.


Hardware to the rescue! I bought an ErgoDox via Massdrop last year and this seemed like the perfect opportunity to learn about its firmware. Compiling my own firmware version addresses the above issues by "escaping" OSX and allowing me to just plug in a keyboard with Workman installed. There are multiple firmware implementations available and I just customized one to fit my needs. This means custom placement of modifier keys and also adding the dead key layer. I'm using Massdrop's configurator for the ErgoDox to get a visual representation of my setup:

ergodox layer 0.

This is just the first layer, the next layer is for symbols and the third is for a numpad on the right hand near home row, the full configuration is available here.

The configurator allows you to compile your own firmware version as well, but currently there is no support for the dead key approach that I'm using. But luckily benblazak/ergodox-firmware has support for sticky keys, which you can use to implement the dead key approach. The project is well documented and it's quite straight-forward to compile your own version on a Mac. The only external dependency I had to download was the compiler as part of the AVR MacPack. Change the layout in src/makefile-options to your target and you're good to go! Simply issue a make in the src sub-folder and then load the resulting firmware.hex with a Teensy Loader onto your ErgoDox.

To implement a Workman Dead version, I used the existing Colemak layout that makes use of the sticky keys functionality and adapted the keys to Workman. The layout definition is split across three function invocations, where each one looks similar to the following:

  const uint8_t PROGMEM _kb_layout[KB_LAYERS][KB_ROWS][KB_COLUMNS] = {
  // LAYER 0
      // unused
      // left hand
      _esc,       _1, _2, _3, _4, _5, _grave,
      0,          _Q, _D, _R, _W, _B, _tab,
      _backslash, _A, _S, _H, _T, _G,
      _shiftL,    _Z, _X, _M, _C, _V, _guiL,
      0,          0,  0,  0,  _esc,
      // left thumb block
           0, 0,
      0,   0, _altL,
      _bs, 2, _ctrlL,
      // right hand
      _equal, _6, _7, _8, _9,      _0,         _esc,
      _dash,  _J, _F, _U, _P,      _semicolon, 0,
              _Y, _N, _E, _O,      _I,         _quote,
      _guiR,  _K, _L, 1,  _period, _slash,     _shiftR,
              _arrowU, _arrowD, _arrowL, _arrowR, 0,
      // right thumb block
      0,      0,
      _altR,  0,      0,
      _ctrlR, _enter, _space

The above snippet is the definition for the first layer and defines the basic Workman layout. The 0's indicate an unused key while the ones with an underscore reference a given key code. For example, _9 refers to the key code that a regular USB keyboard emits when you press the 9 key (I'm using the short name, the full name is more descriptive: KEY_9_LeftParenthesis).

To implement the symbol layer, I use the comma key as a sticky key, wish activates a second layer (for one key press). The definition of the sticky keys is straight-forward: You indicate the layer number on the normal layout (the 1 in comma position and 2 on the left thumb block in the basic layout above).

In addition to the call to _kb_layout, you manage the specific behavior for press & release by passing mappings to _kb_layout_press and _kb_layout_release. These mappings are analogous to the one passed to _kb_layout, but instead of key codes you add references to functions. For the activation of the symbol layer, you add references to lsticky1 or lsticky2 in the press & and release mappings.

Not all symbols are accessible without modifiers on a regular keyboard, but one goal of the dead key approach is to get rid of the modifier. For this to work, you can supply a modifier-specific function in the press & release mapping. For example, this is just the layout for the left hand for the symbol layer:

  0,  0,          0,          0,          0,          0,      0,
  0,  _bracketR,  _bracketR,  _0,         _add_kp,    _2,     0,
  0,  _bracketL,  _bracketL,  _9,         _equal_kp,  _5,
  0,  _comma,     _period,    _backslash, _dash,      _dash,  0,
  0,  0,          0,          0,          0,

To produce {} rather than [] in the third column, the press & release mappings contain calls to kbfun_shift_press_release (aka sshprre) rather than the normal kbfun_press_release (aka kprrel):

 ktrans,        ktrans,         ktrans,         ktrans,         ktrans,         ktrans,         ktrans,
 ktrans,        kprrel,         sshprre,        sshprre,        sshprre,        sshprre,        ktrans,
 ktrans,        kprrel,         sshprre,        sshprre,        sshprre,        sshprre,
 ktrans,        sshprre,        sshprre,        sshprre,        kprrel,         sshprre,        ktrans,
 ktrans,        ktrans,         ktrans,         ktrans,         ktrans,

The tedious bit is to keep the corresponding calls to _kb_layout_press and _kb_layout_release for each layer in sync. Otherwise you might see modifier keys that remain pressed for no apparent reason. For example, if the press & release mappings contain different function references, one to kprrel and the other to sshprre, then the shift modifier would not get released properly. Keeping the different invocations in sync is a bit tedious as all information is passed in a single call where position defines the meaning of a value and you only get arity compiler warnings. But your favorite editor might be able to help you with that ;)

For reference my full layout is available here.