How to remap mouse buttons on GNOME with Wayland, without running an extra service?

I have a new Logitech MX Anywhere 3S mouse. Unlike previous mice in this series,[1] the scroll wheel is really difficult to click. It’s assigned as a middle button, but is basically useless. Fortunately, there is a “smart shift” button right behind that, and it’s easy to use that as a middle button.

I know about Solaar as a tool for configuring mouse buttons, and there are others like input-remapper. However, all of these require running an active service. Solaar also doesn’t seem to work for me after reboots. Perhaps there is a race condition or something, but the effect is: I need to go into the config, change the button config away from middle button, and then back, and then it will work.

I’ve looked online for the past few months, and even the Arch documentation points towards one of these tools. That just feels… hacky. I’d like to just drop a config file somewhere that tells GNOME (or Wayland, or is it iBus or some other library? I’ve lost track of how it’s all plumbed together, to be honest). Is this possible?[2]


  1. I loved the Wireless Anywhere Mouse MX, but that thing eats batteries like crazy ↩︎

  2. and, bonus… can someone explain why it’s not possible under the current architecture? In the olden days, Xmodmap could do this for X11… ↩︎

All input events go through libinput. I could not find a way to do what you want, but suspect libinput can do it. Not sure how that is configured, or if it most be done by the DE with API calls.

Peter Hutter and Hans de Goede will know.

1 Like

To add to the complexity (and/or my confusion), the middle mouse button event shows up in evtest, but the “smart shift” click event does not. So whatever is happening needs to happen at a lower level.

Does it show up when you press it with sudo libinput debug-events in the background? I get messages like these when I press the extra buttons on mine (which is not a Logitech):

 event15  POINTER_BUTTON          +0.632s	BTN_SIDE (275) pressed, seat count: 1
 event15  POINTER_BUTTON          +0.831s	BTN_SIDE (275) released, seat count: 0
 event15  POINTER_BUTTON          +1.791s	BTN_EXTRA (276) pressed, seat count: 1
 event15  POINTER_BUTTON          +1.912s	BTN_EXTRA (276) released, seat count: 0

If so, man 4 libinput has a section on Option "ButtonMapping", but unfortunately I’m not sure how to translate this to Wayland and it’s been a long time since I’ve hacked an Xorg.conf. There is a BUTTON MAPPING section that mentions “Use XSetPointerMapping(3) to modify the button mapping at runtime.”

This comment on GitHub might also be relevant, assuming you get an event generated from libinput:

Hi Matthew,

The easiest way to do this is probably to use a local udev rule to make udev update the button mapping at the kernel level.

E.g. to test I just mapped my right mouse button to middle button by creating a /etc/udev/hwdb.d/99-test.hwdb file with the following contents:

evdev:input:b0003v046Dp4091*
 KEYBOARD_KEY_90002=btn_middle

And then I ran:

sudo udevadm hwdb --update
sudo udevadm trigger

To update the .hwdb file for your situation run: sudo evtest and select your mouse. This will print a bunch of info on your mouse, including a line like this:

# Input device ID: bus 0x03 vendor 0x46d product 0x4091 version 0x111

Note that the bus, vendor and product numbers printed are what you need for the first line in the .hwdb file:

evdev:input:b0003v046Dp4091*

Also note that all fields need to be 4 chars with and for this line a-f from hex numbers need to be uppercase, so 0x46d046D .

To get the scancode to remap the “smart shift” button press it while evtest is still running this will print a line like this:

E: 45.423958 0004 0004 589826	# EV_MSC / MSC_SCAN             589826

Converting the last number (589826 in the example) to hex you then get the value to use after KEYBOARD_KEY_ e.g. the 589826 for the right button from my example is 0x90002 so the line to map it to the middle mouse button becomes:

 KEYBOARD_KEY_90002=btn_middle

Note this line starts with a space, where as the first line must not be indented.

After creating the .hwdb file do not forget to run the 2 commands from above to apply the changes.

I hope this helps.

Regards,

Hans

p.s.

One important last remark a-f hex characters in the second line of the .hwdb file need to be lowercase. Yes first line needs a-f in hex uppercase, second line needs them in lowercase, really.

There’s this utility that allows to map events at the evdev layer: https://github.com/KarsMulder/evsieve

It’s not available on Fedora but I’ve been meaning to package - maybe it does some of what you want.

The “Smart Shift” button does not generate any messages — unless I use Solaar to remap it to Middle Button, in which case it does.

Unfortunately, evtest doesn’t see it either. So it may be that this mouse is a special case.

With hid-recorder from hid-tools, I get this for a left mouse button click and release (extra line added between the two for clarity):

# ReportID: 16 /Vendor Defined Page 1 ['01', '49', '03', '00', '54', '00'] 
E: 000000.000000 7 10 01 49 03 00 54 00
# ReportID: 16 /Vendor Defined Page 1 ['01', '49', '03', '00', '08', '00'] 
E: 000000.005956 7 10 01 49 03 00 08 00
# ReportID: 2 / Button: 1  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0 | X:     0 | Y:     0 | Wheel:    0 | AC Pan:    0 
E: 000000.007955 8 02 01 00 00 00 00 00 00

# ReportID: 2 / Button: 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0 | X:     0 | Y:     0 | Wheel:    0 | AC Pan:    0 
E: 000001.931985 8 02 00 00 00 00 00 00 00
# ReportID: 16 /Vendor Defined Page 1 ['01', '49', '03', '00', '54', '00'] 
E: 000001.979979 7 10 01 49 03 00 54 00
# ReportID: 16 /Vendor Defined Page 1 ['01', '49', '03', '04', '1a', '00'] 
E: 000002.142031 7 10 01 49 03 04 1a 00

However, for the smart-shift button, I get this:

# ReportID: 17 /Vendor Defined Page 1 ['01', '0e', '10', '01', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00'] 
E: 000002.233980 20 11 01 0e 10 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

# ReportID: 16 /Vendor Defined Page 1 ['01', '49', '03', '00', '54', '00'] 
E: 000003.997975 7 10 01 49 03 00 54 00
# ReportID: 16 /Vendor Defined Page 1 ['01', '49', '03', '04', '1a', '00'] 
E: 000004.159970 7 10 01 49 03 04 1a 00

I’m trying to see if anything happens as I change the setting in Solaar, but (somewhat ironically!) Solaar’s UI is not keyboard friendly, so I need to get a second mouse to be able pull that out of the haystack of motion events!

Also, looking more carefully, I notice that the fourth value in ReportID: 17 changes each time I hit “smart shift” — from 00 in smooth-scroll mode to 01 in wheel-ratchet mode. (And this change happens on button-down, not release.)

That suggests to me that this is a report of mouse configuration somehow, and that something else needs to be sent to change that configuration…

Alright. A little bit on background on this mouse and some other Logitech ones.

A little bit more than 12 years ago I started working on the Logitech HID++ protocol at Logitech’s request. Basically, they thought that standard HID was not enough for their need and created a rather complex protocol on top of it.

For the curious, they opened part of their spec[1] and I wrote the first iteration of the hid-logitech-hidpp.ko kernel driver. Solaar then came in and is also “supported” by Logitech (i.e. they are in good terms and Logitech can share specs to the developers when they need).

What matters here is that they are using 2 Logitech specific report IDs: 0x10 and 0x11. And you are seeing them here.

So every time you get those reports, you have to follow the entire spec to make sense out of it (Solaar does it for you and the kernel too to some functions).

So as you discovered, when you receive ReportID: 17 /Vendor Defined Page 1 ['01', '0e', '10', '01', ... this is a notification from the mouse to tell you the state of the “smart shift” mode. And also, as you realized there is no “release” event as this is a state of the mouse that changed, not a button notification.

Luckily, the function 0x1b04 (and probably another) allows to remap or divert any button.

By using “divert” on the smart shift button, you’ll get the notification from the mouse on the HID++ protocol that the button has been pressed/released. However, that doesn’t change the mouse behavior and the ratchet mode on the wheel will also be enabled/disabled every time you click on the button, which is not what you want. (Also this will be on a private channel, the HID++ one which is not reported as mouse events by the kernel).

By remapping the button, this is exactly what you want: the smart shift mode is disabled on the mouse, and the button is remapped to middle click by the mouse itself.

This is nice, but those mice are not capable of storing their current state when powered off. For that you need a mouse from the gaming series. Which means that every time the mouse disconnects or if you reboot your machine, you need to set the mouse to the proper mode.

And just to be clear: this is a mouse/vendor specific that is completely independent of the software stack we use on Linux. We just don’t get enough information from the mouse to be able to remap the smart shift in the upper layers. We have to send a command to the mouse for it to behave “properly”.

So, what can be done?

  • rely on Solaar all the time (it’s a daemon, and it’s buggy in your case, so it needs fixes)
  • or listen to what Solaar sends to the device, and write a small python (or whatever) script that sends that exact command when triggered by a udev rule when the mouse connects[2].

Luckily, your mouse is connecting only though Bluetooth, and so I think you don’t need the full daemon because the mouse should reconnects when we need to send the command.

In a private message you asked me whether HID-BPF would work for that use case, and I don’t think it’s worth it: we can configure the mouse to do the exact thing you want, and we have to configure the mouse anyway.

HID-BPF reacts only to mouse events[3]. You’ll need a userspace helper to send back information to the mouse as we need to be in a separate context than the one from the event[4]. So a small python script will be much easier.


  1. HID++ public spec ↩︎

  2. this allows to ignore what the full protocol is, and all the requests you have to send to get to that single command ↩︎

  3. in the future, it should also reacts to users attempting to access the device ↩︎

  4. we are in IRQ context when we receive an event, and to send back a command, we need to be outside of any IRQ or things will go pretty badly. The simplest right now is to use a separate context from the userspace application that handles the HID-BPF program, but maybe in the future we will be able to delay the call in a workqueue, and be in a separate context. But this is kernel work, and it takes time to get it right ↩︎

1 Like