Bring Logitech MX Master mouse productivity to Linux without the proprietary desktop software. Map any mouse button to a shell command, an app launch, or a radial Actions Ring — all driven by a single TOML config.
Status: v0.1.0 — feature-complete for v1. 178 tests, runs as a systemd user service. X11 only; Wayland is on the roadmap.
- Maps any Logitech MX button (
BTN_BACK,BTN_SIDE,BTN_EXTRA, etc.) to a shell command, an app launch, or a radial overlay. - Actions Ring — hold a bound button to open a 3-12 segment radial menu at the cursor. Move to a segment, release to fire. Release in the center to cancel.
- App-specific profiles — different bindings depending on the focused
window's
WM_CLASS(browser vs terminal vs editor). - No dual-fire — bound buttons are swallowed via a
/dev/uinputvirtual device so the focused app doesn't also see the click. - Always-on —
logitechmouse install-servicewrites a systemd user unit that auto-starts on login.
-
Linux with X11 (Wayland is post-v1; the ring overlay needs X11 primitives).
-
Python 3.11+.
-
Logitech MX-class mouse with at least one extra button (
BTN_BACK,BTN_SIDE, etc.). Tested against MX Master 3/4. Other models work too — see Hardware notes. -
System packages (Debian / Ubuntu / Pop!_OS):
sudo apt install libxcb-cursor0 xdotool # libxcb-cursor0 — Qt6 xcb platform plugin (required for the ring overlay) # xdotool — needed for app-specific profiles (active-window detection)
On other distros, install the equivalents (Arch:
libxcb,xdotool; Fedora:xcb-util-cursor,xdotool). -
Permissions for
/dev/input/event*and/dev/uinput— see Permissions below.
git clone https://github.com/ChristopherLandaverde/logitechmouse.git
cd logitechmouse
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[ring]" # add ,dev for testsThen write a config (~/.config/logitechmouse/config.toml) — copy
examples/config.toml as a starting point — and run:
logitechmouse listen # foreground (good for first run + debugging)
logitechmouse install-service # one-shot: write + enable a systemd user unitAfter install-service, the listener auto-starts on login and survives
reboots.
You need read access to /dev/input/event* and write access to /dev/uinput.
On most distros, joining the input group covers both:
sudo usermod -aG input $USER
# log out and back inIf /dev/uinput exists but isn't writable by input, drop a udev rule:
sudo tee /etc/udev/rules.d/60-logitechmouse-uinput.rules <<'EOF'
KERNEL=="uinput", GROUP="input", MODE="0660"
EOF
sudo udevadm control --reload-rules && sudo udevadm triggerThe app reads ~/.config/logitechmouse/config.toml. A minimal config:
[actions.screenshot]
type = "command"
command = "flameshot gui"
[bindings.thumb]
trigger = "BTN_BACK"
target = "action:screenshot"A [rings.NAME] table defines a radial overlay with 3-12 segments, each
referencing an [actions.X]:
[rings.thumb_ring]
segments = [
{ action = "screenshot", label = "Shot", icon = "fa5s.camera" },
{ action = "lock", label = "Lock", icon = "fa5s.lock" },
{ action = "files", label = "Files", icon = "fa5s.folder" },
]
[bindings.thumb]
trigger = "BTN_BACK"
target = "ring:thumb_ring"Open the ring on key-down; release on a segment to fire; release in the center to cancel.
[profiles.NAME] overrides global bindings when the focused window's
WM_CLASS matches:
[profiles.firefox]
match_wm_class = "firefox"
[profiles.firefox.bindings.thumb]
trigger = "BTN_BACK"
target = "action:firefox_back"Unmatched triggers fall through to the global [bindings.*] set.
Two presets ship: dark (default) and brazil (yellow/blue). Override
individual colors with #rrggbb or #rrggbbaa:
[theme]
name = "dark"
[theme.overrides]
bubble_active = "#ffdf00"
center_label = "#002776"
# Other keys: bubble, dead_zone, label, label_active, cancelThe LOGITECHMOUSE_THEME=<preset> env var overrides the TOML name — handy
for trying a preset without editing config.
The old action = "X" form is still accepted; the loader maps it to
target = "action:X" and logs a one-line migration note.
Capability bits on Logitech MX devices can lie. For example, on the MX
Master tested here, BTN_TASK is advertised as available but never fires —
the gesture button emits BTN_BACK instead. Behavior varies between
specific models.
To see what your mouse actually emits:
logitechmouse devices # find your event node
sudo ./scripts/dump-keys.py /dev/input/eventNN
# now press every button on the mouseUse the codes you actually see as trigger values in your bindings.
logitechmouse devices # list detected input devices
logitechmouse check-config # validate config and exit
logitechmouse run NAME --dry-run # run a configured action once
logitechmouse listen # start the event listener (foreground)
logitechmouse install-service # install + enable systemd user unitThe listener swallows bound codes via /dev/uinput. If the device isn't
writable, it falls back to non-grab mode and bound buttons reach the focused
app. Fix permissions per Permissions.
Log shows Could not load the Qt platform plugin "xcb" or a core-dump:
install libxcb-cursor0 (or your distro's equivalent) and restart.
sudo apt install libxcb-cursor0
systemctl --user reset-failed logitechmouse.service
systemctl --user start logitechmouse.serviceYou're probably on Wayland. Check echo $XDG_SESSION_TYPE — if it says
wayland, switch to an X11 session at the login screen for now. Wayland
support is on the roadmap.
pip install -e ".[dev,ring]"
pytest # full suite (skips Qt tests if no $DISPLAY)
xvfb-run -a pytest # full suite headless (matches CI)See CONTRIBUTING.md for the contributor workflow.
MIT © Christopher Landaverde.