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 週間 前 | |
| definitions | 1 週間 前 | |
| docs | 1 週間 前 | |
| internal | 1 週間 前 | |
| .gitignore | 1 週間 前 | |
| AGENTS.md | 1 週間 前 | |
| CLAUDE.md | 2 週間 前 | |
| LICENSE | 2 週間 前 | |
| README.md | 1 週間 前 | |
| go.mod | 1 週間 前 | |
| go.sum | 2 週間 前 |
Control the RGB lighting on QMK-compatible keyboards. Any keyboard running QMK with a
lighting channel and VIA support is driven; the Impact 80's effect names are built in,
every other board's are fetched with keyboard fetch.
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/
go install copies the binary and creates no data directory, so an installed
tool finds definitions/ and profiles/ under the per-user directory instead of
beside itself. The Impact 80's definition is built into the binary, so an
installed tool still has its effect names. To use a definition from a checkout,
put the file in the per-user directory, or pass --definition; both win over what
the binary carries. A profile in a checkout is used by naming its file:
qmk-rgb-tool load profiles/lava.json.
Effect names come from a VIA definition file — the same one VIA itself reads when you open a board in its web app — so the tool needs that board's file, the way VIA does. Identify the keyboard, then get its definition:
qmk-rgb-tool keyboard fetch # download the definition for the connected keyboard
qmk-rgb-tool keyboard definitions # every definition in use, and where each is from
Where those files live. Both directories are under the platform's per-user configuration directory, and a name in either is read from there and written to there:
| Platform | Directory |
|---|---|
| Linux | ~/.config/qmk-rgb-tool/ ($XDG_CONFIG_HOME) |
| macOS | ~/Library/Application Support/qmk-rgb-tool/ |
| Windows | %AppData%\qmk-rgb-tool\ |
One directory per kind, and not a search. A definition is written by
keyboard fetch and read by every command, so a search path would mean the
file a fetch produced is not the file the next command reads. A profile is
written there and read there, so save <name> followed by load <name> finds
what was just written and a same-named file in a checkout cannot be loaded in its
place.
A profile argument ending in .json is the one way out, and it is not a search:
the argument names the file, so load profiles/lava.json reads that file and
save ./lava.json writes it, wherever they are. Every other argument is a
profile name and comes from the per-user directory. The suffix is matched
without regard to case and the path is handed to the file system as written, so
load lala.json looks for ./lala.json and says so when it is not there, rather
than resolving lala.json to a profile named lala-json. There is no fallback
from a path to a name, so list and the shell completion keep listing the names in
the per-user directory and nothing else. delete is name-only, so a file outside
the per-user directory is read and written and never removed; rm is the tool for
that, and delete says which file it removed.
A save writes the file and creates no directory for it, so a name in a
directory that is not there is refused with the directory's name. The per-user
profiles/ directory is the one exception and is created when missing, because it
is this tool's own and the first save is the case that needs it:
$ qmk-rgb-tool save profiles/typo/x.json
Error: profile directory profiles/typo does not exist; create it first, or save a name
The Impact 80's definition is additionally built into the binary and consulted last, so an installed tool has it; see Effect Names Are Per Board.
A file already in the per-user definitions/ directory is used for its own board
without any argument, so to use a definition you have already, put it in that
directory or pass it with --definition <path>. The Wobkey Impact 80's file is
vendored in the repository's definitions/ and built into
the binary, because that board is not in VIA's collection and so cannot be fetched
at all; that directory is read only at build time.
Without a definition the keyboard is still fully driven with brightness, speed,
color, an effect index and info — only the effect names are missing, and
Effect Names Are Per Board says what that costs and
what the file does and does not tell the 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
On macOS, a Homebrew install already puts a directory on $fpath, so the file
needs no .zshrc change and no compinit re-run — writing it there is enough.
The prefix depends on the architecture, so take the one zsh reports:
print -l $fpath | grep zsh/site-functions
qmk-rgb-tool completion zsh > /opt/homebrew/share/zsh/site-functions/_qmk-rgb-tool
The binary itself has to be on $PATH for any of this to offer more than command
names. The generated script calls it back for every candidate and discards the
error, so a binary that is missing degrades to plain file completion without a
message.
Open a new shell afterwards.
The generated script is a shim: cobra's shells ask the running binary what to
offer, so re-running the command matters when the script itself changes but the
behaviour comes from whichever binary is on $PATH. What the shell offers is
registered per flag and per argument, which is why these work:
$ qmk-rgb-tool effect <TAB> # the QMK subsystem names and the board's own
$ qmk-rgb-tool effect --device <TAB> # the numbers `keyboard info` prints
$ qmk-rgb-tool load <TAB> # the profiles in the per-user profiles/
None of them opens the keyboard — a shell asks while nothing is plugged in, so they work from the data directory and from enumeration alone. A value that cannot be read yields no candidates rather than an error, because a shell prints an error into the prompt, which is worse than an empty list.
# Discover connected keyboards
./qmk-rgb-tool keyboard info
# A lighting command names its target as its first argument
./qmk-rgb-tool effect rgb_matrix breathing
./qmk-rgb-tool effect backlight rainbow_moving_chevron
./qmk-rgb-tool brightness side 160
./qmk-rgb-tool speed all 2
./qmk-rgb-tool color all 00ff00
./qmk-rgb-tool color backlight hsv:85,255,255
# Several channels at once, comma separated; a raw effect index is
# zone-specific, and ID 17 is Backlight-only
./qmk-rgb-tool brightness logo,side 160
./qmk-rgb-tool effect backlight 17
./qmk-rgb-tool enable all
./qmk-rgb-tool disable logo
# Reading needs no target, and a read may still name one to narrow itself
./qmk-rgb-tool info
./qmk-rgb-tool effect all
./qmk-rgb-tool effect logo
# Save and load RGB profiles
./qmk-rgb-tool save paul
./qmk-rgb-tool load paul
./qmk-rgb-tool load paul side
./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 rgb_matrix
The zone is a VIA lighting channel: backlight, rgblight, rgb_matrix, audio
or led_matrix, and every command that names a channel takes it. That name follows
from the channel number, so it works on every QMK keyboard. A board's definition
file may name a channel differently, and that name is accepted too, ignoring
case — the Impact 80 calls channels 2, 3 and 4 logo, Backlight and side.
Where a 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.
Several channels are written comma separated, and all is every channel the
keyboard reports: brightness logo,side 160 reaches exactly those two, and
all next to another name is the same request as all alone, since the union of
the two is the whole keyboard. A name written twice is one channel, spaces around
the commas are ignored, and the channels are always written in channel order, so a
script sees one order for every spelling of the same selection.
The zone is an argument rather than a flag because a flag every help listed told
keyboard info and list about a channel they cannot address, and they ignored
it. A command that writes lighting — effect, brightness, speed, color,
enable, disable — cannot be called without one, and the argument count refuses
it before the keyboard is opened:
$ qmk-rgb-tool brightness 160
Error: brightness takes a zone and a brightness value
usage: qmk-rgb-tool brightness <zone> <val>
A zone the keyboard does not have is refused by name, and where a list names several it names every one that is missing, so nothing is written on the way to a failure:
$ qmk-rgb-tool brightness logo,led_matrix 160
Error: this keyboard has no led_matrix channel
A command that only reads needs no zone: info, effect without a name and
save report every channel the keyboard has, which is what they are for. load
applies the zones its profile names, and a second argument narrows that to the
channel or channels it names.
An unknown name fails before the device is opened. An effect a named channel does
not have fails rather than being skipped, all included — effect all asks for
every channel, so a name only one of them has is a request the tool cannot carry
out, not a reason to write the others and report it as done. load is the one
place a name is skipped instead, with a warning on stderr, because there a whole
profile is being applied and a key the board cannot do is worth saying rather
than refusing over.
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 all
{"catalog": "Impact 80",
"zones": [{"zone": "logo", "channel": 2, "subsystem": "rgblight",
"effect": "none", "id": 0}]}
catalog carries the name from the definition file, so it is the vendor's own
rather than a label this tool invented. effect all reports "catalog": "" and
an empty zones array for a board that has no definition, 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 logo 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 rgb_matrix ff0000 # six digit hex
qmk-rgb-tool color all rgb:ff0000 # the same, written out
qmk-rgb-tool color side 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 backlight 00ff00
Color set to hue 85 sat 255
$ qmk-rgb-tool color all 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 <zone> <name> needs a catalog,
and the catalog is the board's VIA definition file. No catalog is hand-written
into this tool for any board. A transcribed name list and the vendor's file
describing the same board are two places to update one name, and that is how the
names in this repository drifted apart from the names Wobkey publishes.
The Impact 80's own file is the exception, and it is a file, not a list: it is
kept in definitions/ as served and built into the
binary with go:embed, because go install delivers a binary and no data
directory and this board is not in VIA's collection, so there is nothing to
fetch. It is consulted last: a file in a definitions directory wins over the
built-in one, so a shipped definition can be corrected or updated without
rebuilding, and a fresh clone works without any file being placed.
The names are the vendor's own spelling, which the tool does not correct: the
file writes fixed wave and breathe on logo and side, and breathing on
backlight, so the same effect arrives under different names on different
channels of the same board. The tool's own spellings resolve alongside them
(see Compatibility Aliases).
effect writes the ID and reads the register back, because a board can refuse
one. All 47 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 logo none
leaves the previous effect running and says so.
$ qmk-rgb-tool effect logo none
Effect logo "wave" (1) (requested "none" (0))
The read-back proves an ID is one the keyboard takes, never that the name labels it correctly.
The board takes 47 IDs on the backlight channel, 0 to 46, and clamps anything
above 46 down to it. The definition file names 46 of them, ending at riverflow,
so ID 46 has no name and the tool does not invent one:
$ qmk-rgb-tool effect backlight 46
Effect set to index 46
$ qmk-rgb-tool info
Backlight on unknown brightness 255 speed 127 color hsv:0,255
A name no source publishes is a name the tool would be making up, so this one
stays unknown and is set by number. Its behaviour is measured and worth
knowing: set after a moving effect, it leaves the LEDs on the pattern they are
currently showing and stops the animation — cycle_left_right frozen still looks
like a standing rainbow, because that is what it froze. It is also not the same
as speed backlight 0, which leaves the effect selected and only takes the rate
to zero: the two differ in the registers, one on the effect and one on the speed.
Wobkey's own
VIA definition JSON stops at
45 and contains no word for pause, stop, freeze or hold, so VIA cannot set this
ID from its dropdown either. A raw effect backlight 46 is the only way to reach
it.
A keyboard without a catalog is still driven: brightness, speed, color,
an effect 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 rgb_matrix wave
Error: no effect names for this keyboard: run `keyboard fetch` for its VIA definition, or set an
effect by number with `effect <zone> <index>`
$ qmk-rgb-tool enable rgb_matrix
Error: this keyboard has no effect names, so `enable` cannot choose an effect: run `keyboard fetch` for
its VIA definition, or set one with `effect <zone> <index>`
$ qmk-rgb-tool load paul
Warning: profile "paul" has no effect names for this keyboard, so nothing applied; run `keyboard fetch` for
its VIA definition
$ qmk-rgb-tool save paul
Warning: this keyboard has no effect names, so the profile records effect "unknown" and cannot restore it; run
`keyboard fetch` for its VIA definition, or set an effect with `effect <zone> <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 logo rainbow_moving_chevron
Error: effect rainbow_moving_chevron is not supported on rgblight
$ qmk-rgb-tool effect rgb_matrix nonsense
Error: unknown effect: nonsense (an effect ID from 0 to 255 is `effect <zone> <index>`)
VIA's own keyboard collection, the-via/keyboards,
is the only bulk source, and it is more useful than it looks. It holds 2029
definitions under v3/, one per unique vendorId/productId pair, and not one
of them has a lighting or an effects key — a keyboard's effect names are not
where you would look for them.
They are in menus. A definition either names one of VIA's built-in lighting
menus, or spells out its own UI as dropdowns, and a dropdown for
id_qmk_rgb_matrix_effect is a board's effect list, in ID order. Measured over
the collection:
| Boards | |
|---|---|
name VIA's built-in qmk_rgb_matrix menu |
179 |
ship their own id_qmk_rgb_matrix_effect dropdown |
163 |
| of those, an exact prefix of the built-in list | 10 |
The built-in list lives in
the-via/reader/src/common-menus/qmk_rgb_matrix.ts
and has 45 names, All Off through Solid Multi Splash, at IDs 0 to 44. A board
that uses it needs no work at all: the names and the IDs are the built-in ones.
The other 153 deviate, and not only in how many effects they compile in. MonsGeek's
M1 uses QMK's enum identifiers and its own 19: SOLID_COLOR, BREATHING,
CYCLE_ALL, TYPING_HEATMAP, MATRIX_MULTISPLASH. Redragon's K667 ships 14 in
yet another order, with Pixel Fractal already at ID 13. Only 11 of the 163
write names as enum identifiers; 152 use display labels, so the same effect arrives
as All Off, NONE or 00.NONE depending on the vendor.
That is why a catalog cannot be defaulted per subsystem, and it is the same
reason the Impact 80 needs its own. Its backlight channel reorders IDs 15 to 17 —
cycle_out_in, cycle_out_in_dual, then rainbow_moving_chevron, where the
built-in list has rainbow_moving_chevron first. It also edits the set and not
only the order: it drops four names the built-in list has — pixel_rain,
pixel_fractal, typing_heatmap and solid_reactive_simple — and adds five it
does not have: flower_blooming, starlight, starlight_dual_hue,
starlight_dual_sat and riverflow. 45 − 4 + 5 = 46 names, ending at ID 45. A
board can also be at an
ID the built-in list does not name at all, and the source for that name is the
board, not VIA:
| What | Where |
|---|---|
| VIA definition JSON, the origin of all three Impact 80 effect lists | Impact_80.JSON |
| VIA firmware image | impact_80.bin |
| Update instructions, vendor's warning against updating a working board | Driver & Firmware page |
The Impact 80 is not in VIA's collection — there is no Wobkey entry under v3/
— so its names are reachable only through that vendor page.
OpenRGB also drives QMK keyboards over
the very same raw HID interface, but as Controllers/QMKController/ including a
Vial and a Keychron variant: C++ device code, no JSON, no effect tables. It is a
good cross-check for which VID/PID belongs to which model, and no help for effect
names. The project has left GitHub; the mirror is
CalcProgrammer1/OpenRGB.
So a catalog is transcribed per board, and the collection above says where to look for each board's list, but which IDs a board takes is still confirmed against its register. That part cannot be automated from any database.
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 all 200
Brightness logo 160 backlight 255 side 160 (requested 200)
$ qmk-rgb-tool brightness logo 160
Brightness set to 160
$ qmk-rgb-tool speed all 60
Speed logo 4 backlight 60 side 4 (requested 60)
$ qmk-rgb-tool speed backlight 60
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 |
where every larger request lands; the ratio to 1 has not been measured |
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 definition file's own ranges are a separate thing from what the firmware
accepts, and the two disagree about brightness. It offers [0, 160] for
brightness on all three channels, so VIA's sliders stop at 160 everywhere, while
the firmware scales backlight past that to 255. The table above is what the
keyboard does, measured; the ranges in the file are what VIA's UI offers.
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/ directory, which is both where they are written and where they are readoff, breathe, rainbow, rainbow_wave, solid, static resolve to correct effect IDs per channelcolor and speed for key-press illumination--json for keyboard info, info, list, the effect list and keyboard definitionskeyboard 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 (the board also takes 46) |
| Side | side |
4 | 0–6 |
Those three display names come from the board's definition file, in the sub-menu
it puts each channel under. 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. Two of these are the tool's own
spellings: the file writes fixed wave and breathe, and the effect list prints
the file's spelling.
| ID | Name | In the file |
|---|---|---|
| 0 | none |
none |
| 1 | wave |
wave |
| 2 | fixed_wave |
fixed wave |
| 3 | spectrum |
spectrum |
| 4 | breathing |
breathe |
| 5 | light |
light |
| 6 | shutdown |
shutdown |
Backlight uses the complete Impact 80 catalog. Here the two spellings agree, so one column carries the whole list:
| 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 |
| 46 | unnamed — the board takes it, the file stops at 45 |
The names below are the tool's own spellings, which every definition resolves
alongside the vendor's; the output shows whichever the file in use writes, so
The effect list may print fixed wave and breathe where this list says
fixed_wave and breathing. Same effect, same ID.
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 <zone> <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.
Those aliases are the three channels the Impact 80 names — rgblight,
rgb_matrix and audio. defaultAliases in internal/rgb/catalog.go is the
one place they are listed, and it has no entry for backlight or led_matrix,
so on a keyboard that has one of those channels an alias is refused by name
(effect off is not supported on backlight) rather than resolved. This board
has neither, which is why the difference is not visible here.
off resolves on Logo, Backlight and Side, but it cannot be set by effect ID on
logo and side: the firmware reads ID 0 there as "lighting off" and leaves the
mode register where it was, so the effect that was running keeps running.
disable is the command that turns a channel off, because it also writes
brightness 0.
The aliases belong to the tool, not to a board, so they work for every catalog — including one read from a definition file. A name resolves to itself, to the name it is an alias of, or to an alias of it, and spaces and underscores are the same character as far as lookup is concerned. So on this board all four of these select the same effect:
qmk-rgb-tool effect logo breathe # the file's spelling
qmk-rgb-tool effect logo breathing # the tool's spelling
qmk-rgb-tool effect logo fixed_wave # the tool's spelling
qmk-rgb-tool effect logo "fixed wave" # the file's spelling
info and the effect list report the spelling the file uses, so the output
carries fixed wave and breathe where this document's own prose uses the tool's
fixed_wave and breathing. The ID beside the name is the same in both. The file
is also inconsistent with itself across channels — breathing on backlight,
breathe on logo and side — and the tool reports each channel as written
rather than picking one.
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,
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. error
carries omitempty, so a zone that read fine has no error key at all rather
than "error": null — a parser must treat a missing key and an empty string as
"no error". Every other field above is always present.
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, and nothing has to be registered to make a board work: what a board is called and what its channels are called comes from the board or from its definition file.
That signature says nothing about lighting, so a keyboard with no VIA lighting
channel at all is listed and then fails every command that opens it with
this keyboard exposes no VIA lighting channels.
A definition also names its channels, and that is where the name comes from —
there is no other place. They are the names VIA shows, so the tool and VIA call
a channel the same thing, and a name is matched ignoring case. On a board whose
definition writes Backlight, these three reach the same channel:
brightness backlight 160, brightness Backlight 160 and
brightness rgb_matrix 160. The label is the name, and the subsystem stays
accepted because it follows from the channel number. qmk-rgb-tool
keyboard definitions prints the label beside the subsystem it stands for, and
says where each definition came from — user for a file in the per-user
directory, built-in for one compiled into the binary:
Definitions
user /Users/you/Library/Application Support/qmk-rgb-tool/definitions (your user directory)
built into this binary
Impact 80 (0x36B0/0x309F) built-in definitions/impact80.json
logo rgblight 7 effects
Backlight rgb_matrix 46 effects
side audio 7 effects
When a file in the user directory describes a board the binary also carries, both
appear, and the user one is marked (shadows the built-in copy) — the two files
disagree and only the one in the user directory is read. In --json that is the
source field on each entry, "user" or "built-in".
The name of a keyboard comes from two places, in this order: the definition file,
which is the manufacturer's own name for the board, and the USB product string
the keyboard reports itself. On this board both say Impact 80. A keyboard that
reports no product string and has no definition is shown as unknown model, and
that costs the name only — the QMK subsystem names still address its channels.
keyboard info reports whether this tool has effect names for a board, because
that differs from one keyboard to the next: one may have a definition, the next
may not. It does not list the board's channels — that needs the board, and this
command deliberately does not open it.
$ qmk-rgb-tool keyboard info
1 Impact 80 0x36B0/0x309F (effect names)
With --json it reports the same devices, one name, the two identifiers, a
path, and effects — whether this tool has effect names for that board, which is
what replaced the known field. known used to say "keyboards.json lists this
board", and that file is gone, so the field would have been unanswerable:
{"devices": [{"index": 1, "path": "/dev/hidraw7", "vendorId": 14000,
"productId": 12447, "name": "Impact 80", "effects": true}],
"total": 1}
| Command | Description |
|---|---|
qmk-rgb-tool keyboard info |
Discover connected QMK keyboards |
qmk-rgb-tool enable <zone> |
Enable the named lighting zones |
qmk-rgb-tool disable <zone> |
Disable the named lighting zones |
qmk-rgb-tool info [zone] |
Show per-zone RGB state; without a zone, every channel |
qmk-rgb-tool effect <zone> [name\|index] |
Set an effect by name, or a raw effect index 0–255, verified by read-back; a board that does not implement the index clamps it to the highest it does |
qmk-rgb-tool effect <zone> |
With no name, list the effects that zone has; effect all lists every channel |
qmk-rgb-tool keyboard fetch |
Download the VIA definition for the connected keyboard |
qmk-rgb-tool keyboard definitions |
List every definition in use, from the per-user directory and built into the binary, each marked with which it is |
qmk-rgb-tool brightness <zone> <val> |
Set brightness (0–255) on the named zones, verified by read-back |
qmk-rgb-tool speed <zone> <val> |
Set effect speed (0–255) on the named zones, verified by read-back; on logo and side only 0, 1 and 4 are reachable |
qmk-rgb-tool color <zone> <hex> |
Set color (e.g. ff0000) on the named zones |
qmk-rgb-tool color <zone> rgb:<hex> |
The same hex color, written out |
qmk-rgb-tool color <zone> hsv:<h>,<s>,<v> |
Set hue and saturation (0–255) and write v to the brightness of the same zones |
qmk-rgb-tool <command> <zone> ... |
Name the channels: backlight, rgblight, rgb_matrix, audio or led_matrix, the name the board's definition gives it, several comma separated, or all for every channel the keyboard reports. A command that writes lighting cannot be called without one |
qmk-rgb-tool --device <n> ... |
Target keyboard by number (see keyboard info) |
qmk-rgb-tool --definition <path> |
Read effect names from this VIA definition file instead of the one in the data directory; applies to every command that resolves names, and a file for another board is refused |
qmk-rgb-tool --json |
Print JSON instead of text, for keyboard info, info, list, the effect list and keyboard definitions |
qmk-rgb-tool -v, --version |
Print the version |
qmk-rgb-tool save [name\|file] |
Save the current state of every channel as <name>.json in the per-user profiles/ (name lowercased, non-[a-z0-9-_] mapped to -, a leading - prefixed with unnamed-); without a name it writes default. An argument ending in .json is a path, and that file is written instead, named after the file's own name, in a directory that has to exist. It reports the file it wrote, and annotates a path in the per-user directory as such |
qmk-rgb-tool load <name\|file> [zone] |
Load and apply a profile, by name from the per-user profiles/ or from a path when the argument ends in .json; without a zone it applies the profile to every channel it names, with one it applies it to the named channels only |
qmk-rgb-tool list |
List saved profiles |
qmk-rgb-tool delete [name] |
Delete a saved profile; without a name it deletes default. A name only, so an argument ending in .json names the profile without the dots rather than a file to remove. It reports the file it removed |
qmk-rgb-tool completion <shell> |
Write an autocompletion script for bash, zsh, fish or powershell to stdout |
enable and disable take a zone and nothing else. brightness, speed and
color take a zone and a value. effect takes a zone and, optionally, an effect
name or a raw effect ID: two arguments set it, one lists that zone's effects, so
its form is effect <zone> [name|index]. A message that points at the ID form
spells out both arguments — effect <zone> <index> — because the zone is
required, and an index written in the zone's place is read as a channel named
after it.
info takes at most a zone, load a profile name and at most a zone, save and
delete an optional name, and list nothing. list and the keyboard
subcommands reject a stray token, which is how a mistyped invocation is caught
before it looks like a command that did its work.
A zone is an argument and not a flag, so a command that cannot address a channel —
keyboard info, list, delete — has no way to be handed one, and its help says
nothing about channels. That is the whole reason it is spelled this way: a flag on
the root is a flag every help lists, and it was ignored rather than refused
wherever it did not apply.
keyboard info, info, list, keyboard definitions and the effect list print
text, because a person reads them. Pass --json for the machine shape, which is
the same data with the same field names as before:
qmk-rgb-tool info
qmk-rgb-tool --json info
qmk-rgb-tool info --json # the flag is persistent, both spellings work
--definition <path> names the definition file to read instead of the one in
the data directory, and applies to every command that resolves effect names.
Profiles store RGB state (effect, brightness, speed, color) per channel as JSON
files, one per profile. A name lives in the per-user profiles/ directory and is
written and read there, so save <name> and load <name> always agree, and a
checkout's own profiles/ directory is not searched for a name — you reach it by
naming the file, with load profiles/lava.json or save profiles/lava.json. A
profile also records the keyboard it was saved from, because the effect names in
it are that board's:
"board": { "vendorId": "0x36B0", "productId": "0x309F" }
Loading a profile onto another keyboard still works — names that do not exist
there are reported per key and skipped — but it says which profile belongs to
which keyboard. A profile written before this field existed has no board and is
loaded without complaint. 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. load applies the zones the profile names, and a second argument
narrows that to the channels it names. 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
definitions/ # VIA definition files built into the binary with go:embed
internal/device/ # HID discovery
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.