The infinity Keyboard
Status: In Progress

Technically, it is possible to program your own keymap if the EC firmware is source, as it's case for my laptop (open sourcing of the firmware), or if you have a custom keyboard.

You can achieve better results using a software solution such as Kmonad, allowing an unlimited number of layers and macros.

Firmware-level customization: Framework Laptop/Chromebooks

Some Framework community members have already managed to interact directly with the embedded chip to change the key codes. You can use the same trick on Chromebooks, but you have to figure out the key matrix.

The logic is as follows: each key has its own coordinates (in the key matrix) and the EC translates them to scan codes. You can find the key matrix for the framework laptop here. I've re-created it below using keyboad-layout-editor.com.

What is the scan code for "A" key?: 1C, and you can find the rest of the scan codes here.

If we want to map the key (6,5) to A (W in QWERTY layout), you'd run the following command:

ectool --interface=fwk  raw 0x3E0C d1,d1,b6,b5,w1c # W > A

3E0C is the command for updating the key matrix. You can find more: The Framework Laptop's Embedded Controller (EC) :: HowettNET.

These changes would persist for ~10min after shutdown. So consider setting up a script that is executed automatically after each boot.

To manage this process properly, I used a json file that maps the needed keys to their scan codes (key2code.json) and another that represents my keymap (coord2key.json). Then I created create_script.py that generates the script of ectool commands.

The script maps the matrix coordinates to the desired scan code. All these assets are expected to be in the same folder.

#!/usr/bin/python
# create_script.py

import json

script = ['#!/bin/bash']
command = '/usr/sbin/ectool --interface=fwk raw 0x3E0C'
# coord2key contains:
#   "0,2":"q",
#   "6,5":"w",
#   "2,4":"f",
coord2key = json.load(open("coord2key.json","r"))
# key2code contains:
#   "f2":"06",
#   "f3":"04",
#   "f4":"0c",
key2code = json.load(open("key2code.json","r"))

for coord, k in coord2key.items():
    row, col = coord.split(',')
    code = key2code[k.lower()]
    script.append(f'{command} d1,d1,b{row},b{col},w{code}'.lower())

with open("change_layout.sh","w") as f:
    f.write("\n".join(script))
    f.close()

The new keymap (blank for unchanged),

The same keymap represented using the scan codes.

You can then schedule the script to run automatically during the boot step.

In OpenRC, rename the generated change_layout.sh to 00_change_layout.start, chmod +x it and add it to /etc/local.d/. This requires the local service to be enabled.

Software customization

The firmware approach is not always possible and has its limitation. I use it as a first customization step. The second (software) step is to use Kmonad, which offers endless possibilities.

I've used it on Linux and Windows without any runtime issues. The setup can be intimadating at first, but you just have to start and the rest should go smoothly.

  • Start by installing Kmonad: https://github.com/kmonad/kmonad.

You might need to get some dependencies for Xorg to manipulate the input devices. For instance, the User level driver support should be enabled in the kernel (uinput module)

  • In the repo's keymap folder you can start from tutorial.kbd.

I'll go through a minimal keymap file here. First, find the path to your keyboard device. Mine is /dev/input/by-path/platform-i8042-serio-0-event-kbd. Let's define the keyboard config:

(defcfg
  ;; For Linux
  input  (device-file "/dev/input/by-path/platform-i8042-serio-0-event-kbd")
  output (uinput-sink "KMonad Keyboard")
  cmp-seq ralt    ;; Set the compose key to `RightAlt'
  cmp-seq-delay 5 ;; 5ms delay between each compose-key sequence press

  ;; Comment this if you want unhandled events not to be emitted
  fallthrough true

  ;; Set this to false to disable any command-execution in KMonad
  allow-cmd true
)

Now we need to define our source layer, which should represent our input keyboard. It doesn't have to match all key in the keyboard. Its goal is to define the keys that should be channeled through the Kmonad sink.

I'm using the layout injected to the firmware:

(defsrc
  grv  1    2    3    4    5    6    7    8    9    0    -    =    bspc
  tab  q    w    f    p    b    j    l    u    y    ;    [    ]    \
  caps a    r    s    t    g    m    n    e    i    o    '    ret
  lsft x    c    d    v    z    k    h    ,    .    /    rsft
  lctl lmet lalt           spc            ralt rmet cmp  rctl
)

For Kmonad to work, we need at least one deflayer. The colemak layer will be our base layer.

My secondary layer is the digits layer (make sure to handle the parentheses and underscore properly). We also need to define aliases to switch between the two

(defalias
    col  (layer-switch colemak) ;; switch to the base layer
    lcol (cmd-button "echo colmk > /etc/kmonad/status") ;; echo colmk to status file
    scol (tap-macro @lcol @col) ;; switch and log colemak
    dig  (layer-switch digits) ;; switch the digits layer
    ldig (cmd-button "echo digit > /etc/kmonad/status") ;; echo digit to status file
    tdig (layer-toggle digits) ;; digits is active while the button is pressed
    sdig (tap-macro @ldig @dig) ;; echo and switch to digits
    ssdg (tap-next @sdig (layer-toggle digits))
)

Aliases are basically custom keys. @col/@dig (Caps) will switch between the layers, while the digits layer would be active as long as @tdig is pressed (Right Alt)

I've created @lcol (log colemak) as a hack to echo the layer name to /etc/kmonad/status, which can be used to display the current layer using other tools (i3, polybar...). Now, using tap macro, I can assign a key to tap @lcol @col.

With @ssdg I can use the digits layer while holding the Caps key, but it will behave as @sdig when tapped.

Kmonad offers numerous features and I recommend going through the tutorial file (see above) for an extensive guide. It's quite intuitive and only your creativity is the limit.

(deflayer colemak
  grv  1    2    3    4    5    6    7    8    9    0    -    =    bspc
  tab  q    w    f    p    b    j    l    u    y    ;    [    ]    \
@ssdg  a    r    s    t    g    m    n    e    i    o    '    ret
  lsft x    c    d    v    z    k    h    ,    .    /    rsft
  lctl lmet lalt         @sspc           @tdig rmet cmp  rctl
)

(deflayer digits
  grv  1    2    3    4    5    6    7    8    9    0    -    =    bspc
  tab  1    2    3    4    5    6    7    8    9    0    [    ]    \
 @scol ~    {    [    \(   -    \_   \)   ]    }    `    +    ret
  lsft @    #    #    $    !    \    &    *    |    =    rsft
  lctl lmet lalt           spc            ralt rmet cmp  rctl
)

Now we're all set with our basic config keymap.kbd. I've placed it in /etc/kmonad/.

# test the service in debug mode
kmonad -l debug /etc/kmonad/keymap.kbd

We can also create our own kmonad service that starts at boot time. Simply add the following file /etc/init.d/kmonad then activate the service rc-update add kmonad boot.

#!/sbin/openrc-run
# 2023, Ayoub Ghriss
# Distributed under the terms of the GNU General Public License v2
# You don't want to use the local.d trick for this part
# local.d scripts require commands that terminate. Kmonad has to stay running in
# the background 

name="kmonad daemon"
description="Apply kmonad layout"
command=/usr/bin/kmonad
command_args="/etc/kmonad/keymap.kbd"
command_background=true
pidfile="/run/${RC_SVCNAME}.pid"
depend() {
        after *
}

Bonus

Or in case you use systemd and systemctl enable kmonad

# /etc/systemd/system/kmonad.service
[Unit]
Description=kmonad keyboard config

[Service]
Restart=always
RestartSec=3
ExecStart=/usr/bin/kmonad /etc/kmonad/keymap.kbd
Nice=-20

[Install]
WantedBy=default.target

Polybar integration

If needed:

[module/kmonad]
type = custom/script
exec = cat /etc/kmonad/status
exec-if = pgrep -x kmonad
interval = 0
format-prefix-foreground = #5b