QMK RGB Tool
Control the RGB lighting on QMK-compatible keyboards. Supports Impact 80 and Rainy 75 out of the box, and any other keyboard running QMK with the RGB Matrix subsystem and VIA support.
Cross-platform CLI for Linux, macOS, and Windows. Designed for automation, scripting, and agent consumption.
|
|
преди 1 седмица | |
|---|---|---|
| .claude | преди 1 седмица | |
| .opencode | преди 2 седмици | |
| cmd | преди 1 седмица | |
| docs | преди 1 седмица | |
| internal | преди 1 седмица | |
| profiles | преди 1 седмица | |
| .gitignore | преди 1 седмица | |
| AGENTS.md | преди 1 седмица | |
| CLAUDE.md | преди 2 седмици | |
| LICENSE | преди 2 седмици | |
| README.md | преди 1 седмица | |
| go.mod | преди 1 седмица | |
| go.sum | преди 2 седмици | |
| keyboards.json | преди 1 седмица |
Control the RGB lighting on QMK-compatible keyboards. Supports Impact 80 and Rainy 75 out of the box, and any other keyboard running QMK with the RGB Matrix subsystem and VIA support.
Cross-platform CLI for Linux, macOS, and Windows. Designed for automation, scripting, and agent consumption.
On Linux, libudev-dev is required before building:
sudo apt install -y libudev-dev
Build locally:
git clone <repo-url>
cd qmk-rgb
go build -o qmk-rgb-tool ./cmd/qmk-rgb-tool/
And, to run it from anywhere, install globally (run from the repo root, installs to $GOPATH/bin/):
go install ./cmd/qmk-rgb-tool/
completion writes an autocompletion script to stdout for bash, zsh,
fish or powershell. Each of those subcommands also accepts
--no-descriptions to complete without description text. Run
qmk-rgb-tool completion <shell> --help for the path your platform expects;
for bash and zsh on Linux:
# bash: bash-completion 2.x loads this directory, so no sudo is needed
mkdir -p ~/.local/share/bash-completion/completions
qmk-rgb-tool completion bash > ~/.local/share/bash-completion/completions/qmk-rgb-tool
# zsh: the file name must start with an underscore
mkdir -p ~/.zsh/completions
qmk-rgb-tool completion zsh > ~/.zsh/completions/_qmk-rgb-tool
zsh does not include that directory in $fpath by default, so add it and
re-run compinit in ~/.zshrc:
fpath=("$HOME/.zsh/completions" $fpath)
autoload -Uz compinit && compinit
Open a new shell afterwards. The script is generated from the command tree, so re-run the command whenever commands or flags change.
# Discover connected keyboards
./qmk-rgb-tool keyboard info
# RGB commands target every channel the keyboard reports, in channel order
./qmk-rgb-tool effect breathing
./qmk-rgb-tool effect rainbow_moving_chevron
./qmk-rgb-tool effect rainbow_moving_chevron --zone backlight
./qmk-rgb-tool brightness 160
./qmk-rgb-tool speed 2
./qmk-rgb-tool color 00ff00
./qmk-rgb-tool color hsv:85,255,255
# Raw mode IDs are zone-specific; ID 17 is Backlight-only
./qmk-rgb-tool mode 17 --zone backlight
./qmk-rgb-tool enable
./qmk-rgb-tool disable
./qmk-rgb-tool info
# Select exactly one zone
./qmk-rgb-tool brightness 160 --zone side
# Save and load RGB profiles
./qmk-rgb-tool save paul
./qmk-rgb-tool load paul
./qmk-rgb-tool list
./qmk-rgb-tool delete paul
# Select a keyboard by the number shown in `keyboard info`
./qmk-rgb-tool --device 1 enable
--zone accepts a VIA lighting channel: backlight, rgblight, rgb_matrix,
audio or led_matrix, and may be given on any subcommand. The name follows
from the channel number, so it works on every QMK keyboard. A board listed in
keyboards.json may give a channel a display name, and that name is accepted
too — the Impact 80 calls channels 2, 3 and 4 logo, backlight and side.
Where a display name would be the subsystem name of a different channel the
keyboard has, the board is refused rather than a command being sent to the wrong
channel.
--zone is not remembered between runs: repeat it on every invocation. Without
--zone, commands target every channel the keyboard reports, in channel order.
With --zone, commands target exactly the named channel. An unknown name fails
before the device is opened; an effect a named channel does not have fails
rather than being skipped, while a default command skips it and prints a
warning on stderr.
The keyboard is asked which channels it has: one read per channel, and a channel
its firmware does not implement answers as unhandled. The probe covers channels 1
to 15 even though QMK assigns five, so every command that opens a keyboard pays
about fifteen sequential round trips before it does anything else. A read that
fails for any other reason, or an unhandled answer that belongs to a different
channel because the request and response streams fell out of step, aborts the
command with an error naming the channel rather than reporting a short list as
complete. keyboard info does not report the channels, because it never opens
the keyboard.
All output is machine-parseable JSON when applicable. The shapes an agent parses:
// info
{"enabled": true, "mode": "fixed_wave", "brightness": 10, "speed": 0,
"zones": [{"zone": "logo", "channel": 2, "enabled": true, "effect": "fixed_wave",
"effectId": 2, "brightness": 10, "speed": 0,
"color": {"hue": 0, "saturation": 255}, "error": "only on a channel that failed"}]}
// effect --list
{"catalog": "impact80",
"zones": [{"zone": "logo", "channel": 2, "subsystem": "rgblight",
"effect": "none", "id": 0}]}
effect --list reports "catalog": "" and an empty zones array for a board
that has no catalog, and it reads the keyboard to learn which channels to list.
info prints the JSON and then exits non-zero if a channel could not be read.
keyboard info numbers every connected QMK keyboard starting at 1, and
--device takes that number:
./qmk-rgb-tool keyboard info
./qmk-rgb-tool --device 2 brightness 160
Without --device, commands that open a keyboard run only when exactly one is
connected. With two or more, they fail and list the numbers you can choose from,
so a command never hits an unintended keyboard. keyboard info, list and
delete never open a keyboard and work with any number of them.
Numbers are assigned in a stable order (vendor ID, product ID, path), but they
are only guaranteed for the current session. Keyboards are identified by their
HID path, which the operating system reassigns on reboot, and most keyboards
report no serial number. Re-run keyboard info after reconnecting a keyboard
rather than storing the number.
color accepts three notations for one operation:
qmk-rgb-tool color ff0000 # six digit hex
qmk-rgb-tool color rgb:ff0000 # the same, written out
qmk-rgb-tool color hsv:0,255,255 # hue, saturation, value, each 0-255
hsv: takes the numbers the keyboard itself computes in, so a color can be
addressed directly instead of being derived from hex. The hex notations say
nothing about brightness and leave it alone.
The keyboard stores hue and saturation only; it has no value register, so the
value component of a hex color has nowhere to go and is dropped. The value of an
hsv: notation therefore goes to the brightness of the same zones, where the
per-channel firmware transform applies — hsv:0,255,200 leaves logo and side
at 160 while backlight reaches 255. See "Brightness and Speed Are Not Applied
Verbatim" for the table.
Every notation is read back, so the line reports what the keyboard holds:
$ qmk-rgb-tool color 00ff00 --zone backlight
Color set to hue 85 sat 255
$ qmk-rgb-tool color hsv:0,255,200
Color logo hue 0 sat 255 brightness 160 backlight hue 0 sat 255 brightness 255 (requested hue 0 sat 255 brightness 200)
Both notations land on the same grid. The keyboard stores one byte of hue and one
of saturation, so at most 65,536 colors are reachable, and the hex form is
converted to that pair before it is sent — it does not reach more of them.
hsv: addresses the grid directly, which is its whole advantage: a color can be
asked for by the numbers the keyboard itself uses instead of being derived from
a hex triple.
The keyboard holds effect numbers, not names, so effect <name> needs a catalog
and the tool has one for the Impact 80. Its 46 backlight names are the QMK
rgb_matrix_effects.inc of the VIA era, transcribed from that board's vendor VIA
definition, and the 7 logo and 7 side names are that same definition, whose
dropdowns read fixed wave and breathe; the compatibility aliases are the
tool's own.
effect writes the ID and reads the register back, because a board can refuse
one. All 46 backlight IDs are taken, and so are the logo and side IDs from 1
to 6. ID 0 is not: on those two channels the firmware reads it as "lighting
off" and leaves the mode register where it was, so effect none --zone logo
leaves the previous effect running and says so.
$ qmk-rgb-tool effect none --zone logo
Effect logo "wave" (1) (requested "none" (0))
The read-back proves an ID is one the keyboard takes, not that the name labels
it correctly; the names come from the vendor definition. The board takes 47 IDs
on the backlight channel, 0 to 46, of which this catalog names 46 — ID 46 exists,
is reported as unknown, and is set with mode 46.
A keyboard without a catalog is still driven: brightness, speed, color,
mode <index> and info all work, because none of them needs a name. Four
commands need the catalog and say so rather than guessing:
$ qmk-rgb-tool effect wave
Error: no effect catalog for this keyboard; set an effect by number with `mode <index>`
$ qmk-rgb-tool enable
Error: this keyboard has no effect catalog, so `enable` cannot choose an effect; set one with `mode <index>`
$ qmk-rgb-tool load paul
Warning: profile "paul" has no effect catalog for this keyboard; nothing applied
$ qmk-rgb-tool save paul
Warning: this keyboard has no effect catalog, so the profile records effect "unknown" and cannot restore it; set an effect with `mode <index>`
enable needs it because it has to choose an effect to turn a channel on.
load applies nothing without it, because the profile stores effect names.
save still writes the profile, but every effect is recorded as unknown, so
that file cannot restore an effect later.
Naming a real effect on a channel that does not have it is refused, and a name the board does not have anywhere is reported as unknown:
$ qmk-rgb-tool effect rainbow_moving_chevron --zone logo
Error: effect rainbow_moving_chevron is not supported on rgblight
$ qmk-rgb-tool effect nonsense --zone rgb_matrix
Error: unknown effect: nonsense
The keyboard's firmware transforms these values per channel, so the accepted
range is 0–255 but the value the keyboard ends up holding is often different.
brightness and speed read every zone back and report what it actually
applied:
$ qmk-rgb-tool brightness 200
Brightness logo 160 backlight 255 side 160 (requested 200)
$ qmk-rgb-tool brightness 160 --zone logo
Brightness set to 160
$ qmk-rgb-tool speed 60
Speed logo 4 backlight 60 side 4 (requested 60)
$ qmk-rgb-tool speed 60 --zone backlight
Speed set to 60
Observed on the Impact 80:
| Zone | Channel | Brightness | Speed |
|---|---|---|---|
logo |
0x02 |
clamps at 160 | 0 stops the animation, 1 is the slowest movement, above 1 becomes 4 |
backlight |
0x03 |
scales up, saturating at 255 | applied as given |
side |
0x04 |
clamps at 160 | 0 stops the animation, 1 is the slowest movement, above 1 becomes 4 |
Speed is not a smooth dial on logo and side. Only three outcomes are
reachable there, and the value range the vendor's own VIA definition offers for
those two channels is [0, 4], not 0–255:
| Requested | Stored | Effect |
|---|---|---|
0 |
0 |
the animation is frozen, the color stands still |
1 |
1 |
the slowest movement |
2 and above |
4 |
twice as fast; this is where every larger request lands |
The freeze at 0 was observed on logo; side reports the same range and the
same collapse to 4. backlight is the only channel with a usable 0–255 range.
The command exits 0 either way: a value the firmware cannot represent is not a failure, but the summary line always states the value that was actually applied.
profiles/off, breathe, rainbow, rainbow_wave, solid, static resolve to correct effect IDs per channelcolor and speed for key-press illuminationkeyboard info, info, list, and effect --listkeyboard info numbers each keyboard; --device <n> targets one| Zone on the Impact 80 | CLI name | VIA channel | Effect IDs |
|---|---|---|---|
| Logo | logo |
2 | 0–6 |
| Backlight | backlight |
3 | 0–45 |
| Side | side |
4 | 0–6 |
Those three display names live in keyboards.json; the QMK subsystem names for
the same channels are rgblight, rgb_matrix and audio, and both spellings
work on this board.
Logo and Side share this complete effect family:
| ID | Name |
|---|---|
| 0 | none |
| 1 | wave |
| 2 | fixed_wave |
| 3 | spectrum |
| 4 | breathing |
| 5 | light |
| 6 | shutdown |
Backlight uses the complete Impact 80 catalog:
| ID | Name | ID | Name |
|---|---|---|---|
| 0 | none |
23 | flower_blooming |
| 1 | solid_color |
24 | raindrops |
| 2 | alphas_mods |
25 | jellybean_raindrops |
| 3 | gradient_up_down |
26 | hue_breathing |
| 4 | gradient_left_right |
27 | hue_pendulum |
| 5 | breathing |
28 | hue_wave |
| 6 | band_sat |
29 | pixel_flow |
| 7 | band_val |
30 | digital_rain |
| 8 | band_pinwheel_sat |
31 | solid_reactive |
| 9 | band_pinwheel_val |
32 | solid_reactive_wide |
| 10 | band_spiral_sat |
33 | solid_reactive_multiwide |
| 11 | band_spiral_val |
34 | solid_reactive_cross |
| 12 | cycle_all |
35 | solid_reactive_multicross |
| 13 | cycle_left_right |
36 | solid_reactive_nexus |
| 14 | cycle_up_down |
37 | solid_reactive_multinexus |
| 15 | cycle_out_in |
38 | splash |
| 16 | cycle_out_in_dual |
39 | multisplash |
| 17 | rainbow_moving_chevron |
40 | solid_splash |
| 18 | cycle_pinwheel |
41 | solid_multisplash |
| 19 | cycle_spiral |
42 | starlight |
| 20 | dual_beacon |
43 | starlight_dual_hue |
| 21 | rainbow_beacon |
44 | starlight_dual_sat |
| 22 | rainbow_pinwheels |
45 | riverflow |
Effects fall into categories that behave differently:
solid_color, none) display a steady output. The color is set by color and is constant.solid_reactive*) light up on key press. They honor the color set by color — the pressed key's LEDs flash in the configured color. Use speed to adjust how long the illumination lasts.breathing, hue_breathing) slowly fade in and out, and both honor color. breathing keeps the hue and saturation and pulses the brightness of that color; hue_breathing swings the hue around the hue you set by a narrow amount instead of running through the full range.band_*, cycle_*, rainbow_*, starlight*, raindrops, jellybean_raindrops, flower_blooming, pixel_flow, riverflow) animate independently. Most ignore color and use their own color palettes. hue_pendulum and hue_wave cycle through hues.splash, multisplash, solid_splash, solid_multisplash) react to key presses like reactive effects but with a splash pattern. solid_splash and solid_multisplash honor color.gradient_up_down, gradient_left_right) create a color gradient across the keyboard. They do not honor color.alphas_mods) colors modifier keys differently from alphanumeric keys. The colors are built-in and cannot be changed.To set a permanent color, use solid_color and then color <hex>.
The logo and side catalogs hold only seven effects each, and the color you
set is shown by three of them: fixed_wave, breathing and light. wave,
spectrum and shutdown run their own color sequence, so a color set on them is
stored but not displayed.
Effect names are resolved independently for each target zone. Compatibility
aliases are off → none, breathe → breathing, rainbow → spectrum
for Logo/Side and rainbow_moving_chevron for Backlight, rainbow_wave →
wave for Logo/Side, and solid → light for Logo/Side and solid_color
for Backlight. The legacy static alias remains accepted as a compatibility
alias for solid.
The effect catalog follows the QMK RGB Matrix firmware of the VIA era, not
current QMK master: the two lists differ in length, in naming and in numbering
(alpha_mods against alphas_mods, colorband_sat against band_sat, and
pixel_fractal, typing_heatmap and starlight_smooth among the names QMK has
since added), so the catalog must not be repaired against upstream. The
keyboard implements the VIA protocol (version 3). The animations are defined in
QMK rgb_matrix/animations/,
where each effect has its own header file (e.g.
digital_rain_anim.h, solid_reactive_anim.h, riverflow_anim.h).
Its other two channels are not QMK's at all. QMK's lightweight rgblight
subsystem has about 49 modes named STATIC_LIGHT, RAINBOW_MOOD, SNAKE,
KNIGHT, CHRISTMAS and TWINKLE, and shares no name with the seven the Impact
80 offers on logo and side. A per-subsystem catalog taken from QMK would
therefore be wrong for those channels.
ID 39 is multisplash; ID 41 is the distinct solid_multisplash name.
info reports a zones array with each zone's channel, enabled state,
effect name and ID, brightness, speed, and color. The top-level enabled,
mode, brightness, and speed fields summarize the first selected zone.
If one zone cannot be queried, its record contains an error, other records
are retained, and the process exits non-zero after printing the JSON.
The tool uses hidapi for cross-platform HID access. Most platforms need no configuration, but Linux requires extra setup.
To build, libudev-dev is required:
sudo apt install -y libudev-dev
To run without sudo, grant your user group access to hidraw devices. By default, hidraw devices are owned by root (600).
Choose one of the methods below.
Grant your user group access to all HIDRAW devices:
sudo chown root:adm /dev/hidraw*
sudo chmod 660 /dev/hidraw*
This works for the current session only. Permissions reset after reboot.
Create a persistent rule for the Impact 80:
echo 'SUBSYSTEM=="hidraw", ATTRS{idVendor}=="36b0", ATTRS{idProduct}=="309f", MODE="0660", GROUP="adm"' \
| sudo tee /etc/udev/rules.d/99-qmk-rgb-tool.rules
sudo udevadm control --reload-rules
sudo udevadm trigger
sudo udevadm reload
This makes the Impact 80 accessible to members of the adm group on every boot.
No setup required. macOS applications access HID devices directly through IOKit.
No setup required. Windows applications access HID devices directly through the Windows HID API.
After Linux setup, run the tool as your regular user (no sudo needed).
Any keyboard running QMK is detected automatically via the QMK Raw HID signature (Usage Page 0xFF60, Usage 0x61). No manual configuration required.
That signature says nothing about lighting, so a keyboard with no VIA lighting
channel at all is listed here and then fails every command that opens it with
this keyboard exposes no VIA lighting channels.
keyboards.json is looked for next to the executable first, then in the current
working directory. go install puts the binary in $GOPATH/bin while the file
stays in the repository, so run the tool from the repository, put a copy of the
file next to the binary, or name the zones by their QMK subsystem. Without the
file the tool still works: model names, the logo/backlight/side display
names and the effect catalog are what it supplies.
keyboards.json is optional. When present it supplies a name for the model and,
per entry, a display name per channel: keyboard info reports "known": true
for models listed there and "known": false for every other QMK keyboard. Both
are fully controllable — known describes the name lookup, not compatibility.
Deleting or corrupting the file costs names only: the channel vocabulary
survives, because the QMK subsystem name follows from the channel number.
Two models are listed in keyboards.json:
| Keyboard | VID | PID |
|---|---|---|
| Wobkey Rainy 75 | 0x6666 | 0x0001 |
| Wobkey Impact 80 | 0x36B0 | 0x309F |
The vendor's
Driver & Firmware page
is where the effect catalog in internal/rgb/impact80.go comes from. It is the
authority on the wire format, and it settles two things this tool cannot:
side is where that lighting lives.The page links the sources used here directly:
| What | Where |
|---|---|
| VIA definition JSON, the origin of all three effect lists | Impact_80.JSON |
| VIA firmware image | impact_80.bin |
| Update instructions, vendor's warning against updating a working board | Driver & Firmware page |
Stock VIA exposes no way to ask a keyboard which effect IDs it implements, so
the names come from that JSON and not from the board. The counts the board
actually takes were measured here: 47 on the backlight channel, 6 on logo and
side.
| Command | Description |
|---|---|
qmk-rgb-tool keyboard info |
Discover connected QMK keyboards |
qmk-rgb-tool enable |
Enable selected lighting zones |
qmk-rgb-tool disable |
Disable selected lighting zones |
qmk-rgb-tool info |
Show per-zone RGB state (JSON) |
qmk-rgb-tool effect <name> |
Set a zone-aware effect by name, verified by read-back |
qmk-rgb-tool effect |
With no argument, list every effect per channel (JSON) |
qmk-rgb-tool effect --list |
The same list, as a flag |
qmk-rgb-tool brightness <val> |
Set brightness (0–255) on selected zones, verified by read-back |
qmk-rgb-tool speed <val> |
Set effect speed (0–255) on selected zones, verified by read-back; on logo and side only 0, 1 and 4 are reachable |
qmk-rgb-tool color <hex> |
Set color (e.g. ff0000) on selected zones |
qmk-rgb-tool color rgb:<hex> |
The same hex color, written out |
qmk-rgb-tool color hsv:<h>,<s>,<v> |
Set hue and saturation (0–255) and write v to the brightness of the same zones |
qmk-rgb-tool mode <index> |
Set a raw zone-specific effect ID, verified by read-back; a board that does not implement the index clamps it to the highest it does |
qmk-rgb-tool --zone <channel> ... |
Target one channel: backlight, rgblight, rgb_matrix, audio or led_matrix, or a name from keyboards.json |
qmk-rgb-tool --device <n> ... |
Target keyboard by number (see keyboard info) |
qmk-rgb-tool -v, --version |
Print the version |
qmk-rgb-tool save [name] |
Save current RGB state to profiles/<name>.json (name lowercased, non-[a-z0-9-_] mapped to -, a leading - prefixed with unnamed-); without a name it writes default |
qmk-rgb-tool load [name] |
Load and apply a profile from profiles/; without a name it loads default |
qmk-rgb-tool list |
List saved profiles |
qmk-rgb-tool delete [name] |
Delete a saved profile; without a name it deletes default |
qmk-rgb-tool completion <shell> |
Write an autocompletion script for bash, zsh, fish or powershell to stdout |
enable, disable, info and list take no arguments and reject a stray
token. effect, load, save and delete accept an optional name.
brightness, speed, color and mode require exactly one argument.
Profiles store RGB state (effect, brightness, speed, color) per channel as JSON
files in the profiles/ directory, which is resolved against the working
directory the tool runs in. The keys are the names the file was written with, so
a profile written before a board was renamed reports the key it cannot place and
skips it. They are the only way to persist RGB
settings across reboots. The keyboard's VIA protocol does not expose a
standard command to save RGB state to internal EEPROM, so profiles rely on
files instead.
cmd/qmk-rgb-tool/ # Cobra-based CLI
internal/device/ # HID discovery + keyboards.json loader
internal/hid/ # Cross-platform HID access (hidapi)
internal/rgb/ # Effects, colors, state
internal/via/ # VIA protocol implementation
Communicates via the QMK Raw HID interface (Usage Page 0xFF60, Usage 0x61) with 32-byte reports, report number 0.
0x07 — Custom set value0x08 — Custom get valueid_qmk_*_channel values: 0x01 backlight,
0x02 rgblight, 0x03 rgb_matrix, 0x04 audio, 0x05 led_matrix. A
channel the firmware does not compile in answers a get with 0xFF, which is
how the tool discovers what a keyboard has. The Impact 80 uses 0x02 for its
logo, 0x03 for its backlight and 0x04 for its side lighting.0x01,
effect 0x02, speed 0x03, color 0x04. That uniformity is why the tool has
no per-subsystem code path.enable and disable use no separate handshake: they set the effect ID and
brightness, and nothing else.