Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

udev-mouse-button-remap

Remap spare mouse/trackball buttons to other buttons, using the kernel's own udev hwdb mechanism. A thumb button as a second btn_left, a side button as btn_side for browser-back, a spare button as btn_middle so you can hold-to-scroll — any btn_* target works, and a device holds as many mappings as it has spare buttons.

Why this approach

  • Nothing runs afterwards. The script writes two udev files and exits; from then on systemd-udev does the remapping at the kernel input layer. Contrast input-remapper, which keeps a daemon alive to intercept events.
  • Works on Wayland and X11 — it happens below the display server, so xinput's X11-only limitation doesn't matter.
  • No dependencies. Python 3 standard library only. The button's scancode is read straight from /dev/input, so you don't need evtest installed.
  • Desktop button-remap GUIs (e.g. KDE's) usually only bind buttons to keyboard shortcuts, which can't emit a mouse button at all — this can.

Tested on CachyOS/Arch + KDE Plasma (Wayland) with an Elecom HUGE PLUS (Bluetooth).


Quick start

Without cloning anything:

curl -fsSL https://raw.githubusercontent.com/eFiniLan/udev-mouse-button-remap/main/remap-mouse-button.py | sudo python3

Or clone it, if you would rather read it first — which, for anything you run as root, is the better habit:

git clone https://github.com/eFiniLan/udev-mouse-button-remap
cd udev-mouse-button-remap
sudo ./remap-mouse-button.py

To pass a flag over the same pipe, add - so the flag reaches the script instead of the interpreter — python3 --list is an unknown python option:

curl -fsSL https://raw.githubusercontent.com/eFiniLan/udev-mouse-button-remap/main/remap-mouse-button.py | sudo python3 - --list

Note: don't reach for sudo python3 <(curl …). Process substitution hands the script over as /dev/fd/63, but sudo closes every file descriptor above stderr before running a command (closefrom, see man sudoers), so you get /dev/fd/63: No such file or directory. The pipe form above is the one to use: prompts are read from your terminal rather than from stdin, so being piped doesn't break them.

The script lists your input devices, then opens an editor for the one you pick:

Current mappings for HUGE PLUS:
  [ 0] 0x90006  BTN_FORWARD  -> btn_left
  [ 1] 0x90008  BTN_TASK     -> btn_middle

  [a] add a mapping        [r] remove one
  [s] save and apply       [q] quit without saving
>

a asks you to press the button and offers a numbered list of what it can become. Nothing touches /etc until you press s, so you can make several changes, review them, and back out with q. Then reconnect the device.

What you can remap to:

Target What it does
btn_left primary click — gives you a second left button
btn_right context menu
btn_middle middle click, and hold-to-scroll on KDE (default)
btn_side browser back on most desktops
btn_extra browser forward on most desktops
btn_forward explicit forward
btn_back explicit back
btn_task task switcher, where a desktop binds it

Pick one by number, or type any other name that libevdev knows — a key_* target works too, so a spare button can become key_playpause. A name in the wrong shape is rejected rather than written: udev accepts an unknown target silently and then does nothing, which is indistinguishable from a broken remap.

Capture takes the device exclusively (EVIOCGRAB, the same thing evtest --grab does) and holds it until the button comes back up, so neither the press nor its release reaches your desktop. This is not a nicety. Re-capture a button you already mapped to btn_middle without it and you middle-click into whatever has focus — the terminal running the script under sudo — pasting your primary selection into a root shell, where a trailing newline runs it.

Map as many buttons as you like in one session — a as many times as you need, then s once. Re-running later adds to what is already there rather than replacing it.

Command What it does
sudo ./remap-mouse-button.py The editor: add and remove mappings, then save
./remap-mouse-button.py --list Print a device's mappings and exit (no root)
./remap-mouse-button.py --help Usage

Prefer to do it by hand, or the script says your button has no scancode? Follow the manual steps below.


Uninstall

Run the editor, remove the mappings you no longer want with r, and save with s:

sudo ./remap-mouse-button.py

Removing the last mapping for a device drops its record; the files themselves are deleted once nothing of anyone's is left in them. Reconnect the device afterwards.

By hand — the two files are always exactly these:

sudo rm /etc/udev/hwdb.d/70-mouse-button-remap.hwdb
sudo rm /etc/udev/rules.d/71-mouse-button-remap.rules
sudo systemd-hwdb update          # required — without this the old mapping
                                  # lives on in the compiled binary database
sudo udevadm control --reload
sudo udevadm trigger --subsystem-match=input --action=change
# then reconnect the device

Delete those two paths by name, never a glob. 71-*.rules will happily match files that have nothing to do with this project — 71-ti-permissions.rules from Texas Instruments' debug tools, for one.

Upgrading from the shell version

Earlier releases were a bash script that wrote one file pair per device, named after the device rather than after the project:

/etc/udev/hwdb.d/70-<device>.hwdb          e.g. 70-elecom-huge.hwdb
/etc/udev/rules.d/71-<device>-remap.rules  e.g. 71-elecom-huge-remap.rules

Nothing migrates these, by design. They keep working — and keep applying — until you remove them yourself; the current script neither reads nor deletes them, so leaving them in place means two records match the same device. Find them with:

grep -l KEYBOARD_KEY /etc/udev/hwdb.d/*.hwdb

Delete the old pair by name, then re-add your mappings with the new script, which will write them to the two shared files instead:

sudo rm /etc/udev/hwdb.d/70-<device>.hwdb /etc/udev/rules.d/71-<device>-remap.rules
sudo systemd-hwdb update
sudo ./remap-mouse-button.py          # re-add them all in one session

Use case: hold-to-scroll on a trackball

The original reason this exists. Desktops that support "hold a button and move to scroll" often hardcode middle button as the one that does it — so map a spare button to btn_middle, and you get hold the button, roll the ball, scroll. Nice on Elecom trackballs (HUGE, DEFT) and anything else with buttons to spare.

Check your desktop first — you may not need this project for that:

  • GNOME: picks any button as the scroll modifier natively — gsettings set org.gnome.desktop.peripherals.trackball scroll-wheel-emulation-button <n>.
  • Sway / Hyprland / wlroots: set scroll_method on_button_down and scroll_button <code> in your compositor config.
  • KDE Plasma, and anything else that hardcodes middle button with no way to change it: remap a button here, then enable System Settings → Mouse"Hold down middle button and move the mouse to scroll".

That advice is specific to scrolling. For anything else — a second left click, browser back/forward, an extra middle click — none of those desktop settings help, and this project is the answer on GNOME and wlroots too.


How it works, and doing it by hand

1. Identify your device and the button's scancode

The script reads this from /dev/input directly. To check it yourself, evtest is the usual tool (pacman -S evtest, apt install evtest, …) — needed only for this manual route, never by the script:

sudo evtest                # pick your mouse/trackball

Press the target button and note two things:

Event: ... (EV_MSC), code 4 (MSC_SCAN), value 90008      <- SCANCODE (hex)
Event: ... (EV_KEY), code 279 (BTN_TASK), value 1        <- current button

Get the device's modalias, which identifies it for the rule:

cat /sys/class/input/eventN/device/modalias
# input:b0005v056Ep01ACe0108-...   ->  b0005 bus, v056E vendor, p01AC product
#                                       (b0005 = Bluetooth, b0003 = USB)

Note what is not in there: a serial number. Two identical trackballs are indistinguishable to hwdb and will share their bindings.

2. Create the hwdb remap

/etc/udev/hwdb.d/70-mouse-button-remap.hwdb. The property line must start with a space, and the scancode is the MSC_SCAN value in lowercase hex with no 0x:

evdev:input:b0005v056Ep01AC*
 KEYBOARD_KEY_90008=btn_middle

Every device shares this one file, one record each. Per man 7 hwdb, a record runs from its column-0 match line until a blank line or the next column-0 line; # comments are ignored anywhere and never end a record.

3. Force the remap to run on a mouse

Mice are tagged ID_INPUT_MOUSE, not ID_INPUT_KEY, so the default keyboard-remap rules skip them. Add a rule that runs the keyboard builtin for your device — /etc/udev/rules.d/71-mouse-button-remap.rules:

SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="HUGE PLUS", IMPORT{builtin}="hwdb --subsystem=input --lookup-prefix=evdev:", RUN{builtin}+="keyboard"

4. Apply and verify

sudo systemd-hwdb update
sudo udevadm control --reload
sudo udevadm trigger --subsystem-match=input --action=change
# then reconnect the device (toggle Bluetooth / replug) or reboot

sudo evtest    # press the button; it should now report the target you chose
               # e.g. BTN_MIDDLE (274), BTN_LEFT (272), BTN_SIDE (275)

If you mapped it to btn_middle for scrolling, see Use case: hold-to-scroll for the desktop setting that turns it into scrolling. 🎉


Worked example — Elecom HUGE PLUS (Bluetooth)

Device input:b0005v056Ep01AC..., two buttons remapped: "Fn3" = BTN_TASK, MSC_SCAN = 0x90008 for middle-click, and BTN_SIDE, MSC_SCAN = 0x90005 for a second left-click.

/etc/udev/hwdb.d/70-mouse-button-remap.hwdb

# HUGE PLUS - button remaps (managed by remap-mouse-button.py)
#   0x90005  BTN_SIDE  -> btn_left
#   0x90008  BTN_TASK  -> btn_middle
evdev:input:b0005v056Ep01AC*
 KEYBOARD_KEY_90005=btn_left
 KEYBOARD_KEY_90008=btn_middle

The comment header records which physical button each scancode came from, so the mapping list can show something more useful than a hex number.

/etc/udev/rules.d/71-mouse-button-remap.rules

# Mice are ID_INPUT_MOUSE (not ID_INPUT_KEY), so force the keyboard remap builtin.
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="HUGE PLUS", IMPORT{builtin}="hwdb --subsystem=input --lookup-prefix=evdev:", RUN{builtin}+="keyboard"

A second device adds a second record to the same hwdb file and a second line to the same rules file. The script rewrites only its own record and passes everything else through byte-for-byte, including properties you added by hand such as MOUSE_DPI.

Troubleshooting

  • No MSC_SCAN for the button: first re-run and pick a different event node — one mouse often exposes several ("Mouse", "Consumer Control", "Keyboard") and extra buttons frequently sit on a node other than the obvious one. The script names the siblings when capture fails. If none of them emit a scancode, hwdb genuinely cannot remap that button; see Alternatives.
  • Remap didn't apply: re-run the udevadm trigger line and reconnect the device; confirm the ATTRS{name} / modalias match is correct.
  • Scancode format: MSC_SCAN value in hex, lowercase, no 0x.
  • Bluetooth vs USB: the bXXXX in the modalias differs (b0005 BT, b0003 USB) — match what the modalias shows.
  • Double actions: remove any binding set on that button in your desktop's mouse-button GUI, or it fires twice.
  • A hand-written file somewhere else still applies: the script manages exactly the two paths above and does not look for others. A record you wrote yourself in a different file still matches your device, and both apply. Fold it into 70-mouse-button-remap.hwdb, or delete it.
  • python3: command not found: rare on a desktop system, but a minimal install may not have it — pacman -S python, apt install python3.

Alternatives

  • evsieve — a Rust CLI that reads evdev events and re-emits them through uinput. Strictly more capable than hwdb: conditional mappings, one button to a key combination, hotkeys that run scripts, and buttons that emit no MSC_SCAN. The cost is a live process per device, run under systemd. For a Bluetooth device you also need persist=reopen and a udev rule to start the unit on connect, or the remap disappears when the device sleeps.
  • scroll-emulation — middle-button scroll emulation for Wayland compositors that expose no libinput configuration.
  • input-remapper — GUI, but a daemon has to stay running.
  • Remapping a keyboard? Use keyd or xremap. Keyboard remapping is well served and systemd already ships 60-keyboard.hwdb doing this for laptop hotkeys. The mechanism here is the same one, so typing a key_* target does work — but those tools are better at it. Mouse button to mouse button is the gap this project exists for, because desktop GUIs bind buttons to keyboard shortcuts and cannot emit a mouse button at all.
hwdb (this project) evsieve
Running process none one per device, under systemd
Bluetooth reconnect udev reapplies automatically needs persist=reopen + a udev rule
Root at runtime no yes, or /dev/uinput access
Survives reboot yes, by construction only if the unit is enabled
Remaps a button with no MSC_SCAN no yes
Distinguishes two identical devices no yes

Upstream systemd's 70-mouse.hwdb already ships entries for many trackballs, including the Elecom HUGE Plus, but it only sets ID_INPUT_TRACKBALL and MOUSE_DPI — it does not remap buttons. That gap is what this project fills.

NixOS and other declaratively-managed systems: don't use this script. Writing to /etc/udev/hwdb.d is either futile or reverted on rebuild. Use your distribution's own option — on NixOS, hardware.hwdb.files or services.udev.extraHwdb — with the same file contents shown above.

Development

python3 -m pytest tests/ -q

No root, no udev and no hardware needed: the suite points HWDB_DIR, RULES_DIR and SYS_INPUT_DIR at a temp tree.

License

MIT — see LICENSE.

About

Remap mouse/trackball buttons on Linux via udev hwdb — no daemon, works on Wayland and X11. Enables hold-to-scroll on KDE.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages