I design user interfaces and program software. Also I seem to have a blog now.
Blog RSS Mastodon

Recent Posts

Collection: X11 I3wm Setup

Evdoublebind and My Ergonomic Key Bindings

Published

To increase the ergonomics of my daily work I make extensive use of double bind keys, overloaded keys are described more in detail below. To achieve this I created Evdoublebind and configs currently for X11. The goal of this post is to introduce Evdoublebind and document the setup before it is migrated to wayland.

A Quick Introduction to Evdoublebind

Evdoublebind, via evdev, provides double bind keys: keys which are overloaded with functional acting as a modifier when held but another key when tapped alone. Although other applications strive for the same core functionality, Evdoublebind is unique in that it...

Double Binds and the Desired Effect

Effectively I want easy to reach keys to providing extra functionally being a modifier whilst held but different normal key when tapped, pressed and then released without other events occurring. For example my caps-lock key is remapped to the hyper modifier and overloaded with the escape key on tap. Similar binds on the caps-lock are common but I make extensive use doubles binds, the table below includes the list of them.

Base KeyModifier BindTap Bind
SpaceControlSpace
Caps LockHyperEscape
Left ShiftShiftBack Slash
Left AltAltASCII Circumflex
Right ShiftShiftAmpersand
SemicolonSuperSemicolon

When I was developing this mapping I was writing a papers in LaTeX which influenced the binding of the shift and alt keys. Although, I utilize the space bar, remapped to control in all my applications it has been most beneficial in my text editor, Emacs. The strangest feature of my setup semicolon remapped with super allows me to have full control of my tiling window manager with my hands on the home-row and can even navigate windows and cycle workspaces with one hand.

I should note that setup is designed and used with a standard qwerty US layout.

Before Evdoublebind

When I first started to experiment with double binds I utilized Xcape and Xmodmap, only producing a subset of the desired effect describe above. This setup served me for a while but Xcape had a number of problems, such as:

  1. Being generally slow, introducing a noticeable increase in input lag when my laptop was in powersave mode.
  2. Overloaded modifier keys could not be combined2, e.g. in my setup above I could not use space bar as control and semicolon as super to input control+super+_ commands.
  3. Xcape an application X11, does not support Wayland.

While searching for a Wayland compatible solution led me to discussion in a feature request issue for sway, a wayland compositor. The issue discussed implementing the double bind functionality either natively inside the compositor itself, which had opposition for upstream use. To implement this feature at a lower level in the input stack, Interception Tools was mentioned, which provides a unix-style piping interfacing for intercepting and sending inputs events on the evdev input interface.

Using Interception Tools a virtual keyboard could be created that intercepted all the input for the keyboard modifying and injection keys and transmitting them as its own events to achieve overloading feature. Since the mapping is done at a lower level is should work with wayland. However, I found Interception Tools unnecessarily bloated for my needs: requiring the c++ stdlib for some of binaries, requiring multiples programs run in the background and providing little since evdev is already exposed as unix-style interface.

Implementing Evdoublebind

Initially Evdoublebind, utilized the virtual keyboard strategy fully capturing events and reproducing its own. However, in pursuit of simplification and minimization of possible latency, I pivoted to a different strategy which also solved a bug3. Instead of fully capturing events I merely read them, and injected keys when necessary.

Before I discuss some huddles of the new strategy, I need to mention, Evdoublebind is only one part of the setup, a requirement by the level it works at and the desired feature set. Although Evdoublebind does the heavy work, injecting keys, a secondary mapping at a high level which does simple remapping of keys is necessary. Currently this secondary mapping is handled by X11 via Xmodmap however the mapping can be done with an xkb_layout which can be used with sway.

Theoretically this strategy has a fundamental ordering bug. Since the character device forms a queue, a key read followed by immediate a key injection may not immediately follow the read event. However, in practice, even under deliberate stress, I have not observed this issue. I found the simplicity and speed of the current implementation an acceptable trade off. If this issue becomes a problem it may possible mitigated by use of SYN_DROPPED events in some cases.

The actual huddle preventing implement ion of the new strategy is it does produce the same desired effect as describe above without a compromise. In the desired effect pressing a overloaded modifier key down does nothing until either two things happens:

  1. Another key is pressed down before the modifier released causing the overloaded key to act like a modifier, inserting its press before the other key press.
  2. The modifier key is released before any other key pressed causing the overload key to insert a tap of its tap key.

The core problem is that events can't be reordered or conditionally captured, so in case 1 there would be no way to inject a modifier key down before the newly pressed key is emitted.

To overcome this remap the overloaded keys to desired modifier in the secondary map so the modifier is already pressed for case 1. This simplifies the work Evdoublebind drastically. Essentially the only job it has now is to detect when an overloaded key is tapped, injecting the necessary key on such a tap. The trade off that when a tap of an overloaded key occurs two keys are tapped and sent, the modifier and desired tap key. Fortunately a tap of a modifier key usually has no effect.

As a result of allowing every key press to be sent and then preforming remapping in a secondary map, Evdoublebind can not directly emit keys in some cases without collision. For an example of a collision consider the space which I remapped to control in the secondary map. If Evdoublebind directly emitted the space key to produce the overload, the secondary map would convert the tap to a tap of control. To avoid this collision I overload keys without unused keys and map those in the secondary map to desired keys instead.

Configuring the Setup

For the sake of simplicity in implementation, Evdoublebind is source configured. Below is my configuration of Evdoublebind for my setup.

static const KeyBind KEYMAP[] = {
    {.key = KEY_LEFTSHIFT, .tap_key = KEY_BACKSLASH},
    {.key = KEY_RIGHTSHIFT,.tap_key = KEY_7, .tap_modifier = KEY_LEFTSHIFT},
    {.key = KEY_SPACE,     .tap_key = KEY_F13},
    {.key = KEY_CAPSLOCK,  .tap_key = KEY_ESC},
    {.key = KEY_SEMICOLON, .tap_key = KEY_F14},
    {.key = KEY_LEFTALT,   .tap_key = KEY_6, .tap_modifier = KEY_LEFTSHIFT}
};

You'll notice the use of tap_modifier which allows a tap key to briefly active a modifier well inserting that key. This has collision problem but is made necessary by my use of Xmodmap, as it can't directly remap a key to access shift modifier without removing the original binding. However I believe that peformaning the mapping in xkb_layout does not share this limitation.

Then for Xmodmap I use the following configuration. I noticed some issues when using keysyms from some of the remappings, as an alternative I used keycode with code by pressing the key in xev, a program that shows input information.

remove Lock = Caps_Lock
keysym Caps_Lock = Hyper_L
keysym semicolon = Super_R
keycode 65 = Hyper_R
keycode 192 = semicolon colon
keycode 191 = space
clear Mod1 
clear Mod2
clear Mod3
clear Mod4
clear Mod5
add Mod1 = Alt_L Alt_R Meta_L
add Mod2 = Num_Lock
add Mod3 = Hyper_L
add Mod4 = Super_L Super_R
add Mod5 = ISO_Level3_Shift Mode_switch
add Control = Hyper_R

Notable things in this config are:

Starting Evdoublebind

Since Evdoublebind accesses the input character devices, the program usually needs elevated privileges of some sort. You can deal with this in a number of ways here are some:

Future Improvements to Evdoublebind

Currently Evdoublebind only reads from one input device leading to program being ignorant of mouse input. Thus when modifier key is used mouse input when key is released the tap overload is inserted which can be annoying when using graphical software. There is 3 solutions I can think of:

  1. Toggle Evdoublebind when using graphical software to either completely off or a reduced number modifiers. This is what I currently do.

  2. I could poll on multiple input devices so evdoublebind can detect the mouse inputs to prevent unwanted insert. Also would allow one instance of evdoublebind to listen to multiple keyboards with little extra work. However, this could cause issues if the mouse is noisy sending small movements, sending false positives.

  3. Track key press time and have a minimum elapsed time for a tap key press. Could be a little inconsistent from the user point of view and lead to frustration.

Aside from that Evdoublebind key does not use libevdev for possibility premature optimization qualm of bloat.

Overall I think a more feature-rich but bloated version should be created: utilizing libevdev to be more compliant with evdev; returning to the virtual keyboard method but stilling utilize a secondary map to avoid the collision bug3 to avoid the fundamental but in-practice non-hindrance concurrency bug, polling on multiple inputs and tracking the time to make Evdoublebind be able to make smarter decisions. The increased latency introduced by these changes may be non-existent making the increased complexity the only trade off to produce something truly polished.

Footnotes

1

Below is the command used to generate the small binary and check the size, I should note the binary could be smaller by not having a read only page via the linker command norelro. If you also remove the error messages the binary will be less than 4KB, on most modern systems fitting in a single virtual page of memory.

> musl-gcc -static -O3 -flto -Wl,-z,noseparate-code src/evdoublebind.c;\
  strip -s ./a.out; wc -c ./a.out
5184 ./a.out
2

A patch does exist to add the functionally but is limited and requires adding each combination individually.

3

When I was using virtual keyboard mapping strategy in Evmodkey I doing all the remapping inside the application as well, this eliminated the necessity for an secondary map. However, without secondary map when key where remapped to existing keys collsion would occur and pressing them together could cause issues as libinput could not tell them apart.