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.