Reconfiguring Keyboard Keys with XKB

Updated: May 07, 2024

Recently two keys in my laptop keyboard went off, none of my effort successfully bought back those two keys. So, I was thinking what should I do now, here are the list of things I came up

  • Replace the keyboard.

  • Replace the scan-code of two unused keys through hwdb [1].

  • Add the missing keys to one already working key as Level3 key.

The first one is what we normally do, eventually, I will also going to do if I have chance to buy replacement for my laptop keyboard. In the mean time, I need to have a temporary workaround. The second one will make my keyboard loose two other keys instead of the already lost keys. So, I opted for third one.

Scan Codes

When you press and release a key in your keyboard, it generates scan codes [2]. Usually these are two bytes, the first byte gets generated when you press and the second byte get generated when you release flipping the most significant bit. For example, when you press and release ESC key, it generates 0x01 0x81 bytes, the first byte is 000000001 which got generated when you pressed the ESC key, the second byte is 10000001 which got generated when you released ESC key. You can get the scan codes of each key through showkey -s [3] command.

Kernel Keycodes

When kernel [4] receives these scan codes from keyboard event, the input driver converts these scan codes to kernel keycodes [5]. These keycodes are defined under /usr/include/linux/input-event-codes.h headers. Kernel maintains one scancode-to-keycode table, you can map any scan-code to any keycode using setkeycodes [6] command. To view the current scancode-to-keycode table, you can use getkeycodes [7] command.

Console Keymaps

The kernel’s scancode-to-keycode table is not enough for the tty driver, so it defines another table called keymaps [8]. Apart from the normal keys, there are control keys in our keyboard. This keymaps table defines what should happen when a control key and normal key pressed. we can use dumpkeys [9] command to see the current keymaps table and we can use loadkeys [10] command to load a new keymaps table (these keymaps table definitions are available under /usr/share/keymaps directory). Here is one simple example to to load custom keymap

$ # this commands are run from tty3 (you need to press ctrl+alt+f3 to switch to tty3)
$ gzip /usr/share/kbd/keymaps/i386/qwerty/us.map.gz | sed '/ 51 =/s/comma/less/g;/ 51 =/s/less/comma/g' | sudo tee /usr/share/kbd/keymaps/i386/qwerty/test.map
$ echo "compose 'h' 'a' to U+0B95" >> /usr/share/keymaps/i386/querty/test.map
$ loadkeys test
$ # if you press , key now, it will come as < key. if you press shift + , keys (< key) it will come as , key.
$ # if you press left-alt + right-alt, compose will be activated.
$ # With compose activated, if you press 'h' and 'a', then tty driver will generate க
$ # (unicode = 0B95, utf-8 = 0xe0 0xae 0x95) in the console.
$ loadkeys us
$ # switched back to default us keymap

Most of Keymaps table contains “ <keycode> = <level0 keysym> <level1 keysym> … <level256 keysym> “ lines. Here <keycode> represents the kernel keycodes from scancode-to-keycodes table, <level0 keysym> means what character to use when that particular keycode key got pressed, <level1 keysym> means what character to use when Shift modifier is active and keycode key got pressed. Shift modifier will get activated when you press Shift key. There are 9 modifier keys available and each modifier carries different weight,

  1. Shift (weight = 1)

  2. AltGr (weight = 2)

  3. Control (weight = 4)

  4. Alt (weight = 8)

  5. ShiftL (weight = 16)

  6. ShiftR (weight = 32)

  7. CtrlL (weight = 64)

  8. CtrlR (weight = 128)

  9. CapsShift (weight = 256)

With the help of these modifiers, we can choose the level, the sum of weight of all the activated modifiers decides the level. For example, lets assume the current activated keymap have a line like this,

keycode 30 = a b c d e f g h i j k l m n o p q r s t u v w x y z

When we press Control + Shift + a, then the level will be 5 (weight of Control is 4, weight of Shift is 1, so the sum of weight of currently activated modifiers is 5, so the current level is 5), thus, f (which is in the level5 position) is the final keysym, so f will be shown in the console.

When we simply press a, then the level will be 0 (there is no modifiers currently active, so the current level is 0), thus a (which is in the level0 position) is the final keysym, so a will be shown in the console.

Note

the command dumpkeys -f will not show all the levels of a particular keycode, the first line of the output may say keymaps 0-2,4-6,8-9,12 which means, the output contains only the levels level0, level1, level2, level4, level5, level6, level8, level9, level12. So make sure you interpret the dumpkeys -f output correctly.

Compose Key

Apart from keycode assignment, you can add compose key sequences in keymaps, For example, lets assume the current activated keymap have a line like this,

compose 'h' 'a' to U+0B95

When we press leftalt + rightalt, the compose modifier gets activated (Alt + AltGr which is the default key combo to activate compose). After the compose modifier activated, if you press ‘h’ key and ‘a’ key, then you will get ‘க’ in the console.

XKB

XKB [11] is another table similar to keymaps but for Xorg [12] and also used by Wayland [13]. XKB does not use kernel keycodes directly like keymaps table, but converts the kernel keycodes to xkeycodes (<kernel-keycode> + 8 = <xkeycode>). Again, these xkeycodes are mapped into xkeys. These xkeycodes to xkeys mapping is defined under /usr/share/X11/xkb/keycodes directory. These xkeys are then used in symbol tables. These symbol tables are available under /usr/share/X11/xkb/symbols directory. Each symbol table contains lines similar to this,

// example symbol table saved as /usr/share/X11/xkb/symbols/demo
xkb_symbols "demo" {
   key <AC01> {
      type[Group1] = "TWO_LEVEL",
      type[Group2] = "FOUR_LEVEL",
      symbols[Group1] = [ 'a', 'A' ],
      symbols[Group2] = [ 'a', 'A', 'b', 'B' ]
   };
   include "level3(ralt_switch)"
   include "group(win_space_toggle)"
   include "compose(menu_altgr)"
}

XKB contains 8 groups and 256 levels for each group, so you can load 2048 keysyms on a single xkey. The above symbol table only defines Two Groups, The first Group Group1 type is TWO_LEVEL, which means, it has only Level1 and Level2.

Note

XKB levels starts from level1 instead of level0 like in keymaps table. When there is no modifiers currently active, XKB chooses level1 keysym but keymaps table chooses level0 keysym.

The second Group Group2 have FOUR_LEVEL, means, Level1, Level2, Level3, Level4. So, the above table totally loads 6 keysyms to <AC01> xkey (‘A’ key in keyboard).

By default, Group1 will be active, so, when we press ‘a’ key in keyboard, Level1 will be selected and the keysym a will be used, because there is no modifier currently active.

When we press Shift + a key in keyboard, Level2 will be selected and the keysym A will be used, this is because we used type[Group1] = "TWO_LEVEL", this TWO_LEVEL type defines which modifier enables which Level. These type definitions are inside /usr/share/X11/xkb/types directory. If you look at /usr/share/X11/xkb/types/basic file, for TWO_LEVEL, you can see that map[Shift] = Level2 line, which means, Shift modifier will enable Level2.

To use the keysyms in Group2, we have to switch to Group2 by pressing Win + Space keys (The above symbol table includes group(win_space_toggle), the definition of win_space_toggle is in /usr/share/X11/xkb/symbols/group file). Assume that you successfully switched to Group2.

Now, when you press RightAlt + a key in keyboard, Level3 will be selected and keysym b will be used. because we used type[Group2] = "FOUR_LEVEL", this FOUR_LEVEL type definition is inside /usr/share/X11/xkb/types/extra file. If you look at this file, for FOUR_LEVEL, you can see map[LevelThree] = Level3, which means, LevelThree modifier will enable Level3. Also, you can see map[Shift+LevelThree] = level4, which means, Pressing both Shift modifier and LevelThree modifier will enable Level4. The above symbol table includes level3(ralt_switch), the definition of ralt_switch is in /usr/share/X11/xkb/symbols/level3 file. If you look at that definition, it will say that <RALT> will generate ISO_Level3_Shift keysym. This ISO_Level3_Shift keysym interpretation is defined in /usr/share/X11/xkb/compat/iso9995 file, In this file, you can see that ISO_Level3_Shift sets Modifier=LevelThree.

When you press Shift + RightAlt + a key in keyboard, Level4 will be selected and keysym B will be used. We already know from the previous paragraph that FOUR_LEVEL type defines Shift + RightAlt selects Level4 level.

Multi Key

Just like keymaps table's Compose Key, XKB also have facility to Compose multiple keys to generate a single keysym. We have to first activate Compose modifier by pressing Menu + AltGr keys, because in the above table, we used compose(menu_altgr), the definition of menu_altgr is inside /usr/share/X11/xkb/symbols/compose file. If you look at the definition, MENU Key with AltGr sets Multi_key modifier. This modifier enables Compose facility. The key combinations for Compose are under /usr/share/X11/locale/<locale>/Compose file, here <locale> is the LANG code. If you look at Compose file for en_US.UTF-8 (/usr/share/X11/locale/en_US.UTF-8/Compose file), you can see that pressing o and c will produce © character. So, Pressing MENU + Altgr o c will produce © unicode character.

Modifications for my broken Keyboard

After learning all these things, I finally have this override saved in /usr/share/X11/xkb/symbols/local file.

partial alphanumeric_keys modifier_keys
xkb_symbols "override" {
  key <I151> {
    type[Group1] = "TWO_LEVEL",
    symbols[Group1] = [ ISO_Level3_Latch ]
  };
  key <AB03> {
    type[Group1] = "THREE_LEVEL",
    symbols[Group1] = [ c, C, Multi_key ]
  };
  key <AC06> {
    type[Group1] = "THREE_LEVEL",
    symbols[Group1] = [ h, H, Left ]
  };
  key <AC07> {
    type[Group1] = "THREE_LEVEL",
    symbols[Group1] = [ j, J, Down ]
  };
  key <AC08> {
    type[Group1] = "THREE_LEVEL",
    symbols[Group1] = [ k, K, Up ]
  };
  key <AC09> {
    type[Group1] = "THREE_LEVEL",
    symbols[Group1] = [ l, L, Right ]
  };
  key <AC10> {
    type[Group1] = "FOUR_LEVEL",
    symbols[Group1] = [ semicolon, colon, apostrophe, quotedbl ]
  };
};

The meaning of these overrides are as follows

  • Pressing Fn activates ISO_Level3_Latch, Latch means, you don’t have to keep on pressing the key, one press is enough. So, once I press and release Fn key, LevelThree modifier will be active.

  • Pressing Fn c activates Multi_key.

  • Fn h will generate Left Arrow keysym.

  • Fn j will generate Down Arrow keysym.

  • Fn k will generate Up Arrow keysym.

  • Fn l will generate Right Arrow keysym.

  • Fn ; will generate apostrophe keysym (single quote).

  • Fn Shift + ; will generate quotedbl keysym (double quote).

I have to enable my override with the following steps,

  • add local:override to /usr/share/X11/xkb/rules/evdev under !option

  • enable ‘local:override’ to ‘xkb-options’ under gsettings

    $ gsettings set org.gnome.desktop.input-sources xkb-options "['local:override']"
    

There are few tools which helps to write XKB configuration files, Here are the few ones which I know

setxkbmap [14]:

Used to compile xkb configuration files and generate data to load into Xorg Server process

xkbcomp [15]:

Used to get set xkb configuration data from/to Xorg Server process

xev [16]:

Used to show xkey codes and xkeysyms when we press any in keyboard

xkbcli [17]:

Used to show details about each key press in keyboard (like xev)

evtest [18]:

Used to attach with input device and show events from that device

libinput [19]:

Used to attach with input device and show events from that input device

Drawback of XKB

All the custom modification works as long as we are working locally, if we try to use any remote application, or a VM, our XKB customization will not work, because those remote apps directly sends scan-codes to the remote location rather than the generated XKB keysyms.