It's a mouse,
not a platform.
A nibble is half a byte. This one is an under-1-MB download that runs your Logitech mouse — native settings window, menu bar, or terminal. No daemon, no account, no telemetry.
$ nibble status device G502 LIGHTSPEED · receiver 046D:C539 protocol HID++ 4.2 · link ok battery 63% · 3.92 V · charging dpi 1600 rate 1000 Hz mode host $ nibble dpi 3200 dpi 3200 ✓ read back from device $ nibble rate rate 1000 Hz (supported: 1000/500/250/125 Hz) $ nibble battery 63% 3.92V charging $ nibble doctor Nibble doctor v1.8.1 ✅ input-monitoring granted ✅ transports receiver 046D:C539 ✅ device G502 LIGHTSPEED · HID++ 4.2 ✅ battery 63% charging (0x1001) ✅ remap-capable 0x8110 MouseButtonSpy ✅ config ~/.config/nibble.json · 2 maps · profile Gaming ✅ remap-engine running · 2 mapping(s) · 0x8110 spy ✅ menubar running (remap engine host) • login-startup on • login-replay installed • low-battery-alert below 20% All good. $
G HUB is a 4 GB install. Nibble is under 1 MB.
Small, native, and honest.
Under 1 MB. Zero dependencies.
One static binary built with make. No runtime, no installer, no framework — nothing to keep updated.
Nothing lives in the background.
No daemon, no telemetry, no account. Nibble runs, applies, exits. The opt-in menu bar is the only resident mode.
Remap any button. No G HUB.
The button list is read from the device itself, so any Logitech mouse just works. Bind a keystroke, a macro, or a system action — and switch whole sets with profiles. Quit the menu bar and factory behavior is back instantly.
Verified writes. Real volts.
DPI 50–25600 and 125–1000 Hz writes are read back from the device and confirmed; battery is honest millivolts through a LiPo curve — and rgb off buys the hours back.
Speaks HID++ 4.2, natively.
Raw protocol through IOKit over a Lightspeed / Unifying receiver, a USB cable, or Bluetooth — the same language G HUB speaks.
A real window, not a web view.
Hand-written AppKit: a settings window with DPI, report rate, lighting and a button editor, plus a menu bar that draws the battery inside the mouse icon. No Electron, no SwiftUI runtime.
It tells you why it stopped.
Permissions and engine state are on screen, each failure next to the button that fixes it — and nibble doctor prints the same checks with a concrete next step. Every read command speaks --json, for scripts and agents.
Nothing to take on faith.
MIT, one directory of Swift, no dependencies to audit. 244 checks cover the protocol framing, parsing and config merges — clone it and make test — and a mutation audit proves they bite.
Set once. Comes back at login.
Your config is a tiny JSON. One switch registers the menu bar as a login item so remaps survive a reboot; another re-applies DPI, rate and lighting through a one-shot launchd agent that exits immediately.
One binary. Every control.
| Command | Effect |
|---|---|
| nibble status | Overview: link, battery, DPI, report rate, feature flags |
| nibble battery | One line, script-friendly: 50% 3.85V charging |
| nibble doctor | Diagnose permissions, device, config, engine — each failure prints its fix |
| nibble dump | Enumerate all HID++ features (diagnostic) |
| nibble dpi [50-25600] | Get / set DPI, verified by read-back |
| nibble rate [125|250|500|1000] | Get / set report rate in Hz |
| nibble rgb off|show | Lights off (power saving) / list zones and effects |
| nibble mode [host|onboard] | Get / set control-mode flag |
| nibble wheel free|ratchet [threshold N] | SmartShift wheel mode — MX-series only (untested) |
| nibble onboard info|backup | Onboard memory info / read-only full dump |
| nibble config init|show | Create from current state / print ~/.config/nibble.json |
| nibble apply | Apply the config file (runtime writes) |
| nibble replay install|uninstall | Auto-apply DPI / rate / lighting at login via a one-shot launchd agent |
| nibble startup [on|off] | Start the menu bar at login, so button remaps survive a reboot |
| nibble buttons | Enumerate programmable buttons (0x1b04 MX-series / 0x8110 G-series) |
| nibble spy [seconds] | Live button-event monitor, G-series diagnostic |
| nibble remap | Interactive remap: press a button → assign keystroke / macro / system action / disable |
| nibble profile [list|use|new|copy|rename|delete] | Switch between whole sets of button mappings |
| nibble menubar | Interactive menu bar: battery + DPI/rate/RGB; hosts the remap engine (~15 MB, opt-in) |
| Nibble.app | Menu bar launched from Finder — comes with the cask (or make install-app); adds low-battery notifications and the login item |
| nibble version | Print the version (--json on this one too) |
| nibble ui | Native settings window: General (DPI, rate, lighting, login, alerts, health) + Buttons (remap table with profiles) |
Three lines to a configured mouse.
The Windows port is beta: the CLI and tray are built, pass the same protocol vectors as the macOS binary, and CI exercises every no-device path on real Windows — but none of it has touched a real mouse yet. Hardware validation is still to come; the release and Scoop package ship only after it passes. Follow along on GitHub. Linux comes after — the protocol core is OS-free, so a port is one transport file (hidraw).
Install
macOS 13+, no toolchain needed — the cask ships a prebuilt universal app with the CLI included. Logitech mouse on a Lightspeed / Unifying receiver, over a USB cable, or paired directly over Bluetooth. Tested on G502 LIGHTSPEED via receiver; the Bluetooth-direct path follows the HID++ spec but is not yet verified on hardware.
brew tap ben0128/nibble brew trust ben0128/nibble brew install --cask nibble
Or build from source (Command Line Tools are enough):
git clone https://github.com/ben0128/nibble && cd nibble make ./nibble status
The cask already puts Nibble.app in /Applications. Building from source instead? make install-app does the same — menu bar launched from Finder, low-battery notifications, “start at login”.
0xE00002E2 just means the permission is missing; no restart needed. Button remapping additionally needs Accessibility — the engine asks once and retries on its own, so remaps start working seconds after you grant it.
~/.config/nibble.json
Create it from your current state with nibble config init. Omitted keys are left untouched.
{
"dpi": 1600,
"reportRateHz": 1000,
"rgb": "off",
"lowBatteryPercent": 20,
"activeProfile": "Gaming",
"buttonProfiles": {
"Gaming": {
"G502 LIGHTSPEED Wireless Gaming Mouse": {
"G7": { "type": "keys", "keys": "cmd+space" },
"G8": { "type": "macro", "keys": "cmd+c, 150ms, cmd+v" },
"G9": { "type": "system", "action": "mission-control" }
}
}
}
}
Also available: lowBatteryNotify false to silence alerts, and wheelMode "free" / "ratchet" with wheelThreshold 1–254 (MX-series). Then nibble apply — or nibble replay install to re-apply at every login. A config from an older version, with mappings directly under buttonMaps, is read as the Default profile and migrated on first write.
Runtime writes, by design
Writes go to device RAM — a power cycle reverts to the onboard profile. replay re-applies your config at login, in one shot.
Host mode, handled
Report-rate and RGB writes need the host-mode flag; commands flip it automatically and it reverts with the mouse.
Your flash stays yours
Onboard flash is read-only by design. onboard backup dumps every sector — restore factory settings via G HUB on any machine, any time.