Monthly Archives: June 2018

Cool Mouse Stuff with Linux

Linux has built-in tools for programming the buttons on your mouse or trackball. You don’t need any drivers or proprietary software. Just a few simple tools you can install from the repos. I’m doing this on Ubuntu 16 and 18, but it should work on any recent Linux distro.

First, use xev to detect how your system is recognizing the mouse buttons. Start xev from a command prompt, then hover over its window, click each button, and look at the text in the command prompt window. This will tell you the # for each button. The first 3 buttons are always defined the same way: left-click is button 1, middle-click 2, right-click 3. Extra buttons are “non-standard” and can be defined in any way.

To change what a button does, use xbindkeys. Its control script is a file in your home directory called .xbindkeysrc. The app xte fabricates keystrokes and mouseclicks.

For example: here’s an .xbindkeysrc that does 2 things:

  • Single click of button 2 (middle button) causes double-click of button 1 (left)
  • Single click of button 9 causes Control-click of button 1.
# button 2 (scroll wheel) is double click
"/usr/bin/xte 'mouseclick 1' 'mouseclick 1'"
b:2 + release

# button 9 (upper left alternate) is control + left click
"/usr/bin/xte 'keydown Control_L' 'mouseclick 1' 'keyup Control_L'"
b:9 + release

This should work everywhere in all apps. It does work in some apps (like Thunar), but not in others (like Firefox). I wondered – why?

Limitation 1

After experimenting, I discovered that xte is too fast for some apps. I added a few strategically located brief (10 ms) delays and it worked everywhere.

Example: change the above to this:

# button 2 (scroll wheel) is double click
"/usr/bin/xte 'mouseclick 1' 'usleep 10000' 'mouseclick 1'"
b:2 + release

# button 9 (upper left alternate) is control + left click
"/usr/bin/xte 'keydown Control_L' 'mousedown 1' 'usleep 10000' 'mouseup 1' 'keyup Control_L'"
b:9 + release

The usleep param is in microseconds, so 10,000 is 10 milliseconds. This now works in all apps.

Limitation 2

Another limitation: it’s hard to make the same button duplicate itself from xbindkeys. If you do this, it can lead to infinite loops. For example: xbindkeys can easily make a single click of button 2, cause a double-click of button 1. But xbindkeys can’t make a single click of button 1, cause a double-click of the same button. This will fail or cause an infinite loop.

For that, use imwheel. Originally intended to adjust the sensitivity of the scroll wheel, it is also useful for other things. This is because internally, a scroll wheel is just 2 buttons: up and down. To make it more sensitive, imwheel duplicates each click of those buttons. But imwheel can do this with any buttons, not just the scroll wheel. Its config file is .imwheelrc, in your home directory.

Here’s an imwheel config that doubles the scroll wheel sensitivity:

# Apply to all apps (individual apps can be customized)
".*"
# Amplify scroll up/down
None,    Up,    Button4,    2
None,    Down,    Button5,    2
# Amplify scroll left/right
None,    Left,    Button6,    10
None,    Right,    Button7,    10

On my trackball, the scroll wheel is Button 4 and Button 5. The scroll wheel tilts L and R; these are buttons 6 and 7. Yours may be different; check it with xev.

Elecom Trackball, IOGear GCS1102 KVM Switch, Ubuntu 18

My Logitech Trackman thumb trackball is so old it has a PS/2 plug. Its buttons are finally starting to fail and I needed to replace it. But Logitech doesn’t make wired trackballs anymore. I must have wired because (1) I use an IOGear GCS1102 KVM Switch, and (2) I prefer the instantaneous smoother response of wired devices.

I Googled around and discovered the Elecom M-XT3URBK, worth a try. At first it didn’t work. On a whim, I checked my GCS1102 (open a text editor, hit Ctrl-F12, press F4, and it prints its settings) and found it was in mouse emulation mode. I disabled mouse emulation mode (Ctrl-F12, press m) and the Elecom worked perfectly. Xev detected all buttons except the far right, which I don’t need anyway.

Good stuff:

  • Tracks at least as well (smooth, fast, accurate) as the Logitech Marble
  • Switch-adjustable DPI (low is still pretty high and works best)
  • Buttons
    • Standard L and R
    • Scroll wheel clicks like a button (without occasionally moving, like the Logitech did)
    • Scroll wheel clicks L and R to scroll horizontally
    • Two small buttons to the L of the L button
  • It’s comfortable to use
  • Elecom makes a mirror image left-handed version
  • Price: only $40 – half Logitech’s prices!

Bad stuff:

  • The cable is a bit on the short side, but still long enough I didn’t need an extension.
  • It’s a bit small for my large hands

So far, no problem. It works, scroll wheel and all buttons, without any drivers. Just plug and play. But there’s more…

SENSITIVITY and ADJUSTMENT

The Elecom is so much more sensitive than my old Logitech, I had to turn down the settings. But with the GCS1102’s mouse emulation disabled, every time I switch, each computer sees the mouse being un-plugged and re-plugged. When this happens, Ubuntu doesn’t restore the mouse settings. When I switch away and back, the mouse is back to its hyper-sensitive default setting.

I wrote a shell script /apps/bin/setMouse.sh that uses xinput to set the mouse:

#!/usr/bin/env bash
# The UI for setting mouse track speed in Ubuntu 18 is broken.
# Do it manually here
# NOTE: as of June 2018, the mouse was device 12.
# You can check this with: xinput --list --short
# To show device settings: xinput --list-props 12

echo "`date`, $USER, $DISPLAY, $XAUTHORITY" >> /apps/bin/mouse.log

if [ -z "$1" ]
then
    a="0.1"
else
    a="$1"
fi
if [ -z "$2" ]
then
    m="0.4"
else
    m="$2"
fi
if [ -z "$3" ]
then
    d="`xinput | grep Mouse | grep pointer | cut -f 2 | cut -c4-5`"
else
    d="$3"
fi

aprop=`xinput --list-props $d | grep 'Accel Speed (' | cut -f2 | cut -c23-25`
if ! [ -z "$aprop" ]
then
    xinput set-prop $d $aprop "$a"
fi
mprop=`xinput --list-props $d | grep 'Coordinate Transform' | cut -f 2 | cut -c35-37`
if ! [ -z "$mprop" ]
then
    xinput set-prop $d $mprop "$m", 0.0, 0.0, 0.0, "$m", 0.0, 0.0, 0.0, 1.0
fi

echo "`date` Mouse set up $a $m $d" >> /apps/bin/mouse.log
xinput --list-props "$d" >> /apps/bin/mouse.log

But I don’t want to have to run this script every time I switch the GCS1102. The Linux Udev system should do the job!

UDEV RULES

First, I needed a Udev rule to trigger my script. The rule must detect the USB device being plugged in, so I need its vendor and product IDs. To get these, run lsusb:

Bus 003 Device 122: ID 056e:00fb Elecom Co., Ltd

Now add a Udev rule. Mine was /etc/udev/rules.d/61-elecom-mouse.rules:

SUBSYSTEM=="usb", ATTR{idVendor}=="056e", ATTR{idProduct}=="00fb", ACTION=="add", RUN+="/apps/bin/setMouse.sh"

But it didn’t work. The script was running (I could see the log entry it creates). But the mouse sensitivity wasn’t being set.

DELAYED EXECUTION

My first thought was that the Udev event was firing before the device was registered and ready to be configured. So I added a sleep to the command. But, you can’t sleep while Udev is running your command. So I had Udev run a script that returns immediately, while sleeping a few seconds then running setMouse. A command like this:

(sleep 3 ; /apps/bin/setMouse.sh) &

But that didn’t work. It worked from the command line, but when run from Udev, the background was ignored and Udev waited for the entire time. That’s bad for 2 reasons: (1) hangs Udev for 3 seconds, (2) Udev is still waiting, so the sleep is pointless because the device won’t be ready until Udev is done.

In short, Udev is too smart by half. I needed a way to trick Udev into returning immediately, while my script pauses a few seconds then runs. I found this with the at command. First, install it:

sudo apt install at

Then, make the Udev rule look like this:

SUBSYSTEM=="usb", ATTR{idVendor}=="056e", ATTR{idProduct}=="00fb", ACTION=="add", RUN+="/usr/bin/at -M -f /apps/bin/setMouseElecom.sh now"

This tells at to trigger the script to run immediately, but at returns to Udev immediately. The script doesn’t run under Udev but under at. The setMouseElecom.sh script looks like this:

#!/usr/bin/env sh
# Called by Udev when Elecom trackball is plugged in
sleep 2
/apps/bin/setMouse.sh 0.1 0.4

Note: at can only run sh scripts, not bash.

But, it still didn’t work. The script ran, but its xinput commands always failed.

XINPUT AND XSESSION

I guessed it might be that at, running as root, didn’t have permission for my X session. I had to somehow make it join my X session before the xinput commands would work. I made a script called joinXandRun.sh:

#!/usr/bin/env bash

# Get the number of the active terminal (requires root)
TTY=$((`fgconsole`-7))

# Set env vars needed to simulate the user
export DISPLAY=:${TTY}.0
USER=`who -s | grep "(:$TTY)" | cut -d' ' -f1`
export XAUTHORITY=/home/$USER/.Xauthority

echo "`date`, $USER, $DISPLAY, $XAUTHORITY: $1" >> /apps/bin/joinXandRun.log
bash -c "$1 $2 $3 $4 $5 $6 $7 $8 $9"

This script finds the active X console, and its user and display. Then it sets the critical DISPLAY  and XAUTHORITY environment vars to “join” that X session. Then it runs another script passed into it.

Then I made setMouseElecom.sh call this script:

#!/usr/bin/env sh
# Called by Udev when Elecom trackball is plugged in
sleep 2
/apps/bin/joinXandRun.sh /apps/bin/setMouse.sh 0.1 0.4

Guess what — this worked!

The really cool thing about this is that the Udev rule is device-specific, and setMouse.sh takes parameters. I could set this up to auto-detect and configure several different kinds of mice or trackballs. Each would have a different Udev rule, matching different vendor and device IDs, firing different scripts that pass different mouse setting params to setMouse.sh. This will be useful for my laptop, which uses the Elecom at work but another Logitech Trackman at home.

It turns out all the pieces are essential. Lessons learned:

  • When Udev triggers the add event, the device being added cannot yet be configured. So if your Udev script configures the device, it has to:
    • Return immediately to Udev, so Udev can finish its processing
    • Meanwhile, trigger its config processing to run after a few seconds
  • To configure devices that are part of the X session, you need to “join” the session. Otherwise, the X system won’t let your code configure the device.

PS: none of this is specific to Ubuntu 18. It should work on any recent Linux distro.