Keyboard machinations with Kmonad

↩ blog

← Previous: Emacs Sidebars
→ Next: Literate Programming 2: Jumping from compile errors to the literate program

<2021-07-22T20:25:53+0530>

Yesterday, as I was going through the xcape issue tracker trying to find a way to configure it such that Shift would emit parentheses and place the cursor between them when tapped, I learned about Kmonad.

I began looking through their documentation to see if it could do what I wanted. I was pleasantly surprised to see that they chose s-expressions for their configuration format. I couldn't find my answers in the documentation, but the small-yet-active community in the IRC channel confirmed that it had the constructs for what I wanted.

Time to install it and give it a shot.

Installation

It wasn't present in the Debian Testing repositories, but installation was far from the ordeal I was expecting - I downloaded the Linux binary from their releases, did the usual chmod u+x <binary>, and placed it in ~/bin/. It's not every day that life is so easy - that was all it took.

Configuration

Each configuration file requires a defcfg form -

(defcfg
  ;; ** For Linux **
  input  (device-file "/dev/input/by-id/usb-04d9_1203-event-kbd")
  output (uinput-sink "KMonad output")
  fallthrough true)

The easiest thing to do was to swap Caps Lock and Escape, like I do with setxkbmap.

(defsrc CapsLock Esc)
(deflayer default Esc CapsLock)

I tried invoking kmonad with the -d option to dry-run my configuration and report any errors. Once I had a configuration it was happy with, I tried running it without -d, and ran into the uinput permissions problem covered in their FAQ. With some hassle and more help from the IRC channel (lesson learned - udev rule files must have the extension .rules), I finally got it running.

Inserting parentheses with Shift

I had a few false starts here; first, I tried -

(defalias
  parens      (tap-macro \( \) Left :delay 5)
  left-shift  (tap-next @parens LeftShift)
  right-shift (tap-next @parens RightShift))

(defsrc
  LeftShift RightShift
  CapsLock Esc)

(deflayer default
  @left-shift @right-shift
  Esc CapsLock)

…only for it to complain about the :delay keyword. I was helpfully informed on the IRC channel that the :delay feature was not yet released.

Then, I tried -

(defalias
  parens      (tap-macro \( P5 \) P5 Left)
  left-shift  (tap-next @parens LeftShift)
  right-shift (tap-next @parens RightShift))

(defsrc
  LeftShift RightShift
  CapsLock Esc)

(deflayer default
  @left-shift @right-shift
  Esc CapsLock)

…which did result in () being inserted when I pressed Shift, but also in the cursor going backward indefinitely until I pressed something else.

With more input from the community over the IRC channel, I finally got it working -

(defalias
  parens      (tap-macro \( P5 \) P5 Left P5)
  left-shift  (tap-next @parens LeftShift)
  right-shift (tap-next @parens RightShift))

(defsrc
  LeftShift RightShift
  CapsLock Esc)

(deflayer default
  @left-shift @right-shift
  Esc CapsLock)

Using Space to send Ctrl

Lastly, I tried getting Space to trigger Ctrl when used as part of a key chord (or "held", in Kmonad parlance). The rationale was that Ctrl is used in nearly every program—the idea of using the strongest fingers to press it, and to do so without leaving the home row, was alluring. Within Emacs, I'm a heavy user of C-m for Enter, C-w for backward-kill-word, and C-h for backward-delete-char, along with many other Ctrl-based bindings (even in a modal editing setup).

This was what got me to finally learn, through trial and error, what all the different button-definition commands were for.

tap-next behaves similar (but not identical) to xcape -

  • to send Space, you tap and release Space;
  • to send Ctrl, you hold Space and press another key.

I tested it in Emacs' speed-type-top-1000, and quickly ran into its downside - if you type quickly, Space may not release fast enough and you can unwittingly send Ctrl. Having to avoid it while typing slowed me down quite a lot.

With tap-hold -

  • to send Space, you tap and release Space within the timeout;
  • to send Ctrl, you hold Space beyond the timeout.

This, too, is not suitable for fast typing.

tap-hold-next is a combination of the above two, and is (I think) the exact behaviour of xcape -

  • to send Space, you tap and release Space;
  • to send Ctrl, you either hold Space and press another key, or hold Space beyond the timeout.

Also not suitable for fast typing. The use case is to prevent unintentional triggers of Ctrl (not very useful in this case, but imagine if it was Esc (tap) and Ctrl (hold) instead of Ctrl and Space, and an accidental Esc could close your chat window) - if you accidentally press Space, just hold it down and it will trigger Space instead of Ctrl.

tap-next-release seemed like the solution -

  • to send Space, you tap Space (no timeout, no waiting for the release);
  • to send Ctrl, you hold Space and press and release another key to send Ctrl-<that key>.

With this, I was able to type at my usual speed. The only downside 1 was that holding down the keychord did nothing (e.g. you can't hold down C-left to go back many words).

tap-hold-next-release removed that downside -

  • to send Space, you tap Space (no timeout, no waiting for the release);
  • to send Ctrl, you either hold Space and press another key, or hold Space and another key for longer than the timeout. Once the timeout is exceeded, holding down the chord repeats it.

My final configuration looks like this -

(defcfg
  ;; ** For Linux **
  ;; TVSe Gold Prime
  input  (device-file "/dev/input/by-id/usb-04d9_1203-event-kbd")
  output (uinput-sink "KMonad output")
  fallthrough true)

(defalias
  parens      (tap-macro \( P5 \) P5 Left P5)
  ;; tap-hold-next rather than tap-next, to prevent accidental
  ;; insertion of parentheses while typing capital letters
  left-shift  (tap-hold-next 700 @parens LeftShift)
  right-shift (tap-hold-next 700 @parens RightShift)
  space       (tap-hold-next-release 200 Space LeftCtrl))

(defsrc
  LeftShift RightShift
  Space
  CapsLock Esc)

(deflayer default
  @left-shift @right-shift
  @space
  Esc CapsLock)

Epilogue

I'm quite excited to get used to this new keyboard layout, even though I'm still accidentally hitting Caps Lock and exiting chats in Gajim at the moment. 😅 (stemming from the muscle memory of my previous configuration)

In the upcoming days I might try creating a layer for emojis, to get a uniform interface for the ones I use regularly. I also want to try binding C-h to Backspace and C-w to C-backspace, hopefully bringing these bindings out of terminals and my Emacs to all applications.

Drop me a line

Support me on Liberapay

Footnotes:

1

A possible downside to setting tap/hold behaviour for Space in general is that you can't hold it down to insert many spaces. Not something I'm particularly concerned about. I also imagine it would be an issue if one were playing a game, such as an FPS.