17

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.

1

5 Answers 5

9

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 !!

6
  • Thanks a lot for the effort and sharing it with us! +1 ... Though I have not tested it yet. BTW I have almost given up on this - would be great if that works as expected :) Jan 6, 2016 at 13:39
  • Your'e welcome ! For me, it works flawlessly. If you have trouble, let me know. I try to make my answer complete, but as I spent almost two days trying to make it work, I may have forget something. Will be glad to help / edit my post. Jan 7, 2016 at 7:37
  • I just realized that ctrl+click1 was a very bad choice to close a tab since it open a link in a new tab. I edited my answer removing last part of the python script and tweaking with xbindkeys, cleaner solution Jan 7, 2016 at 8:21
  • just want to let you know, that I had no time to test it yet - but I'll for sure make it the accepted answer if it works as expected - sorry for the wait - I am a bit busy atm Jan 11, 2016 at 17:14
  • I wonder if we can use above script to debounce a faulty mouse switch. I have a worn-out mouse. I use an autohotkey script to fix it in Windows but there is no tool to fix it in Linux. Here is Autohhotkey script for left button fix autohotkey.com/board/topic/63555-debounce-mouse-keys I hope somebody port it to Linux using python3--evdev
    – kenn
    Nov 20, 2016 at 18:13
6

I found a solution with PyUserInput. This ends up being quite simple and does not require administration rights. With python 2 and PyUserInput installed, I used the following script:

#!/usr/bin/python
from pymouse import PyMouseEvent
from pykeyboard import PyKeyboard

k = PyKeyboard()
class MouseToButton(PyMouseEvent):
    def click(self, x, y, button, press):
        if button == 8:
            if press:    # press
                k.press_key(k.control_l_key)
            else:        # release
                k.release_key(k.control_l_key)

C = MouseToButton()
C.run()

After giving execution rights to the script, I call it with a line in ~/.xsessionrc, for instance

~/path/to/script.py &

Note. this does not prevent the mouse button event from firing. In my case I used xinput set-button-map to change the xinput button mapping and assign the number of the button I was interested in to something that was not in use.

For instance, if you want to use button 8 on your mouse but button 8 has already a function (for instance, page-next), you could use the following .xsessionrc

logitech_mouse_id=$(xinput | grep "Logitech M705" | sed 's/^.*id=\([0-9]*\)[ \t].*$/\1/')
xinput set-button-map $logitech_mouse_id 1 2 3 4 5 6 7 12 9 10 11 12 13 14 15 16 17 18 19 20
./.xbuttonmodifier.py &

provided button 12 carries no meaning to the OS, and assign a custom function to button 12 in .xbuttonmodifier.py, the script I described above.

2
  • But it doesn't prevent the original event from firing. So if I map button 8 to shift, and hold button 8 in Firefox, it also tries to go back to the previous page, which is undesirable.
    – user23013
    Aug 4, 2017 at 13:24
  • 1
    True. To tackle this I used changed the id of the button I was interested in to an used button in xinput. See edited question.
    – Maxim
    Aug 4, 2017 at 13:51
2

I have a partial solution. I haven't figured out how to unmap the existing button, so you end up with a button click and the modifier you want. So if that mouse button has some existing purpose, it will still fire. For example, remaping the right mouse button to a control key will result in a control+click being sent.

Anyway, I found a forum post that is similar to your question for which the answer was to install btnx and configure your modifiers through that. It seems btnx is no longer available through the repo. There is a ppa, but it doesnt work for the latest ubuntu.

Forum post: post: http://ubuntuforums.org/showthread.php?t=1245930

But the source is available:

You could compile it from source, but this will put files on your system that the package manager cannot maintain.

Namely, the following files:

/usr/local/sbin/btnx
/etc/init.d/btnx
/usr/share/pixmaps/btnx.png
/usr/share/btnx-config (directory, multiple files)
/usr/share/applications/btnx-config.desktop
/usr/share/omf/btnx-config/btnx-manual-C.omf
/usr/share/locale/de/LC_MESSAGES/btnx-config.mo
/usr/share/locale/fr/LC_MESSAGES/btnx-config.mo
/usr/share/locale/nl/LC_MESSAGES/btnx-config.mo
/usr/share/locale/ru/LC_MESSAGES/btnx-config.mo

The following symlinks:

/etc/rc0.d/K49btnx -> ../init.d/btnx
/etc/rc1.d/K49btnx -> ../init.d/btnx
/etc/rc6.d/K49btnx -> ../init.d/btnx
/etc/rc2.d/S49btnx -> ../init.d/btnx
/etc/rc3.d/S49btnx -> ../init.d/btnx
/etc/rc4.d/S49btnx -> ../init.d/btnx
/etc/rc5.d/S49btnx -> ../init.d/btnx

So... if you don't mind building from source...

Get the dependencies for btnx:

sudo apt-get install libdaemon-dev git

If you've never built anything from source, you might need build-essential, too:

sudo apt-get install build-essential

Then get and compile btnx:

git clone https://github.com/cdobrich/btnx
cd btnx
./configure
make
sudo make install
cd -

It has a separate GUI configuration tool. Get the dependencies for it:

sudo apt-get install libgtk2.0-dev libglade2-dev

Now get and compile the gui config tool:

git clone https://github.com/cdobrich/btnx-config
./configure
make
sudo make install

Now run the tool:

sudo btnx-config

Click on Detect Mouse Buttons If you want to be able to read the directions while you use the tool, resize the window that pops up, the dialog text gets clipped later if you don't and if you try to resize during detection it will cancel the detection. Just make the window a bit larger.

Click on Press to start mouse detection, then try not to move the mouse until the text changes... Takes about 5-10 seconds. The text will change. When it does, ignore what it says and click Forward.

Click the "Press to start button detection" button

Here you will click one button of your mouse several times (until the status bar fills up). Then set the name of the button to something you will recognize later (ex: LeftButton) Click the Add button.

Repeat this for each mouse button (don't forget scroll wheels, scroll clicks, etc). You can probably skip any buttons you don't want to remap.

When you have added all the buttons, click on OK.

In the main GUI, click on Buttons, in the left pane select teh button you want to remap. It will use the names you entered in the previous steps. For your purposes, you'll want to select just a Key modifier under Key combination on the right.

Don't click delete on this screen, it will remove the button. You'll have to go back and detect the button again if you do.

Go back to the Conrigurations screen and click restart btnx.

Try out the new button.

If you want to uninstall the applicaitons, stop the btnx program and then go into the respective git checked out directories and make uninstall:

sudo /etc/init.d/btnx stop
cd btnx
sudo make uninstall
cd -
cd btnx-config
sudo make uninstall
cd -
3
  • 2
    Thanks a lot for the detailed answer you posted at pastebin. But I am afraid that I am not allowed to use an untrusted ppa or build an application from unknown source without reviewing it in detail on my device. Nevertheless I'll give you a vote up for the effort you've put into it. Apart from that I recommend that you update your answer here and copy what you have written there as this information might be helpful for others but might be overlooked. Finally I have read that the package might even not compile under Ubuntu or Debian - have you tried that actually? May 24, 2015 at 15:47
  • Btw: You can gain additional 100 reputation easily by linking your askubuntu account with other Stack Exchange accounts like e.g. Linux & Unix. May 24, 2015 at 15:59
  • Yeah, I tested it under Ubuntu 14.04. I made the right mouse button send a control key and confirmed that it worked before posting. And then almost abandoned the post because of the reputation requirement.
    – Stephen
    May 24, 2015 at 16:30
0

There's a method for the Logitech mouse, use an unofficial driver named logiops https://github.com/PixlOne/logiops (Respect to the contributors) . I found that this project makes my side buttons perfectly mimic Ctrl and Shift.

Here is my configuration. I'm using the M720 mouse, and the cid:0x56 is the forward button, and the cid:0x53 is the backward button. More help can be found in the wiki of this repo page.

devices: (
{
    name: "M720 Triathlon Multi-Device Mouse";
    buttons: (
        {
            cid: 0x56;
            action =
            {
                type: "Keypress";
                keys: ["KEY_LEFTCTRL"];
            };
        },
        {
            cid: 0x53;
            action =
            {
                type: "Keypress";
                keys: ["KEY_LEFTSHIFT"];
            };
        }
    );
    hiresscroll:
    {
        hires: true;
        invert: false;
        target: false;
    };
}
);
0

Despite being tremendously late, here is my solution. For sure not the most elegant, but it seems to work (xdotool required)

I have changed a mouse button to ctrl pressed for 1 sec, changing my .xbindkeysrc as follows:

# Ctrl button pressed for 1 sec
"xdotool keydown ctrl;sleep 1;xdotool keyup ctrl"
    b:10

# Show desktop
"wmctrl -k on"
    b:11

# Restore desktop windows and release ctrl key
"wmctrl -k off;xdotool keyup ctrl"
    control + b:11
1
  • To be more clear: the 1 sec sleep is to prevent unintentional keypress from causing ctrl key unlimited pressure, in the 'Restore desktop' I force ctrl release anyway
    – Dario Neri
    Mar 12 at 17:02

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .