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

Recent Posts

Making Low Level Keyboard Hacks Easy to Use

Published

While writing documentation for Evdoublebind, I realized how difficult it was to setup. It was designed to be as simple as possible but since it works at a low-level, users needed to do so as well. Additionally, complex configuration of XKB was required for the full feature set.

The main improvements to ease of use was: Introducing a unified configuration system which use accurate higher level representations of key values; Automating as much of the configuration as possible; and adding tool, evdoublebind-inspector, quickly get the information needed for the setup. But before I discuss the improvements why it hard to use in the first place.

Making it Hard

Source Configuration

Although, source configuration is not too much of burden, finding the perfect setup may require experimentation; users will trying many configurations thus needing to rebuild it each time.

Illusion of Simplicity:

The previous version used the labels from linux/input-event-codes.h for configuration so you could write {.key = KEY_LEFTSHIFT, .tap_key = KEY_BACKSLASH}, this lead to a number of problems:

Manual Investigation

When manually configuring XKB or using a non-US layout figuring what keys to bind was a lot of grepping around /usr/share/X11/xkb/ and using xev.

The Partial Configuration Annoyance

Broken configurations can result in broken input. This is mainly a problem when initially configuring a complex setup. For me this usually happens when the XKB settings are used but evdoublebind is not running. Since the XKB remaps my space-bar to control without evdoublebind running I have no space-bar. For me I solved this by binding a key in my window manager to toggle the setup, both XKB settings and evdoublebind. Importantly the binding is accessible from any combination of configurations.

Making it Easy

The original program was tiny, fast and simple but was hard to setup. Well making it easier I did not want to compromise on it's simplicity which is why most effort was in making extra programs to help with configuration.

Installation

As the original program was just a signal 100 line C file which required source configuration, I felt OK leaving installation details unspecified and only a spartan one line build script was provided. But since the project has grown and I eliminated the need for source configuration, a makefile is now provided, which can install the binaries. The makefile will by default compile all the binary with the system gcc but optionally can, using make musl-static, build static musl binaries instead. Currently, when installing, the group sticky bit is set on evdoublebind so user can run the process directly.

Additionally, a shell script, install_xkb_rule.sh, is provided to install a custom xkb_rule that adds the evdoublebind:mapping option.

Changes to Evdoublebind Itself

Since the key labels from linux/input-event-codes.h can be wrong, Evdoublebind should be configured from the keycode numbers directly. Using numbers the mapping can be easily specified via command line argument removing the need for source configuration. Thus the settings can be pass as like this,

 evdoublebind "/dev/input/by-id/path-kbd" "58:189,42:190,54:191"

which is hardly an improvement in terms easy of use if your write the mapping by hand.

The New Configuration Setup

Introducing, evdoublebind-make-config which has two purposes, generating the XKB configs and the evdoublebind arguments. The config generating uses xkbcommon to accommodate different keyboard layouts. Further the configuring allows for a direct description of the entire desired behavior. Writing, <CAPS> : Hyper_L | Escape, says caps-lock should be the hyper key when held and escape when tapped. The output keys are specified in XKB keysyms which are the resulting representation of key press after translation.

unused : <FK19> <FK20> <FK21> <FK22> <FK23> <FK24> <I159> <I160>
kbd : /dev/input/by-id/usb-Logitech_Logitech_G710_Keyboard-event-kbd
kbd : /dev/input/by-id/some-other-keyboard
<CAPS> : Hyper_L   | Escape             #Tap Caps -> Escape
                                        #Hold Caps -> Hyper
<LFSH> : Shift_L   | backslash          #Tap Left Shift -> \
<RTSH> : Shift_R   | ampersand          #Tap Right Shift -> &
<LALT> : Alt_L     | asciicirum         #Tap Left alt -> ^
<SPCE> : Control_L | space              #Hold Space -> control
<AC10> : Super_L   | semicolon colon    #Hld Semicolon -> Super

Notice the unused list which defines which keys to use as intermediate key-codes for tap keys. Running evdoublebind-make-config mysetup.conf, generates the XKB config, default locataion ~/.xkb/symbols/evdoublebind,

partial modifier_keys
xkb_symbols "mapping" {
   replace key <CAPS> { [ Hyper_L, Hyper_L, Hyper_L, Hyper_L ] };
   replace key <FK19> { [ Escape, Escape, Escape, Escape ] };
   modifier_map Mod3 { <CAPS> };

   replace key <LFSH> { [ Shift_L, Shift_L, Shift_L, Shift_L ] };
   replace key <FK20> { [ backslash, backslash, backslash, backslash ] };
   modifier_map Shift { <LFSH> };
...

and outputs to STDOUT the arguments for running the config for evdoublebind. Although, I would save the arguments to a file instead regenerating them each time, you can run,

evdoublebind-make-config mysetup.conf | while read args; do
  evdoublebind $args &
done

or, if you only have one keyboard, (one kbd specified),

evdoublebind $(evdoublebind-make-config mysetup.conf) 

If your only want to generate the evdoublebind arguments and not make the XKB config pass the -a flag to evdoublebind-make-config.

The Setup for XKB

Since evdoublebind-make-config generates the configuration for you all you have to do is tell XKB to use it.

Currently there is no way to add an option to XKB without redefining the entire rule set. Running install_xkb_rule.sh installs a modified rule set to /.xkb/rules/evdev-doublebind. Then for configuration on X11 you can usually,depending on your desktop environment/window manager, run

setxkbmap -I$HOME/.xkb -rules 'evdev-doublebind' -option evdoublebind:mapping\
 -print | xkbcomp -w 2 -I$HOME/.xkb - $DISPLAY

some desktop environment/window manager have there own way of configuring XKB. Sway: Add the following to your sway config, man sway-input for my details or Input configuration wiki.

input * {
    xkb_rules  evdev-doublebind
    xkb_options evdoublebind:mapping
}

For gnome and cinnamon you can use gsettings set org.gnome.desktop.input-sources to configure XKB although I have tried run something like this in a terminal,

gsettings set org.gnome.desktop.input-sources sources "[('xkb', 'us')]"
gsettings set org.gnome.desktop.input-sources xkb-rules "['evdev-doublebind']"
gsettings set org.gnome.desktop.input-sources xkb-options "['evdoublebind:mapping']"

Evdoublebind-inspector

Before to do anything with evdoublebind you needed to find your keyboard dev input node, which previously was done by searching around /dev/input/by-id/ and /dev/input/by-path with a bit of trial and error. Now, just run sudo evdoublebind-inspector. You will get a list of inputs evdoublebind-inspector thinks might be your keyboards and it outputs events from those keyboards so you can make sure.

Found 2 Possible Keyboards.
---
/dev/input/by-id/usb-Logitech_Logitech_G710_Keyboard-if01-event-kbd
/dev/input/by-id/usb-Logitech_Logitech_G710_Keyboard-event-kbd
---
[/dev/input/by-id/usb-Logitech_Logitech_G710_Keyboard-event-kbd]{
  EVDEV: keycode:28 name:KEY_ENTER;
  XKB: key[36]:<RTRN> keysym:Return;
}
[/dev/input/by-id/usb-Logitech_Logitech_G710_Keyboard-event-kbd]{
  EVDEV: keycode:31 name:KEY_S;
  XKB: key[39]:<AC02> keysym:s;
}
...

Besides helping you identify your keyboard it also gives you lots of the information needed to make the configuration file for evdoublebind-make-config. As the <KEY_NAME> in the XKB line allows you to identify the key your want to rebind. The keysym is also the same keysyms used in the config.

There are only two things evdoublebind inspector can't directly help you find, both of which pertain to keys not on your keyboard and should be mostly edge cases.

A Simple Example, that Just Works.

Previously you had to do some fiddling around, finding you keyboard and tweak values, figuring out XKB before you could just see evdoublebind work properly.

But now you can run the following on X11, (and assuming you desktop environment doesn't get in the way like gnome does), after installation.

#!/bin/sh
#install XKB rule set: evdev-doublebind
./install_xkb_rule.sh

 echo 'unused : <FK19> <FK20> <FK21> <FK22> <FK23> <FK24>' > basic.conf

# find keyboards, THIS MAY FIND TO MANY KEYBOARDS!!! for real setup please
# run `evdoublebind-inspector` to identify your actual keyboards
echo "sudo evdoublebind-inspector -k : to get keyboards"
sudo evdoublebind-inspector -k | awk '{print "kbd:" $1;}' >> basic.conf
echo '<CAPS> : Control_L   | Escape' >> basic.conf

# Generate XKB_option will go in `~/.xkb/symbols/evdoublebind` and `evdb.in`.
evdoublebind-make-config -c evdb.args basic.conf || exit #abort on failure

#make sure no other instances are running
killall evdoublebind

#Start evdouble-bind
cat evdb.args | while read args; do
    evdoublebind $args &
done

# Alternatively you could generate the arguments from the config
# and pass the directly
# evdoublebind-make-config basic.conf | while read args; do
#     sudo ../build/evdoublebind $args &
# done

#xkb to use the generated option
setxkbmap -I$HOME/.xkb -rules 'evdev-doublebind' -option evdoublebind:mapping\
 -print | xkbcomp -w 2 -I$HOME/.xkb - $DISPLAY
echo "it is 'normal' for xkbcomp to output some warnings."

This shell scripts setups and starts a CAPS as Control and tap for escape setup and requires no extra configuration to work.

This is what default setting for the old evdoublebind did as well however it did not configure XKB and so did not fully work.

Evdoublebind as a Stand-Alone Utility without XKB

The naive setup is for Evdoublebind to emit keycode already associated with keycodes on the keyboards. This was previously, done by write a config like {.key = KEY_LEFTSHIFT, .tap_key = KEY_BACKSLASH}. The naive approach has may problems as discussed above and limits functionally since can only add tap keys,which are already defined for keyboard layout, to keys, that are already modifiers. However, this limited amount of functionality may be enough.

Additionally, some desktop environments (e.g. gnome) provide configuration which make easy to map Capslock to control. These limited configurations, although in a manual fashion can be used broaden the naive setup as in this case Capslock could now be overload by Evdoublebind as well.

Previously, it was easy to use Evdoublebind in a naive way that did not require configuring XKB. However, since configuring XKB layouts,rules and options is inconsistent and sometimes not well supported across different desktop environments, using the Naive approach may still be desirable.

The updated Evdoublebind can still be used in the naive way.

Using evdouble-make-config, the only thing that matters is the unused line and the key definition at the beginning of each line in the mapping, unused keys are used in order.

unused : <ESC> 
kbd : /dev/input/by-id/some-other-keyboard
<CAPS> : void  |  void

This would generate the mapping 58:1.

Or instead you could write the mapping manually, if you run sudo evdoublebind-inspector you can see the EVDEV keycodes, these are what evdoublebind uses for the mapping.

[/dev/input/by-id/usb-Logitech_Logitech_G710_Keyboard-event-kbd]{
   EVDEV: keycode:31 name:KEY_S;
   XKB: key[39]:<AC02> keysym:s;
}

The mapping argument is just a comma sperated list of BASE_KEYCODE:TAP_KEYCODE,....

Once you have your mapping you can just run evdoublebind as shown below.

 evdoublebind "/dev/input/by-id/path-kbd" "58:189,42:190,54:191"

Conclusion

I think I have made Evdoublebind a lot easier to use without sacrificing any of the efficiency or simplicity of the original program. Most importantly I limited the scope of details the user most know and consolidated the bulk of configuration to one file. So, if after reading this you still have troubles configuring evdoublebind please open a issue.

ASIDE: Linux Keyboard Mapping Identifiers

To processing and translating a key event on a modern linux system there 4 important identifies shown below in the order they used to process the key event.

1. Evdev Keycode [docs]

  • What evdoublebind uses to identify and send keys.
  • The main identifier for keys in evdev.
  • Not translated to keyboard layout yet!
Defined by kernel found in `linux/input-event-codes.h`
#define KEY_X			45
#define KEY_C			46
#define KEY_V			47
Used by Old Evdoublebind `KEYMAP`
const KeyBind KEYMAP[] = {
   {.key = KEY_SPACE,     .tap_key = 183}, 
   {.key = KEY_CAPSLOCK,  .tap_key = KEY_ESC},
};
Used by XKB to calculate XKB Keycode
int evdev_offset = 8; //Usally 8 for most keyboards/layouts.
xkb_keycode_t xkbcode = ev_keycode + evdev_offset;

2. XKB Keycode [docs]

  • Represents a key in XKB before translation or labeling.
  • XKB keycode can be calucated from a Evdev Keycode by adding 8.
Calculated from Evdev Keycode
int evdev_offset = 8; //Usally 8 for most keyboards/layouts.
xkb_keycode_t xkbcode = ev_keycode + evdev_offset;
Used to create XKB Key Name in XKB keycode file.
// <XKB_KEY_NAME> = XKB_KEY_CODE;
// from: /usr/share/X11/xkb/keycodes/digital_vndr/pc
    <LCTL>	= 17;
    <LALT>	= 25;
    <SPCE>	= 41;
    <RALT>	= 57;
Provided from output of `xev`
   KeyRelease event, serial 28, synthetic NO, window 0x2400001,
   ...
    state 0x0, keycode 9 (keysym 0xff1b, Escape), same_screen YES,
   ...
   

3. XKB Keyname

  • A convenient label that map directly to a XKB Keycode.
Used to define XKB Keysyms
// key <XKB_KEY_NAME> { [ XKB_KEY_SYM ... ] };
   key <FK14> { [ semicolon, colon ]  }; 
   key <SPCE> { [ Control_L ]  };
   key <CAPS> { [ Hyper_L ] };

4. XKB Keysym [docs]

  • The key representation after the keyboard layout mapping has completed
Mapping to define from XKB Keysyms
// key <XKB_KEY_NAME> { [ XKB_KEY_SYM ... ] };
   key <FK14> { [ semicolon, colon ]  }; 
   key <SPCE> { [ Control_L ]  };
   key <CAPS> { [ Hyper_L ] };
Provided from output of `xev`
   KeyRelease event, serial 28, synthetic NO, window 0x2400001,
   ...
    state 0x0, keycode 9 (keysym 0xff1b, Escape), same_screen YES,
   ...