Skip to content

Instantly share code, notes, and snippets.

@gsinclair
Last active October 7, 2024 16:14
Show Gist options
  • Save gsinclair/f4ab34da53034374eb6164698a0a8ace to your computer and use it in GitHub Desktop.
Save gsinclair/f4ab34da53034374eb6164698a0a8ace to your computer and use it in GitHub Desktop.

Karabiner layouts for symbols and navigation

Gavin Sinclair, January 2022

Introduction

I use Karabiner (configured with Gosu) to make advanced key mappings on my Apple computer. Karabiner allows you to create “layers”, perhaps simulating those on a programmable mechanical keyboard. I make good use of these layers to give me easy access (home-row or nearby) to all symbols and navigational controls, and even a numpad.

The motivation is to keep hand movement to a minimum. Decades of coding on standard keyboards has unfortunately left me with hand and wrist pain. I will soon enough own a small split keyboard which will force me to use layers to access symbols etc., so this Karabiner solution, which has evolved over months, is a training run for that.

What is a layer? A simple example is that when I hold down f I get access to nearly all delimeter pairs: ( ) [ ] { }. The key idea is that I activate a layer with my left hand and select the character with my right hand.

Note that this is all taking place on a QWERTY keyboard, but it’s the key positions that are important, not the letters.

This document exists to share the idea and the details for others who are interested. Share at will.

An overview of the layers

My layer keys are all easily accessible with the left hand:

    w   e   r
a   s   d   f
        c   v

The selection keys are easily accessible with the right hand, in most cases using only the main three fingers:

     u   i   o
(h)  j   k   l
     m   ,   .

Navigation and editing

The a layer provides arrow movement with hjkl. It also gives access to Tab, Enter and Page Down/Up. This ASCII diagram attempts to demonstrate the a layer.

   U  I                     Tab    Tab
H  J  K  L            Left  Down    Up   Right
   M  ,  .                  Enter  P-Dn  P-Up

Often, though, we want to move a word at a time, or to the beginning or end of the line. The r-e-w layers provide range of motion for three operations:

  • delete (r)
  • move (e)
  • select (w)

The target keys here are laid out below to emphasise their position:

      U   I                      <   >
  H   J   K   L           <<-   <-   ->   ->>

Just to be clear, this is the meaning of the symbols above:

  <    left one character
 <-    left one word
<<-    beginning of line

So with the correct combination of left-hand and right-hand I can achieve any normal motion, selection or deletion. This entirely replaces Option-Left and Command-Right and Shift-Option-Left, etc.

For me, the most common operations are delete character left and delete word left. Moving left and right a word at a time is also common. Moving or deleting to the beginning or ending of the line are somewhat common. All of these are easily done with my layer design and I much prefer not having to press Command or Option to achieve this simple things, and I definitely do not want to move my right hand to an arrow cluster for this sort of thing ever again!

Selecting text is something I don’t find myself doing often, but it is easy to do because it follows the same right-hand logic as the other operations (moving and deleting).

Symbols and numbers

To bring numbers to the home row I wanted to make a number pad. To bring all symbols to the home row several layers are required. To have any hope of memorising them all some thought is needed to logical groupings. I’m reasonably happy with the choices I’ve settled on, but other people could make equally valid decisions.

Broadly speaking the layers are designed as follows, in descending order of left-hand friendliness

  • delimiters (f) [plus a few other symbols]
  • arithmetic symbols (d)
  • punctuation symbols (s)
  • number pad (v)
  • remaining symbols (c)

In every case priority is given to right-hand comfort by using the keys:

U  I  O
J  K  L
M  ,  .

Delimiters (f)

Access to delimiters is super important for a programmer, so I placed this under the most convenient key (f).

{   }   !
(   )   ?
[   ]   $

For a long time there were only six symbols in this layout, but ultimately the logic of making more symbols accessible through the convenient f key won out. Initially I used it for “leftover” symbols, but eventually I realised that having frequently-used symbols here made the most sense.

Arithmetic symbols (d)

The hash sign snuck in here because one meaning of it is “number”.

<   >   #
+   -   =
*   /   %

Punctuation symbols (s)

Quotes are on the top row because they are typographically up high. Comma and period are on the best two fingers because they are so common. Semicolon and colon line up nicely with comma and period. Backtick is a quote, of sorts, so it goes with the other quotes. Ampersand and tilde have no logical reason to be where they are specifically, but ampersand is reasonably common so it’s good to be in a comfortable position.

'   "   `
,   .   &
;   :   ~

Number pad (v)

The main nine positions here are naturally the numbers 1–9, but a number pad also needs to contain zero and period, the places for which I settled on after som experimentation. It is also handy to have hyphen and backspace, and I included plus and Enter because a normal number pad has them.

BS   7   8   9   +
 .   4   5   6   -
 0   1   2   3   Enter

Remaining symbols (c)

Note the way that three of these symbols form a visually logical layout, which helps greatly in remembering them.

^
|   @
\   _

Experience report

This layout has evolved over time until quite recently, and so muscle memory for the various symbols does vary quite a lot. I am fluent with navigation, delimiters, and to some extent arithmetic. I have to think about punctuation, unfortunately. Number pad usage is OK, and will be better when I have a column-aligned keyboard in the future.

I have a handwritten aide-memoire on hand at all times to help me find the right key. And I am thinking of writing a simple command-line app that gives typing practice with symbols, introducing a couple at a time or something.

The biggest difficulty with all this is that the layers are software layers and are very susceptible to mis-timings. That is, I frequently get (say) dl on the screen instead of =. This is rather annoying, to say the least, and I hope that when I have a programmable keyboard in future (with hopefully a better implementation of layers) then this situation will be improved. The sad truth is that you can’t just hold down a layer key and take your time selecting the right-hand key. Holding down f for too long (half a second, say), will simply dump an f on the screen. There are configurations you can make with Karabiner, but frankly I don’t understand them.

But despite this, it is worth it. My typing is pitifully slow and cumbersome (when symbols are involved) and that is very frustrating, but my hands are more comfortable, and I know that the overall situation will improve over time.

The configuration

Karabiner uses a JSON file for its configuration, but that is extremely unwieldy. Goku (brew install yqrashawn/goku/goku) is a far better option: it uses a file format called EDN, and when you run goku it converts that to JSON for you. You can look at basic examples elsewhere. My configuration is below.

{

 :devices {
           :sculpt-keyboard [{:product_id 1957 :vendor_id 1118}]
           }

 :simlayers {
             :f-mode {:key :f}    ; delimeters    ( ) [ ] { } and other symbols ~ $ &
             :d-mode {:key :d}    ; arithmetic    + - * / = % < > #
             :s-mode {:key :s}    ; punctuation   ? ! : ; ' " ` ~
             :a-mode {:key :a}    ; navigation hjkl + tab + enter + page down/up
             ;
             :q-mode {:key :q}    ; General shortcuts (browser etc.) - not settled
             :w-mode {:key :w}    ; Selection left and right (letter, word, line)
             :e-mode {:key :e}    ; Movement left and right (letter, word, line)
             :r-mode {:key :r}    ; Deletion left and right (letter, word, line)
             ;
             :g-mode {:key :g}    ; Mouse scroll, desktop left-right, zoom in-out, screenshot (not implemented)
             ;
             :v-mode {:key :v}    ; Number pad with + - BS ENTER as well
             :c-mode {:key :c}    ; Slashes and lines  ^ | \ _ @
             :x-mode {:key :x}    ; Some multi-character shortcuts like <= (not implemented)
             }

 :main [

        {:des "Swap Win and Alt on Sculpt keyboard"
         :rules [:sculpt-keyboard
                 [:left_option :left_command]
                 [:left_command :left_option]
                 [:right_option :right_command]
                 [:application :right_option]
                 ]
         }

        {:des "CAPSLOCK is CTRL if pressed in combination, otherwise ESC"
         :rules  [
            [:##caps_lock        :left_control     nil         {:alone :escape}]
          ]}

        {:des "f-mode for delimeters and ! ? $"
         :rules [:f-mode
                 ;; u i j k m comma -> !Sopen_bracket !Sclose_bracket !S9 !S0 open_bracket close_bracket
                 [:##u :!Sopen_bracket]
                 [:##i :!Sclose_bracket]
                 [:##j :!S9]
                 [:##k :!S0]
                 [:##m :open_bracket]
                 [:##comma :close_bracket]
                 ;; o l period -> !S1 !Sslash !S4
                 [:##o :!S1]
                 [:##l :!Sslash]
                 [:##period :!S4]
                ]
         }

        {:des "d-mode for arithmetic"    ;;    < > #    + - =    * / %
         :rules [:d-mode
                  [:##u     :!Scomma]               ; d -> o        <
                  [:##i    :!Speriod]               ; d -> p        >
                  [:##o         :!S3]               ; d -> o        #

                  [:##j         :!Sequal_sign]      ; d -> j        +
                  [:##k         :hyphen]            ; d -> k        -
                  [:##l         :equal_sign]        ; d -> l        =

                  [:##m :!S8]                       ; d -> m        *
                  [:##comma :slash]                 ; d -> ,        /
                  [:##period :!S5]                  ; d -> .        %
                ]
         }

        {:des "s-mode for punctuation"   ;;    ' " `    , . &    ; : ~
         :rules [:s-mode
                 [:##u :quote]
                 [:##i :!Squote]
                 [:##o :grave_accent_and_tilde]
                 [:##j :comma]
                 [:##k :period]
                 [:##l :!S7]
                 [:##m :semicolon]
                 [:##comma :!Ssemicolon]
                 [:##period :!Sgrave_accent_and_tilde]
                ]
         }

        {:des "a-mode for hjkl movement and nm enter and ui tab and ,. PageDn/Up"
         :rules [:a-mode
                  [:##h :left_arrow]
                  [:##j :down_arrow]
                  [:##k :up_arrow]
                  [:##l :right_arrow]
                  [:##n :return_or_enter]
                  [:##m :return_or_enter]
                  [:##u :tab]
                  [:##i :tab]
                  [:comma :page_down]
                  [:period :page_up]
                ]
         }

        {:des "r-mode for deleting characters with ui, words with jk and lines with hl"
         :rules [:r-mode
                  [:##u :delete_or_backspace]   ; r -> j   Delete word backwards
                  [:##i :delete_forward]        ; r -> j   Delete word backwards
                  [:##j :!Odelete_or_backspace] ; r -> j   Delete word backwards
                  [:##k :!Odelete_forward]      ; r -> k   Delete word forwards
                  [:##h :!Cdelete_or_backspace] ; r -> h   Delete to beginning of line
                  [:##l :!Cdelete_forward]      ; r -> l   Delete to end of line
                ]
         }

        {:des "e-mode allows for easy back and forth one character, word or line"
         :rules [:e-mode
                  [:##u         :left_arrow]          ; e -> u    Left
                  [:##i         :right_arrow]         ; e -> i    Right
                  [:##j         :!Oleft_arrow]        ; e -> j    Opt+Left
                  [:##k         :!Oright_arrow]       ; e -> k    Opt+Right
                  [:##h         :!Cleft_arrow]        ; e -> h    Cmd+Left
                  [:##l         :!Cright_arrow]       ; e -> l    Cmd+Right
                  [:n           :return_or_enter]     ; e -> n    Enter
                  [:m           :return_or_enter]     ; e -> m    Enter
                ]
         }

        {:des "w-mode = e-mode + SHIFT (i.e. selection, not just movement)"
         :rules [:w-mode
                  [:##u         :!Sleft_arrow]         ; e -> u    Shift+Left
                  [:##i         :!Sright_arrow]        ; e -> i    Shift+Right
                  [:##j         :!SOleft_arrow]        ; e -> j    Shift+Opt+Left
                  [:##k         :!SOright_arrow]       ; e -> k    Shift+Opt+Right
                  [:##h         :!SCleft_arrow]        ; e -> h    Shift+Cmd+Left
                  [:##l         :!SCright_arrow]       ; e -> l    Shift+Cmd+Right
                ]
         }

        {:des "q-mode for general shortcuts like browser tab navigation"
         :rules [:q-mode
                  [:##j :!CSopen_bracket]  ; q -> j    tab to the left:  Cmd-{
                  [:##k :!CSclose_bracket] ; q -> k    tab to the right: Cmd-}
                  [:##l :!TCf           ]  ; q -> l    toggle full screen: ^⌘F
                  [:##u :!Cclose_bracket]  ; q -> u    browser back:     Cmd-[
                  [:##i :!Cclose_bracket]  ; q -> i    browser forward:  Cmd-]
                  [:##o :f2             ]  ; q -> o    F2 (useful in Excel)
                  [:##p :f4             ]  ; q -> p    F4 (useful in Excel)
                ]
         }

        {:des "v-mode for number pad"
         :rules [:v-mode
                 [:u :7]
                 [:i :8]
                 [:o :9]
                 [:j :4]
                 [:k :5]
                 [:l :6]
                 [:m :1]
                 [:comma :2]
                 [:period :3]
                 [:p :!Sequal_sign]
                 [:semicolon :hyphen]
                 [:slash :return_or_enter]
                 [:y :delete_or_backspace]
                 [:h :period]
                 [:n :0]
                ]
        }

        {:des "c-mode for remaining symbols ^ | \\ _ @"
         :rules [:c-mode
                 [:##u :!S6]
                 [:##j :!Sbackslash]
                 [:##k :!S2]
                 [:##m :backslash]
                 [:##comma :!Shyphen]
                ]
        }

        #_{:des "x-mode for some programming pairs like <= (not yet implemented)"
         :rules [:x-mode
                ]
        }

        #_{:des "g-mode for mouse scroll, desktop left-right, zoom in-out, screenshot"
         :rules [:g-mode
                ]
        }

        {:des "Forward slash is an easier right-shift (if combined)"
         :rules  [
            [:slash        :left_shift     nil         {:alone :slash}]
          ]}

        ;; Using keys for CTRL etc (home-row-mods) isn't practical with plain Karabiner.
        ;; Some changes to timeout settings would be required, and the documentation is 
        ;; not clear enough.
        #_{:des "Convenient CTRL (T,Y) and COMMAND (G,H)"
         :rules  [
            [:##t        :left_control     nil         {:alone :t}]
            [:##y        :left_control     nil         {:alone :y}]
            [:##g        :left_command     nil         {:alone :g}]
            [:##h        :left_command     nil         {:alone :h}]
          ]}

 ]
}

@yarishraman
Copy link

This is pure Gold! I am happy today as if I had found the treasure of my life.
As a programmer, I appreciate your hard work and enthusiasm for productivity.

I am also in hunting for a mechanical keyboard (Corn-ish Zen, Cantor) , kindly update your choise as well.

@velios
Copy link

velios commented Nov 24, 2022

Why in "Swap Win and Alt on Sculpt keyboard" you don't use
[:##left_option :##left_command] instead of [:left_option :left_command].
Does this decision have any special meaning? It seems to me that the current solution closes the possibility of using abbreviations such as Option + smth + key

@bastianwegge
Copy link

This has saved a lot of time on my end! Thank you very much!

@jfhector
Copy link

Thanks! This is really helpful.

I found the following quote interesting, and I have a question about it:

I frequently get (say) dl on the screen instead of =. This is rather annoying, to say the least, and I hope that when I have a programmable keyboard in future (with hopefully a better implementation of layers) then this situation will be improved.

Why do you think that a QMK (or similar)-based layer config using home row keys would not have that problem?

My understanding is that many (maybe most?) people using mechanical keyboards with QMK or ZMK have the same issues you describe, and that it can be improved with tweaking the timings a lot, but it stays difficult.

I haven’t done it myself though, and so if you think differently it’d help me to know what I don’t know.

thanks!

@bastianwegge
Copy link

@jfhector the problem with this is, that you're bound to the threshold start-end of the :sim-layer. Say you use d (D) and then l (L), every time you type a word with dl (i.e. rapidly) there's a chance you get the exact timing to hit your sim-layer. It's configurable though using:

:simlayer-threshold 500
:profiles {
    :Default { :default true
        :sim     50     ;; simultaneous_threshold_milliseconds (def: 50)
                        ;; keys need to be pressed within this threshold to be considered simultaneous
        :delay   300    ;; to_delayed_action_delay_milliseconds (def: 500)
                        ;; basically it means time after which the key press is count delayed
        :alone   300    ;; to_if_alone_timeout_milliseconds (def: 1000)
                        ;; hold for 995s and single tap registered; hold for 1005s and seen as modifier
        :held    500    ;; to_if_held_down_threshold_milliseconds (def: 500)
                        ;; key is fired twice when 500 ms is elapsed (otherwise seen as a hold command)
    }
}

This is taken from https://github.com/tIsGoud/goku/blob/master/karabiner.edn

In QMK you have things like IGNORE_MOD_TAP_INTERRUPT, read about it here: https://precondition.github.io/home-row-mods#ignore-mod-tap-interrupt. This greatly decreases your chances of hitting a home-row mod when rolling (pressing multiple keys at once). BUT in the end, QMK also has the same threshold start-end which you can (and should) play around with. If you follow the article above, you will find mentions about it and even tipps on how to find the sweet-spot.

Hope this helps 👋

@jfhector
Copy link

Thanks!

@jfhector
Copy link

jfhector commented Apr 17, 2023

Here's an idea:

(Caveat: I'm new to this, and I've only been using your config / simlayers for a few minutes -- not weeks like you have -- so I don't yet know what it's like day to day. But I'm sharing this thought it case it helps).

1. It seems like the main cause of issues is not pressing the layered key quickly enough

If I understand the reason why you (and I, yes) end up with (say) "dl" printed instead of "=", is that:
a. l (L) wasn't pressed quickly enough after d (D)
b. Or l (L) was pressed before d (D) rather than after

I imagine that a. is responsible for most of the issues, as b. seems easier to avoid.

2. Why wasn't l (L) pressed quickly enough after d (D)?

My hypotheses are that:

  1. l (L) wasn't pressed quickly enough because we sometimes hesitate a little bit too long.
  2. Reduce the cost of pressing the wrong key (instead of L) will reduce that hesitation time.

If I press another key that's defined as part of your d-mode, then I get a different arithmetic character printed. That's not so bad because I can just keep d (D) pressed while deleting my mistake and attempt press l (L) again.

The main problem is when, while holding d (D), I press a key that's not defined in your d-mode (e.g. H, Y or N). For example if I after pressing d (D) I accidentally press h (H) instead of l (L), then:

  • "dl", which takes me at least 2 strokes to delete
  • I'm not in d-mode any more, meaning that I need to release and press d again

3. Here's the idea: defining more right-hand keys in each simlayer, making those that are not useful inactive

If I define other right-hand keys as part of your d-mode (in priority H, Y and N) and make them do nothing in d-mode, then pressing those accidentally does nothing, other setting the d-mode variable to 1 (meaning that if I keep on pressing D, I stay in d-mode and can try to aim for the key I want again).

On my computer I've added this for your d-mode:

  [:##y :vk_none]  ; disable y in d-mode
  [:##h :vk_none]  ; disable h in d-mode
  [:##n :vk_none]  ; disable n in d-mode

... and it does help me. Now, when I press d (D), I make sure I press a right hand key quickly and don't worry about getting it wrong. As a consequence, I do press a right hand key quicker, and it seems to reduce the number of times I end up with "dl" or similar printed. I feel that's promising (because I'd rather not need to buy/build myself a QMK/ZMK keyboard). But, as I said, the next few weeks will tell whether that's enough.

@Noerdsteil
Copy link

Hey,
such a good explanation. I immediately used this config. Thank you so much.

One of my first use cases is the navigation.

Now, my question is:
Is there a specific reason not to take the natural home keys of the right hand for the arrows?

j k l ;

instead of

h j k l

Appreciate your feedback on this.

@WesleyYue
Copy link

@Noerdsteil that's because hjkl is inherited from vim which a lot of people are already familiar with

@farzadmf
Copy link

farzadmf commented Sep 4, 2024

Question for experts:

I'm using the :devices section and configuring the key [change] bindings as shown here, but it doesn't seem to be working unless I do it in Karabiner UI.

:devices {
  :razor-black-widow [{:product_id 521 :vendor_id 5426}]
}
{
  :main [
    {
      :des "[GOKU] keyboard change"
      :rules [
        :razor-black-widow
        [:left_command :left_control]
        [:left_control :left_command]
      ]
    }
    ; ... other modifications ...
  ]
}

EDIT: not sure if it's significant or not, but I see two instances of my device in Karabiner

image link (Github doesn't allow me to upload an image for some reason!)

UPDATE: I think it is working, but not the way I expect/want it. I basically want to swap CTRL and CMD keys, but:

  • If I do it like this, it seems like other applications (SKHD, Hammerspoon, etc.) will still see the non-swapped keys.
  • If I do it through the UI, it seems "global" and everyone sees it the same

So, I ended up doing it through the UI

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment