Sixty Percent Keyboard

6 min read Original article ↗

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.