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-udevdoes the remapping at the kernel input layer. Contrastinput-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 needevtestinstalled. - 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).
Without cloning anything:
curl -fsSL https://raw.githubusercontent.com/eFiniLan/udev-mouse-button-remap/main/remap-mouse-button.py | sudo python3Or 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.pyTo 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 - --listNote: don't reach for
sudo python3 <(curl …). Process substitution hands the script over as/dev/fd/63, butsudocloses every file descriptor above stderr before running a command (closefrom, seeman 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.
Run the editor, remove the mappings you no longer want with r, and save
with s:
sudo ./remap-mouse-button.pyRemoving 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 deviceDelete 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.
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/*.hwdbDelete 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 sessionThe 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_downandscroll_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.
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/trackballPress 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
MSC_SCANpresent → continue.- No
MSC_SCAN→ see Troubleshooting.
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.
/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.
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"
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. 🎉
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.
- No
MSC_SCANfor 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 triggerline and reconnect the device; confirm theATTRS{name}/ modalias match is correct. - Scancode format:
MSC_SCANvalue in hex, lowercase, no0x. - Bluetooth vs USB: the
bXXXXin the modalias differs (b0005BT,b0003USB) — 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.
- 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 needpersist=reopenand 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.hwdbdoing this for laptop hotkeys. The mechanism here is the same one, so typing akey_*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.
python3 -m pytest tests/ -qNo root, no udev and no hardware needed: the suite points HWDB_DIR,
RULES_DIR and SYS_INPUT_DIR at a temp tree.
MIT — see LICENSE.