« Back to article list

Sixty Percent Keyboard

Table of Contents

Sixty Percent Keyboard

Update: Also picked up a Kraken Pro 60 BRED edition with linear gateron silver speed switches - linear is quite a unique typing experience.

So, I've been a fan of mechanical keyboards since the time I first got to use my Unicomp mechanical keyboard (https://www.pckeyboard.com/page/SFNT) with the little Thinkpad-style nub on it a few years ago (likely, much to the dismay of my coworkers - that thing sounds like a hammer pounding nails).

As my Unicomp was still at the work office, around the start of the pandemic, I picked up a cheap mechanical keyboard, a Redragon TKL (tenkeyless) on Amazon for about $40. It had great reviews, and was a lot of fun to use for my daily driver at home (for remote work and personal usage).

However, it recently started flaking out - sometimes keys simply wouldn't work - on much plugging/unplugging, and smacking the back of the board, they would begin to work again if I got lucky.

I wasn't sure if this was an issue with my GNU/Linux system - it seemed like sometimes resetting the peripherals at the kernel level would do the trick:

cat bin/keyboard-reset.sh

#!/bin/bash

rmmod atkbd ; rmmod serio_raw ; rmmod i8042 ; rmmod psmouse ; rmmod serio ; modprobe serio_raw ; modprobe serio ; modprobe psmouse ; modprobe i8042; modprobe atkbd reset=1

Maybe that little script was pure placebo effect - maybe it made a difference - I don't really know.

In the end, after a month or two of this degraded usage (I use the USB keyboard part time, as I would otherwise stick to the built in keyboard on my beloved Thinkpad W-530), I ended up purchasing a Stoga keyboard that had a pretty neat/unique look, and was cheap enough to serve as a good confirmation that the problem was with the Redragon and not my setup (search: "STOGA Mechanical Keyboard" on Amazon if interested - no affiliate links here - this article isn't trying to sell anything).

Meanwhile, I was also itching for a mechanical keyboard that would be built into a laptop - however, I'm more of a $450 laptop kind of guy, so shelling out $3000 for an Alienware with the Cherry MX builtins was a no go.

In the end, I settled on a hybrid approach - configuring my GNU/Linux system to work extremely well with the keyboard, so that the following happens on plugging up the keyboard:

And the following happens when I remove it:

When all is said and done, this is the result - talk about a perfect fit! (and saving $2500 in the process)

Configuring the setup

The first thing we need to have is a way to detect when the USB plug in has occured. This can be done quite easily with udev rules.

I found a tutorial for this here: http://granjow.net/udev-rules.html - the gist of it is that I ended up using only the symlink creation rule - the other rules had a multitude of issues that I note in the comment block in the full script at the end of this post.

Anyways, creating this file was next on my agenda:

cat /etc/udev/rules.d/42-hello-usb-gaming-keyboard.rules

ATTRS{product}=="Usb Gaming Keyboard", SYMLINK+="helloUsbGamingKeyboard"

Now, whenever the keyboard is added or removed, the symlink is created or deleted at /dev/helloUsbGamingKeyboard

At this point, more complex udev rules can be built, or I can monitor it in userspace using inotify (this is what I settled on).

To monitor the directory for changes, I watch it similar to this:

inotifywait -mr --format '%f %e' -e create,delete /dev -q | xargs -I{} echo {}

This is basically an "echo server" of sorts, and helps to see how you could build something out of inotify (the final script does not use xargs).

It is saved in a file named event-dispatcher.sh, and piped as such in the script itself:

listen_dir=/dev

start () {
    inotifywait -mr --format '%f %e' -e create,delete $listen_dir -q | $0 stdin
}

Now, why wrap in a start() function? Well, because the script is self-contained and handles acting as a daemon to listen for the events, as well as processing/dispatching the various events.

You can see that here:

handle_target() {
    target=$1
    event=$2
    # echo "event-dispatcher(target: $target, event: $event)"

    case "$target" in
        ""                     ) echo Try "$0 start"           ;;
        start                  ) start                         ;;
        helloUsbGamingKeyboard ) usb $event                    ;;
        usb_init               ) usb_init                      ;;
        usb_deinit             ) usb_deinit                    ;;
        init_keybinds          ) init_keybinds                 ;;
        keyboard_off           ) keyboard_off                  ;;
        keyboard_on            ) keyboard_on                   ;;
        # *                    ) echo unknown action "$target" ;;
    esac
}

# Read all this from stdin - pipe only script here
if [[ "$1" == stdin ]]; then
    while read line
    do
        target=$(echo $line | cut -f1 -d' ')
        event=$(echo $line | cut -f2 -d' ')

        handle_target "$target" "$event"
    done < /dev/stdin
else
    handle_target "$1" "$2"
fi

It may seem odd in the main handler to expose functions in the main case statement - I find there was a lot of value in that, as I could leave the script executing, and as new events from inotifywait fork the sub-processes, it is always able to run the most up to date event (and lets me manually call for debugging/building the script).

The final blurb is of a similar nature - it can simulate the CREATE/DELETE scenario when called with input arguments, or when reading from stdin (this is how inotifywait passes the input with the -m flag) it will keep reading in a loop forever.

So, it sees "what" type of event, and of that event, it then chains another case statement:

usb () {
    case "$1" in
        CREATE) $0 usb_init   ;;
        DELETE) $0 usb_deinit ;;
    esac
}

In this case, you can see where it re-executes the script instead of directly calling these other functions. So, when inotifywait generates some output into the stdin similar to:

helloUsbGamingKeyboard CREATE

It knows it's time to hit all the items on the task list above.

That is done via:

init_keybinds () {
    echo About to init keybinds - gotta wait a moment for things to register...

    pkill -9 xcape
    pkill -9 xbindkeys

    # When the usb keyboard exists, we want to flip these around.
    if [[ -L /dev/helloUsbGamingKeyboard ]]; then
        sleep 3
        echo Doing USB mode
        xmodmap -e 'keycode 9 = grave asciitilde'
        xmodmap -e 'keycode 49 = Escape asciitilde'

        # Use sensible scroll direction for touchpad (phone-like)
        xmodmap -e "pointer = 1 2 3 5 4 6 7 8 9 10 11 12"
    else
        sleep 1
        echo Doing normal mode
        xmodmap -e 'keycode 9 = Escape asciitilde'
        xmodmap -e 'keycode 49 = grave asciitilde'

        # Use normal scroll direction for nub
        xmodmap -e "pointer = 1 2 3 4 5 6 7 8 9 10 11 12"
    fi

    echo Initializing keybinds now!

    # Make capslock our left control key
    xmodmap -e 'clear lock'
    xmodmap -e 'keycode 135 = Super_L'
    xmodmap -e 'keycode 0x42 = Control_L'
    xmodmap -e 'add Control = Control_L'

    # And make a tap our Esc, and a hold the full key
    xcape -e 'Control_L=Escape' &
    xbindkeys &
    xset r rate 250 90
}

# https://askubuntu.com/questions/160945/is-there-a-way-to-disable-a-laptops-internal-keyboard
keyboard_off () {
    echo Disabling onboard keyboard

    id=$(xinput --list | egrep "AT Translated" | awk '{ print $7 }' | cut -d'=' -f2)
    keyboard=$(xinput --list | egrep "AT Translated" | awk '{ print $10 }' | sed -e 's/[^0-9]//g')

    # Ensure multiple kb off doesn't ruin the first generated (correct) files here.
    if [[ ! -f "/tmp/kb-id" ]]; then
        echo $id > /tmp/kb-id
        echo $keyboard > /tmp/kb-keyboard
    fi

    # Disable the keyboard via id
    xinput float $id

    # Turn on touchpad, enable click.
    synclient TouchpadOff=0
    synclient TapButton1=1
}

keyboard_on () {
    echo Re-enabling onboard keyboard

    id=$(cat /tmp/kb-id)
    keyboard=$(cat /tmp/kb-keyboard)

    # Re-enable it via stored id
    xinput reattach $id $keyboard

    # Turn off touchpad (we have the thinkpad nub) and disable click.
    synclient TouchpadOff=1
    synclient TapButton1=0
}

usb_init () {
    echo [.:: --- DETECTED USB GAMING KEYBOARD --- ::.]
    notify-send -u normal 'USB Change' 'Added: usb gaming keyboard'
    $0 keyboard_off
    $0 init_keybinds
}

usb_deinit () {
    echo [.:: --- UNDETECTED USB GAMING KEYBOARD --- ::.]
    notify-send -u normal 'USB Change' 'Removed: usb gaming keyboard'
    $0 keyboard_on
    $0 init_keybinds
}

Which is mostly self explanatory with the inline comments.

It leverages a combo of xmodmap/xcape to set up the proper bindings (there is no way I can use Fn+Esc to enter the grave (`) character, given how often it's used in the various lisp-likes I use, and in things like markdown), as well as my long time favorite (re)bind of CapsLock as a dual purpose key (thus restoring where it should be in both Emacs and Vim land).

The xinput stuff handles disabling the built in keyboard, while persisting enough info to restore it on an unplug of the USB keyboard.

It uses notify-send to send out the message to the GUI (requires some handler to be active, I use dunst).

The xset rate stuff is to make the key repeat rate useful.

Comments/discussion

Leave a comment down below at the very bottom of this page (or on reddit/hn).

In particular, leave one recommending my next 60% keyboard I should focus on acquiring :)

I've been eyeing up the Ducky Mini One Two v2, but the Mecha Mini has been sold out, and is $350 on ebay (compared to MSRP of $120 or so, only $20 more than the supposedly more flimsy plastic variant), as well as the Kraken Pro 60% keyboard (something like HHKB is too rich for my blood).

TLDR - Complete script to do this

#!/bin/bash

# Requires binaries: inotifywait, xcape, xbindkeys, xmodmap, xset, synclient

# This will listen on $listen_dir for inotify events of file
# creation/removal, and run the appropriate scripts when they occur.
# This is more easily debugged than the udev scripts, which seem to
# fire off multiple times for no discernible reason (update: I think
# this reason is that the loose match on the /dev events get fired off
# many times, with slight variances on the hidden /dev file that is
# discovered - this script limits it to only the single file name
# match).

# This can be booted, and "watch" for presence of /dev device additions via udev,
# while running the relevant code in userspace.
#
# Sample /etc/udev/rules.d/42-hello-usb-gaming-keyboard.rules file:
#
#    ATTRS{product}=="Usb Gaming Keyboard", SYMLINK+="helloUsbGamingKeyboard"
#
# Where the attrs were found with a combo of "udevadm monitor" and:
#
#     udevadm info -a /dev/usb/hiddev1 | grep -i keyboard

# set -x
listen_dir=/dev

# Watch for events with something like this
# inotifywait -mr --format '%f %e' -e create,delete /tmp -q | event-dispatcher.sh
start () {
    inotifywait -mr --format '%f %e' -e create,delete $listen_dir -q | $0 stdin
}

init_keybinds () {
    echo About to init keybinds - gotta wait a moment for things to register...

    pkill -9 xcape
    pkill -9 xbindkeys

    # When the usb keyboard exists, we want to flip these around.
    if [[ -L /dev/helloUsbGamingKeyboard ]]; then
        sleep 3
        echo Doing USB mode
        xmodmap -e 'keycode 9 = grave asciitilde'
        xmodmap -e 'keycode 49 = Escape asciitilde'

        # Use sensible scroll direction for touchpad (phone-like)
        xmodmap -e "pointer = 1 2 3 5 4 6 7 8 9 10 11 12"
    else
        sleep 1
        echo Doing normal mode
        xmodmap -e 'keycode 9 = Escape asciitilde'
        xmodmap -e 'keycode 49 = grave asciitilde'

        # Use normal scroll direction for nub
        xmodmap -e "pointer = 1 2 3 4 5 6 7 8 9 10 11 12"
    fi

    echo Initializing keybinds now!

    # Make capslock our left control key
    xmodmap -e 'clear lock'
    xmodmap -e 'keycode 135 = Super_L'
    xmodmap -e 'keycode 0x42 = Control_L'
    xmodmap -e 'add Control = Control_L'

    # And make a tap our Esc, and a hold the full key
    xcape -e 'Control_L=Escape' &
    xbindkeys &
    xset r rate 250 90
}

# https://askubuntu.com/questions/160945/is-there-a-way-to-disable-a-laptops-internal-keyboard
keyboard_off () {
    echo Disabling onboard keyboard

    id=$(xinput --list | egrep "AT Translated" | awk '{ print $7 }' | cut -d'=' -f2)
    keyboard=$(xinput --list | egrep "AT Translated" | awk '{ print $10 }' | sed -e 's/[^0-9]//g')

    # Ensure multiple kb off doesn't ruin the first generated (correct) files here.
    if [[ ! -f "/tmp/kb-id" ]]; then
        echo $id > /tmp/kb-id
        echo $keyboard > /tmp/kb-keyboard
    fi

    # Disable the keyboard via id
    xinput float $id

    # Turn on touchpad, enable click.
    synclient TouchpadOff=0
    synclient TapButton1=1
}

keyboard_on () {
    echo Re-enabling onboard keyboard

    id=$(cat /tmp/kb-id)
    keyboard=$(cat /tmp/kb-keyboard)

    # Re-enable it via stored id
    xinput reattach $id $keyboard

    # Turn off touchpad (we have the thinkpad nub) and disable click.
    synclient TouchpadOff=1
    synclient TapButton1=0
}

usb_init () {
    echo [.:: --- DETECTED USB GAMING KEYBOARD --- ::.]
    notify-send -u normal 'USB Change' 'Added: usb gaming keyboard'
    $0 keyboard_off
    $0 init_keybinds
}

usb_deinit () {
    echo [.:: --- UNDETECTED USB GAMING KEYBOARD --- ::.]
    notify-send -u normal 'USB Change' 'Removed: usb gaming keyboard'
    $0 keyboard_on
    $0 init_keybinds
}

usb () {
    case "$1" in
        CREATE) $0 usb_init   ;;
        DELETE) $0 usb_deinit ;;
    esac
}

handle_target() {
    target=$1
    event=$2
    # echo "event-dispatcher(target: $target, event: $event)"

    case "$target" in
        ""                     ) echo Try "$0 start"           ;;
        start                  ) start                         ;;
        helloUsbGamingKeyboard ) usb $event                    ;;
        usb_init               ) usb_init                      ;;
        usb_deinit             ) usb_deinit                    ;;
        init_keybinds          ) init_keybinds                 ;;
        keyboard_off           ) keyboard_off                  ;;
        keyboard_on            ) keyboard_on                   ;;
        # *                    ) echo unknown action "$target" ;;
    esac
}

# Read all this from stdin - pipe only script here
if [[ "$1" == stdin ]]; then
    while read line
    do
        target=$(echo $line | cut -f1 -d' ')
        event=$(echo $line | cut -f2 -d' ')

        handle_target "$target" "$event"
    done < /dev/stdin
else
    handle_target "$1" "$2"
fi

Comment area

Reddit:

https://old.reddit.com/r/programming/comments/pznang/sixty_percent_keyboard/

https://old.reddit.com/r/MechanicalKeyboards/comments/pznbjz/sixty_percent_keyboard/

Hackernews:

https://news.ycombinator.com/item?id=28725635