Linux configuration tool for Pulsar gaming mice.
Plugin architecture — each mouse model has its own protocol driver. Currently supports the Pulsar Xlite Wired, Pulsar X2 Wired, Pulsar X2H Wired Medium, Pulsar X2A Medium Wired, Pulsar Xlite v4, Pulsar Feinmann 8K / FO1, Pulsar wireless mice on the Nordic chipset (21 models), Pulsar X2 CrazyLight, Pulsar X2 Wireless (Areson), and the Pulsar Feinmann F01 Noctua Edition — see Supported Mice below.
Reverse-engineered from USB HID captures of Pulsar Fusion on Windows 11. Wireless (Nordic) protocol based on python-pulsar-mouse-tool by andrewrabert.
| Home | Performance |
|---|---|
![]() |
![]() |
| Colour Picker | Customize |
|---|---|
![]() |
![]() |
| Button Remap | Power |
|---|---|
![]() |
![]() |
| Tools | Input Test |
|---|---|
![]() |
![]() |
| System Tray |
|---|
![]() |
These are the real GUI reading a real mouse, so they only show what that
mouse can do — a wired one has no Power page, and only some models report
link quality or have a fan. If yours shows more, tools/screenshots.py
regenerates the whole set in one run; see
docs/screenshots.md.
| Model | Driver | VID:PID | Status |
|---|---|---|---|
| Pulsar Xlite Wired | xlite_wired |
3710:1401 |
Supported (Sonix, 50 DPI step) |
| Pulsar X2 Wired | x2_wired |
3710:1402 |
Supported (Sonix) |
| Pulsar X2H Wired Medium | x2h |
3710:1403 |
Fully supported |
| Pulsar X2A Medium Wired | x2a |
3710:1404 |
Fully supported |
| Pulsar Xlite v4 | xlite_v4 |
3710:3401 |
Mostly untested (same Sonix protocol); its lift-off register and profile list were read on both connections — it keeps the X3's mode-and-tenths lift-off and its three settings (0.7/1/2 mm, the shortest written and read back off the mouse), has six profiles, and carries the XS-1's DPI range like the rest of the Dongle Kit |
| Pulsar X3 Wired | x3 |
3710:3409 |
Supported (same Sonix protocol as the Xlite v4; battery while charging, sleep timer and Low Power Mode confirmed on hardware on its cable; six profiles, DPI to 32000 in steps of 10, and its own lift-off encoding - 0.7/1/2 mm, as Fusion2 writes it) |
| Pulsar 1K Wireless Dongle | xlite_v4_wireless |
3710:5402 |
Supported (1K wireless dongle shared by the Fusion2 line, same Sonix protocol; battery, charging, sleep timer, Low Power Mode, link quality and which mouse is on the dongle confirmed on hardware with a Pulsar X3; asks which mouse it carries and then describes that one — its profiles, DPI range and lift-off distances, including the X3's 0.7 mm) |
| Pulsar X3 LHD | x3_lhd |
3710:3410 |
Supported (the X3's driver under the left-handed model's own ID and name; settings, stage tables, lift-off and battery while charging confirmed on hardware on its cable) |
| Pulsar 8K Wireless Dongle | fusion2_8k_dongle |
3710:5403 |
Supported (the 1K dongle's driver under its own ID; battery, profiles, DPI stages, lift-off, polling rate up to 8000 Hz and which mouse is on the dongle confirmed on hardware with a Pulsar X3 LHD; it answers that last one under the previous request's header) |
| Pulsar Feinmann 8K / FO1 | feinmann8k |
3710:5404 |
Fully supported (wireless dongle, 8K Hz polling, 6 onboard profiles) |
| Pulsar wireless (Nordic): Xlite, Xlite V3, X2, X2A, X2H, X2 V2 and their Mini/Large/Custom variants | nordic |
3554:f507 3554:f508 3554:f509 |
Supported (Nordic chipset, battery status, sleep timer 10 s–10 min; 21 models share these IDs and the protocol, so one driver serves them all and asks the mouse which it is — only the X2A Wireless, X2A Mini and the two KM=14 X2 Customs have the right-hand thumb pair, and on the rest the driver refuses those two rather than writing to slots their firmware uses for something else) |
| Pulsar X2 CrazyLight Wireless | x2_crazylight |
3710:5406 |
Fully supported, reads and writes confirmed on hardware (Nordic protocol under the Sonix VID, 8K Hz polling, 4 profiles) |
| Pulsar X2 CrazyLight Wired | x2_crazylight_wired |
3710:3414 |
Fully supported, reads and writes confirmed on hardware (same protocol as the dongle, 1K Hz max) |
| Pulsar X2 Wireless (Areson dongle) | x2_areson_wireless |
25a7:fa7c |
Reads and writes confirmed on hardware (Nordic layout; GUI and button remapping tested) |
| Pulsar Feinmann F01 Noctua Edition (dongle) | feinmann_noctua |
3710:5504 |
Fully supported, reads and writes confirmed on hardware (Nordic protocol despite the Feinmann name; fan control; no LEDs; DPI to 42000 at 1 DPI steps) |
| Pulsar Feinmann F01 Noctua Edition (wired) | feinmann_noctua_wired |
3710:7507 |
Fully supported, reads and writes confirmed on hardware (same protocol as the dongle; 8K Hz over the cable) |
| Pulsar X2 Wireless (Areson cable) | x2_areson_wired |
25a7:fa7b |
Reads and writes confirmed on hardware (Nordic layout; 1K Hz max; GUI tested; charging LED overrides RGB effects while cabled) |
Want to add support for your mouse? See Adding a new driver below.
Debian / Ubuntu:
sudo apt install python3-usb python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 gir1.2-dbusmenu-glib-0.4Fedora:
sudo dnf install python3-pyusb python3-gobject gtk4 libadwaita libdbusmenuArch Linux:
sudo pacman -S python-pyusb python-gobject gtk4 libadwaita libdbusmenu-glibOn GNOME you also need the AppIndicator shell extension for the tray icon to appear:
sudo apt install gnome-shell-extension-appindicator # Debian/Ubuntu
# then enable it (Ubuntu):
gnome-extensions enable ubuntu-appindicators@ubuntu.com
# or on other distros:
gnome-extensions enable appindicatorsupport@rgcjonas.gmail.comThen restart GNOME Shell (log out/in, or Alt+F2 → r → Enter on X11).
KDE Plasma & Hyprland support the system tray natively — no extra extensions needed.
Download from the latest release:
# Debian / Ubuntu
sudo dpkg -i pulsar-mouse-linux_*.deb
# Fedora
sudo dnf install ./pulsar-mouse-linux-*.noarch.rpmchmod +x pulsar-mouse-linux-*-x86_64.AppImage
./pulsar-mouse-linux-*-x86_64.AppImage # GUI
./pulsar-mouse-linux-*-x86_64.AppImage --cli # CLIBundles pyusb. Requires system Python 3, PyGObject, GTK4, and libadwaita (the same packages listed in Requirements above). udev rules must be installed separately (see below).
tar xzf pulsar-mouse-linux-*.tar.gz
cd pulsar-mouse-linux-*
sudo ./install.shgit clone https://github.com/packerlschupfer/pulsar-mouse-linux
cd pulsar-mouse-linux
pip install --user -e .A PKGBUILD lives in packaging/, so no AUR account or helper is
needed:
git clone https://github.com/packerlschupfer/pulsar-mouse-linux
cd pulsar-mouse-linux/packaging
makepkg -siIt pulls its dependencies from the official repositories, installs the entry
points to /usr/bin and the udev rules to /usr/lib/udev/rules.d, and upgrades
cleanly with pacman -U on later releases.
To track main instead — worth it if you're testing something from an issue,
since --version reports the last release either way — use
makepkg -p PKGBUILD-git -si, whose package version names the commit. See
packaging/README.md for what changes per release.
(Not on the AUR yet: registration there is paused while they deal with a wave of automated sign-ups. The PKGBUILD is ready to publish the moment it reopens — or sooner, if someone with an existing account would like to maintain it.)
Ships a flake. Run it directly without installing:
nix run github:packerlschupfer/pulsar-mouse-linux # GUI
nix run github:packerlschupfer/pulsar-mouse-linux#cli # CLIOr add it as a flake input:
inputs.pulsar-mouse-linux = {
url = "github:packerlschupfer/pulsar-mouse-linux";
inputs.nixpkgs.follows = "nixpkgs";
};then either reference inputs.pulsar-mouse-linux.packages.${system}.default directly, or pull in inputs.pulsar-mouse-linux.overlays.default and use pkgs.pulsar-mouse-linux as normal. The package ships the udev rules under lib/udev/rules.d/ — on NixOS, add it to services.udev.packages = [ pkgs.pulsar-mouse-linux ]; and they'll be picked up automatically, no manual udevadm steps needed.
Also published as a rolling release on FlakeHub — use this instead of the github: reference if you'd rather resolve through FlakeHub's CDN/registry than GitHub directly:
nix run "https://flakehub.com/f/packerlschupfer/pulsar-mouse-linux/*.tar.gz"inputs.pulsar-mouse-linux = {
url = "https://flakehub.com/f/packerlschupfer/pulsar-mouse-linux/*.tar.gz";
inputs.nixpkgs.follows = "nixpkgs";
};Packages install udev rules automatically. For git installs:
sudo cp udev/50-pulsar-mouse.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm triggerAccess is granted via TAG+="uaccess" (systemd-logind ACL) — no group membership or re-login needed; just re-plug the mouse.
pulsar-mouse-gui # if installed via .deb
# or: PYTHONPATH=src python3 -m pulsar_mouse.guiGTK4 + libadwaita settings window with integrated system tray:
- Auto-detects connected Pulsar mouse and adapts UI to its capabilities
- Reads all settings from the mouse on startup, writes on Apply
- System tray via D-Bus StatusNotifierItem (no GTK3 conflict)
- Tray shows DPI/polling rate on change, quick DPI presets, polling rate radio buttons
- X hides the window (tray stays alive), Quit from tray menu exits
- "Start on Login" toggle on the Tools page for autostart
- "Start in Tray", under Start on Login on the Tools page, leaves the window closed at login. The tray icon or a later launch still opens it. If nothing is hosting a tray, the window opens after a short wait
- Input test dialog with mouse diagram and event log
- Battery level and charging state (on supported wireless mice) shown on the Home page and as a tray icon overlay - polled every 60s, so plugging/unplugging the charging cable can take up to a minute to be reflected. This is deliberate, not a bug: polling more often would mean more frequent USB interface claims and a higher chance of interfering with the mouse's own wireless power-saving sleep.
- Desktop mouse settings (GNOME acceleration profile, pointer speed)
# Show all settings (auto-detects mouse)
sudo pulsar-mouse
# Show one profile
sudo pulsar-mouse --profile 1
# Polling rate (global)
sudo pulsar-mouse --poll 1000 # 125 / 250 / 500 / 1000
# Debounce (global, ms)
sudo pulsar-mouse --debounce 3
# Switches (global: on/off)
sudo pulsar-mouse --angle-snap off
sudo pulsar-mouse --ripple off
sudo pulsar-mouse --motion-sync off
# DPI stages for profile 1 (up to 6 stages)
sudo pulsar-mouse --profile 1 --dpi 400,800,1600,3200
sudo pulsar-mouse --profile 1 --dpi 400,800,1600,3200 --active-stage 2
# Lift-off distance (per-profile)
sudo pulsar-mouse --profile 1 --lod 1 # 1 mm or 2 mm
# LED brightness and effect (per-profile)
sudo pulsar-mouse --profile 1 --brightness 200
sudo pulsar-mouse --profile 1 --led steady
sudo pulsar-mouse --profile 1 --led breathe --breathe-speed 50
# Per-stage LED colour (per-profile, RGB 0-255)
sudo pulsar-mouse --profile 1 --stage-color 1 255 0 0 # stage 1 = red
# Button remapping (per-profile)
sudo pulsar-mouse --profile 1 --button thumb1 dpi+
sudo pulsar-mouse --profile 1 --button thumb1 ctrl+c
sudo pulsar-mouse --profile 1 --button thumb1 vol+ # media keys
sudo pulsar-mouse --profile 1 --button thumb1 'media(0x0192)' # any HID usage (quote it)
# Profile backup/restore (per-profile)
sudo pulsar-mouse --profile 1 --export profile1.json
sudo pulsar-mouse --profile 2 --import profile1.json
# Battery/charging status as JSON, for scripts (wireless mice only)
pulsar-mouse --battery-json
There's a Noctalia shell plugin backed by this CLI — a bar widget, a desktop/lock-screen widget showing battery percentage and charging status, and a quick-controls panel (DPI, polling rate, debounce, angle snap/ripple/motion sync, lift-off distance, LED, and on wireless mice power management, plus a profile switcher on multi-profile mice). Works with just the pulsar-mouse CLI installed; more efficient if pulsar-mouse-gui is also running, since the widgets read its periodic reading instead of polling the mouse themselves.
It used to live in this repo at pulsar-mouse/, but moved to its own repo, pulsar-mouse-noctalia, since noctalia-dev/community-plugins (submitted here) expects each plugin as its own directory with no unrelated code alongside it. See that repo's README for settings and details.
Once the submission is merged, installing it will just be a search away in Noctalia's plugin store. Until then (or for local development), add it as a path source:
noctalia msg plugins source add pulsar-mouse path ~/dev/pulsar-mouse-noctalia # or wherever you cloned that repo
noctalia msg plugins enable harveywuk/pulsar-mouseThen add the bar or battery widget from Noctalia's widget editor.
Off by default. Switch it on under Tools and the app sends one desktop notification when the battery falls to your threshold — once per discharge, not once a minute. Charging clears it, as does recovering five points above the threshold, so a reading sitting on the line doesn't warn twice. "Keep the warning until I dismiss it" asks the desktop never to expire it, at normal urgency, so it still respects Do Not Disturb.
It uses the reading the tray already takes every minute, so nothing polls the mouse more
often, and it only runs while pulsar-mouse-gui does. The warning's settings live in
~/.config/pulsar-mouse/config.json, written only when you change one. Start in Tray
is stored in that file too.
Two ways to read it, both meant for exactly that, so a widget never has to talk to the mouse itself — which matters because only one program can hold a mouse's config interface at a time:
pulsar-mouse --battery-json
{"wireless": true, "battery_percent": 86, "power_connected": false, "battery_mv": 4201}and, while pulsar-mouse-gui is running (the window or just the tray), the same reading
lands in ~/.cache/pulsar-mouse/battery.json on every poll, plus signal_percent and
updated_at. It is written to a temporary file and renamed into place, so a reader always
sees a complete file.
battery_mv is null on most mice: of the drivers here, only the Nordic-protocol ones
report millivolts, and they are worth having where they exist — a percentage often moves
in steps of 5 where the voltage moves continuously and dips under sensor load.
wireless is false, with nothing else, on a mouse that has no battery.
Other projects are welcome to emit the same shape; espulse already does, so one widget can read either.
Each Pulsar mouse model uses a different USB protocol. To add support for a new model:
- Create
src/pulsar_mouse/drivers/yourmodel.py— one driver class per module. Built-in discovery, which the.deband AppImage rely on, keys drivers by module name, so a second class in the same file silently replaces the first. If the mouse shows up under different PIDs on the cable and the dongle, give each connection its own module (seex2_crazylight.py/x2_crazylight_wired.py) - Subclass
PulsarDevicefrompulsar_mouse.base - Define
capabilitiesas a class variable (aDeviceCapabilitiesdataclass) - Implement the protocol methods (
open,close,get/set_polling_rate,get/set_dpi_stages, etc.) - Add an entry point in
pyproject.toml:The entry-point name must match the module name; otherwise a pip install registers the driver twice[project.entry-points."pulsar_mouse.drivers"] yourmodel = "pulsar_mouse.drivers.yourmodel:YourClass"
- Add udev rules for the new VID/PID in
udev/50-pulsar-mouse.rules - Run
PYTHONPATH=src python3 tools/check-drivers.py. CI runs the same checks, which catch mistakes that still compile and import, such as a second driver class in one module, a missing udev rule, or a DPI range the encoder can't express
The CLI and GUI will automatically detect the new driver and adapt their UI.
External driver packages can also register via entry points without modifying this repo.
Need to reverse-engineer the protocol first? See docs/usb-capture-guide.md for step-by-step Wireshark/USBPcap instructions and tools/parse-capture.py to decode the captures.
The GUI has a "Desktop Mouse Settings" section on the Performance page that auto-detects whether GNOME, Hyprland, or KDE Plasma is running and shows the matching controls.
# GNOME (Wayland or X11)
gsettings set org.gnome.desktop.peripherals.mouse accel-profile 'flat'
gsettings set org.gnome.desktop.peripherals.mouse speed 0 # 0 = neutral
# Hyprland - live immediately, but runtime-only; add the input {} block
# below to your hyprland.conf (or a sourced file) to persist it
hyprctl keyword input:accel_profile flat
hyprctl keyword input:sensitivity 0.0
# input {
# sensitivity = 0.0
# accel_profile = flat
# }
# KDE Plasma - written to kcminputrc, requires kwriteconfig5/6
kwriteconfig6 --file kcminputrc --group Mouse --key XLbInptAccelProfileFlat true
kwriteconfig6 --file kcminputrc --group Mouse --key XLbInptPointerAcceleration 0.0You will find advice around to add usbhid.mousepoll=1 to your kernel command
line "for lower latency". Don't, on these mice. This README used to say it
too, which is the reason it is spelled out here.
That parameter is in milliseconds and it overrides what the mouse asks for,
in both directions. These mice already ask for the fastest interval the bus
has — every endpoint on an X2A reports bInterval=1 — so at best the setting
changes nothing, and on a mouse running above 1000 Hz it is a cap: 1 ms is
1000 Hz, where an 8K dongle here was measured sending about 7200 reports a
second.
What to check, if you suspect something is throttling your mouse:
cat /sys/module/usbhid/parameters/mousepoll # 0 is what you want0 means the kernel uses the interval the mouse requests, which is the right
answer for every mouse this app supports. Anything else was set by you or by
your distribution, and on a high-rate mouse it is worth removing from
/etc/default/grub (then sudo update-grub and reboot).
To see what your own mouse asks for, with its bus address from lsusb
(Bus 001 Device 009 below is 1-6.4 or similar in the paths — the easiest
way is to look at all of them and pick out yours):
lsusb | grep -i pulsar
grep . /sys/bus/usb/devices/*:*/ep_*/bInterval01 is one interval, the fastest the bus offers. On this maintainer's X2A
every endpoint reads 01, while the hubs and keyboards around it read 0c.
| Setting | Scope | cat | reg (write/read) | sub |
|---|---|---|---|---|
| Polling rate | global | 0x01 | 0x09 / 0x89 | 0x02 |
| Debounce | global | 0x04 | 0x03 / 0x83 | 0x03 |
| Angle snap | global | 0x07 | 0x04 / 0x84 | 0x02 |
| Ripple control | global | 0x07 | 0x03 / 0x83 | 0x02 |
| Motion sync | global | 0x07 | 0x05 / 0x85 | 0x02 |
| LOD | per-profile | 0x07 | 0x02 / 0x82 | 0x03 |
| DPI stages (bulk) | per-profile | 0x05 | 0x04 / 0x84 | 0x21 / 0x15 |
| DPI active stage | per-profile | 0x05 | 0x01 / 0x81 | 0x02 |
| Stage LED colour | per-profile | 0x05 | 0x05 / 0x85 | 0x05 |
| Brightness | per-profile | 0x03 | 0x03 / 0x83 | 0x03 |
| LED effect | per-profile | 0x03 | 0x04 / 0x84 | 0x0F |
Packet: 64 bytes, Interface 3, HID Feature report (wValue=0x0300). Checksum: bytes[62:64] = LE uint16(sum(bytes[0:62])).
One line each; the detail behind them is in the commits, the issues those link, and the code comments next to whatever each finding explains.
- @packerlschupfer — Original creator and maintainer of pulsar-mouse-linux
- @harveywuk — Feinmann 8K/FO1 driver, multi-profile support, button remapping, desktop pointer-settings integration, Nix flake, GUI redesign, protocol reliability fixes (stale-reply guard, LED read-modify-write, RF settle timing)
- @variasjon — Xlite v4 wireless dongle driver, and the readings on both its connections that gave that mouse its lift-off encoding, its third lift-off setting, its sixth profile and its real DPI range
- @f-o-o-g-s — Pulsar X3 and 1K dongle support, Fusion2 captures and protocol decoding, the dongle's lost-request recovery, write pacing and DPI range, the GUI's event listeners and read-reliability fixes, the profile-change and button-mask reports, the media usages measured past the eight this app named, the screenshot set, and a great deal of hardware testing
- @mwsmws22 — Nordic keyboard shortcut fix (shortcut-table writes, Xlite V3 button mapping)
- @Scout339 — Logo design, wireless mouse testing
- @iamtherobin — Pulsar Fusion USB captures and screen recordings that the X2 CrazyLight drivers (dongle and wired) were decoded from
- @dbergPROTAKT — Pulsar X2 Wireless (Areson IDs) driver, with hardware testing over both the dongle and the cable; the hidraw framing checks that keep another program's replies out of ours, and the swap and retry timing behind the sleeping-mouse fixes
- @martingem — The low-battery warning and Start in Tray, each proposed as a design before any code, plus the fix for a missing session bus leaving the GUI with no first read, no event listener and no battery timer
- @macieqzz — Pulsar X2A Wireless button layout confirmed on hardware (thumb3/thumb4/DPI addresses and function slots)
- @jonkristian — Pulsar X3 LHD and 8K dongle support, decoded from hardware including the dongle's stale-header answer, and the log that settled the link-up/down report
- @bluesceada — Xlite V3: found that it shares the X2A Wireless's USB IDs and differs only in the key matrix, the captures that decoded the CID/MID identity read, the readings that corrected the Nordic firmware string, the Cfg.ini model table, the captures that measured the sleep timer, the two dumps that decoded the media-key record, and the libadwaita 1.5 fallback that fixed the GUI on Ubuntu 24.04
- @Wyatt-Robinson — Feinmann F01 Noctua Edition: USB capture and screen recording the driver was decoded from, plus hardware testing
- meniscus- — Found that a wheel turn over a GUI slider silently changed the setting, on an X2 CrazyLight
- andrewrabert — python-pulsar-mouse-tool, reference implementation for the Nordic wireless protocol
- python-pulsar-mouse-tool — Linux tool for the Pulsar X2 V2 Mini (wireless, battery support)
- espulse — Linux tool for the Pulsar eS line, whose dongles speak a third protocol this app does not: report
0x0ewith ana5magic and an opcode byte, documented in that repo








