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:

1 Like

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

1 Like

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

In case you are still bothered by this issue, we’ve made some HID-BPF progress, and it turns out I was wrong, you can solve your issue with HID-BPF starting from kernel v6.3, if your mouse connects through Bluetooth. If you are using the Logitech Bolt receiver it’s going to be harder.

I’ve created a MR with the roughly equivalent mouse I have here: the MX Master 3B.

This MR just adds a new file in src/bpf, which is all you need, except for a few tweaks.

If you want to give it a go:

  • in a terminal, starts sudo hid-recorder on the Anywhere 3, to capture the events Solaar is sending/receiving.
  • open Solaar, and configure the smart shift mouse button to report a middle mouse button.
  • ctrl-C in hid-recorder, and quit solaar.

hid-recorder should give you something roughly like:

# ReportID: 17 /Array ['ff', '09', '3b', '0xff430002', 'c4', '0xff430002', '0xff430002', '52', '0xff430002', '0xff430002', '0xff430002', '0xff430002', '0xff430002', '0xff430002', '0xff430002', '0xff430002', '0xff430002', '0xff430002', '0xff430002']
E: 000024.862989 20 11 ff 09 3b 00 c4 00 00 52 00 00 00 00 00 00 00 00 00 00 00
  • then copy src/bpf/Logitech-MX-Master-3B-middle-button.bpf.c (after checking out my branch on my personal fork)
  • edit your new file with:
    → the HID_DEVICE() provided by udev-hid-bpf list-devices
    → the change in the Feature Index in disable_smart_shift
    → make sure the cid/remap fields are also identical between Solaar and the one from my MX Master 3B
    → make sure the report descriptor shown in hid-recorder shows the block containing the Report ID 0x11 starting at offset 69 (and that it matches the tests in check_bluetooth_report_descriptor())

That should be it for the customization of your mouse.

next step is to try:

cargo build && \
sudo ./target/debug/udev-hid-bpf --verbose add \
      /sys/bus/hid/devices/0005:046D:* \
      Logitech-MX-Master-3B-middle-button.bpf.o

(replace the name of the bpf object, of course)

If you run sudo cat /sys/kernel/debug/tracing/trace_pipe (and then Ctrl-C), you should see the following:

    udev-hid-bpf-120883  [007] ...11 666759.464981: bpf_trace_printk: successfully found Logitech MX Master 3B
    udev-hid-bpf-120883  [007] ...11 666759.614371: bpf_trace_printk: disable smart shift ret value: 20
    udev-hid-bpf-120883  [007] ...11 666759.614381: bpf_trace_printk:  ** ret value: 0

And your mouse should have that extra middle mouse button.

To make this persistent, run sudo ./install.sh at the root of your clone of udev-hid-bpf, and done :slight_smile:

And if this works, please share the file back, so we can add it in the udev-hid-bpf for anyone who needs it too :blush:

1 Like

Cool, thanks for keeping this in mind.

In case you are still bothered by this issue, we’ve made some HID-BPF progress, and it turns out I was wrong, you can solve your issue with HID-BPF starting from kernel v6.3, if your mouse connects through Bluetooth. If you are using the Logitech Bolt receiver it’s going to be harder.

I am using the Logitech Bolt receiver. How much harder is harder? I’m not above buying a USB Bluetooth adapter if that’s the best solution.