Ubuntu – How to map modifiers (e.g. CTRL) to mouse thumb buttons using xbindkeys

mousexbindkeysxdotoolxte

This question has already been asked but was never answered properly. After clearance with @Seth I am now asking it again. This will allow me to respond and possibly modify the question a lot easier. The original question can be found here:

Map Ctrl and Alt to mouse thumb buttons


Issue:

Though it is very simple to map any keystrokes to a mouse button using xbindkeys in conjunction with xdotool or xte it seems a lot more problematic to map a modifier key (e.g. ALT, CTRL, SHIFT etc.) to it.

The final soloution should allow i.a. a CTRL + click (e.g. for selecting multiple entries of a list) with just the mouse.

A couple of possible approaches to solve this can be found here at Stack Exchange as well as at other Linux related forums. But none of those work as expected as they lead to other issues and side effects.

Notes:

Some of the examples below involve Guile with Scheme syntax and rely on .xbindkeysrc.scm file whereas others rely on the .xbindkeysrc file with its respective syntax. I am aware that they won't work together.

Furthermore the below snippets rely on xdotool only but I am open to approaches involving other applications like e.g. xte as well – though it seems both lead to the same results and therefore I am using just xdotool actions here.

Approach A:

Updating the .xbindkeysrc file with:

"xdotool keydown ctrl"
  b:8

"xdotool keyup ctrl"
  release + b:8

That's what I initially tried but it has the side-effect that the modifier is being held and can not be released.

Approach B:

Updating the .xbindkeysrc.scm file with:

(xbindkey '("b:8") "xdotool keydown ctrl")
(xbindkey '(release "b:8") "xdotool keyup ctrl")

(xbindkey '("m:0x14" "b:8") "xdotool keydown ctrl")
(xbindkey '(release "m:0x14" "b:8") "xdotool keyup ctrl")

Found at http://www.linuxforums.org/forum/hardware-peripherals/169773-solved-map-mouse-button-modifier-key.html and tries to address the issue where the modifier is being held (as described at approach a).

Though it fixes that it does only work partially as it is not possible to perform other mouse clicks while the thumb button is pressed.

Approach C:

Updating the .xbindkeysrc file with:

"xdotool keydown ctrl"
  b:8

"xdotool keyup ctrl"
  release + control + b:8

Tried out by OP of the linked question here at askubuntu. A lot simpler and more solid as it does not involve modifier states. Nevertheless the issue remains, i.e. a CTRL + click is not possible.

It seems that xbindkeys itself is the problem here as it recognizes the click but won't execute it. This can be tested using xev | grep button and xbindkeys -v:

A normal mouse click as recorded by xev should look like:

state 0x10, button 1, same_screen YES
state 0x110, button 1, same_screen YES

As well as for the thumb button:

state 0x10, button 8, same_screen YES
state 0x10, button 8, same_screen YES

But when having the above xbindkeys configuration enabled it does not record anything. Though it makes sense for the thumb button as it is mapped to CTRL and therefore is not a mouse button anymore it is strange that button 1 is not recorded as well. This is likely because xbindkeys does not execute it but itself is recognizing it:

Button press !
e.xbutton.button=8
e.xbutton.state=16
"xdotool keydown ctrl"
    m:0x0 + b:8   (mouse)
got screen 0 for window 16d
Start program with fork+exec call
Button press !
e.xbutton.button=1
e.xbutton.state=20
Button release !
e.xbutton.button=1
e.xbutton.state=276
Button release !
e.xbutton.button=8
e.xbutton.state=20
"xdotool keyup ctrl"
    Release + m:0x4 + b:8   (mouse)
got screen 0 for window 16d
Start program with fork+exec call

Approach D:

Updating the .xbindkeysrc file with:

"xdotool keydown ctrl"
  b:8

"xdotool keyup ctrl"
  release + control + b:8

"xdotool click 1"
  b:1

Just too simple … but leads to an infinite loop of clicks.


UPDATE:

In the meantime I've bought a Logitech G502 and noticed that once configured via the driver on Windows not only the profile itself is stored on the device memory but the actual keypress is done by the mouse. That in fact solved my problem on Linux!

The only other mouse I remember that was able to do that was the Razer Copperhead back in the days. But I guess there are other mice available today which can do the same.

Best Answer

I spent a lot of time trying to make that binding work. I eventually found a solution, which is complicated but works well and doesn't imply third party software. I share it here hoping it will help people. Besides, I know this is not perfect in terms of security, so any constructive feedback is more than welcome.

There are solutions who are really nice, like the one proposed here, but It always suffer from the limitation of xbindkeys who grab the entire mouse, making modifers+mouse click mapping uncertain. Plus the guile based solution from the above link use ctrl+plus/ctrl+minus which isn't recognize by Gimp for example.

I figured out that what we want is a mouse button who act as a keyboard, so I used uinput, who can be accessed via python, wrote a script that monitor /dev/my-mouse for the thumb button click and send the ctrl key to the virtual keyboard. Here are the detailed steps :

1. Make udev rules

We want the devices to be accessible (rights and location).

For the mouse :

/etc/udev/rules.d/93-mxmouse.conf.rules
------------------------------------------------------------
KERNEL=="event[0-9]*", SUBSYSTEM=="input", SUBSYSTEMS=="input", 
ATTRS{name}=="Logitech Performance MX", SYMLINK+="my_mx_mouse", 
GROUP="mxgrabber", MODE="640"

Udev will look for a device recognized by the kernel with names like event5, and I select my mouse with the name. The SYMLINK instruction assure I will find my mouse in /dev/my_mx_mouse. The device will be readable by a member of the group "mxgrabber".

To find information about your hardware, you should run something like

udevadm info -a -n /dev/input/eventX

For uinput :

/etc/udev/rules.d/94-mxkey.rules
----------------------------------------------------
KERNEL=="uinput", GROUP="mxgrabber", MODE="660"

No need for symlink, uinput will always be in $/dev/uinput or $/dev/input/uinput depending on the system you're on. Just give him the group and the rights to read AND write of course.

You need to unplug - plug your mouse, and the new link should appear in /dev. You can force udev to trigger your rules with $udevadm trigger

2. Activate UINPUT Module

sudo modprobe uinput

And to make it boot persistant :

/etc/modules-load.d/uinput.conf
-----------------------------------------------
uinput

3. Create new group

sudo groupadd mxgrabber

Or whatever you have called your access group. Then you should add yourself to it :

sudo usermod -aG mxgrabber your_login

4. Python script

You need to install the python-uinput library (obviously) and the python-evdev library. Use pip or your distribution package.

The script is quite straightforward, you just have to identify the event.code of you button.

#!/usr/bin/python3.5
# -*- coding: utf-8 -*-

"""
Sort of mini driver.
Read a specific InputDevice (my_mx_mouse),
monitoring for special thumb button
Use uinput (virtual driver) to create a mini keyboard
Send ctrl keystroke on that keyboard
"""

from evdev import InputDevice, categorize, ecodes
import uinput

# Initialize keyboard, choosing used keys
ctrl_keyboard = uinput.Device([
    uinput.KEY_KEYBOARD,
    uinput.KEY_LEFTCTRL,
    uinput.KEY_F4,
    ])

# Sort of initialization click (not sure if mandatory)
# ( "I'm-a-keyboard key" )
ctrl_keyboard.emit_click(uinput.KEY_KEYBOARD)

# Useful to list input devices
#for i in range(0,15):
#    dev = InputDevice('/dev/input/event{}'.format(i))
#    print(dev)

# Declare device patch.
# I made a udev rule to assure it's always the same name
dev = InputDevice('/dev/my_mx_mouse')
#print(dev)
ctrlkey_on = False

# Infinite monitoring loop
for event in dev.read_loop():
    # My thumb button code (use "print(event)" to find)
    if event.code == 280 :
        # Button status, 1 is down, 0 is up
        if event.value == 1:
            ctrl_keyboard.emit(uinput.KEY_LEFTCTRL, 1)
            ctrlkey_on = True
        elif event.value == 0:
            ctrl_keyboard.emit(uinput.KEY_LEFTCTRL, 0)
            ctrlkey_on = False

5. Enjoy !

All you need now is make your python file executable, and ask your desktop manager to load the file at startup. Maybe also a glass of wine to celebrate the good work !

6. Extra for free

I use xbindkeys for additional behavior. For instance, the following configuration may be nice if you have a mouse with wheel side clicks :

~/.xbindkeysrc
---------------------------------------------
# Navigate between tabs with side wheel buttons
"xdotool key ctrl+Tab"
  b:7
"xdotool key ctrl+shift+Tab"
  b:6

# Close tab with ctrl + right click
# --clearmodifiers ensure that ctrl state will be 
# restored if button is still pressed
"xdotool key --clearmodifiers ctrl+F4"
  control+b:3

For this last combinaison to work, you must disable the button you configured for the python script, otherwise it will still be grabed by xbindkeys. Only the Ctrl key must remain :

~/.Xmodmap
-------------------------------------------
! Disable button 13
! Is mapped to ctrl with uinput and python script
pointer = 1 2 3 4 5 6 7 8 9 10 11 12 0 14 15

Reload with $ xmodmap ~/.Xmodmap

7. Conclusion

As I said in the beginning, I'm not perfectly happy with the fact that I have to give myself the wrights to write to /dev/uinput, even if it's thought the "mxgrabber" group. I'm sure there is a safer way of doing that, but I don't know how.

On the bright side, it works really, really well. Any combinaison of keyboard or mouse key how works with the Ctrl button of the keyboard now works with the one of the mouse !!