# 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. ## Installation On Linux, `libudev-dev` is required before building: ```bash sudo apt install -y libudev-dev ``` Build locally: ```bash git clone 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/`): ```bash 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 or a profile from a checkout, put the file in the per-user directory, or pass `--definition`; both win over what the binary carries. ## Setup 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: ```bash 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 each 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 ` followed by `load ` finds what was just written and a same-named file in a checkout cannot be loaded in its place. 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](#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 `. The Wobkey Impact 80's file is vendored in the repository's [`definitions/`](definitions/README.md) 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`, `effect ` and `info` — only the effect names are missing, and [Effect Names Are Per Board](#effect-names-are-per-board) says what that costs and what the file does and does not tell the tool. ## Shell Completion `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 --help` for the path your platform expects; for bash and zsh on Linux: ```bash # 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`: ```zsh fpath=("$HOME/.zsh/completions" $fpath) autoload -Uz compinit && compinit ``` 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: ```bash $ qmk-rgb-tool effect --zone # the QMK subsystem names and the board's own $ qmk-rgb-tool effect --device # the numbers `keyboard info` prints $ qmk-rgb-tool load # 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. ## Usage ```bash # 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 # A raw effect index is zone-specific; ID 17 is Backlight-only ./qmk-rgb-tool effect 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. 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. `--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: ```jsonc // 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": "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 --list` 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. ## Selecting a Keyboard `keyboard info` numbers every connected QMK keyboard starting at 1, and `--device` takes that number: ```bash ./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 Notations `color` accepts three notations for one operation: ```bash 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: ```console $ 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. ## Effect Names Are Per Board The keyboard holds effect numbers, not names, so `effect ` 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/`](definitions/README.md) 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](#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 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, never that the name labels it correctly. ### ID 46 Has No Name 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 46 --zone backlight 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 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](https://drive.wobkey.com/f/d/6BtO/Impact_80.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 46` is the only way to reach it. A keyboard without a catalog is still driven: `brightness`, `speed`, `color`, `effect ` 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 names for this keyboard: run `keyboard fetch` for its VIA definition, or set an effect by number with `effect ` $ qmk-rgb-tool enable 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 ` $ 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 ` ``` `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 ``` ### Where Effect Names Can Come From VIA's own keyboard collection, [`the-via/keyboards`](https://github.com/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`](https://github.com/the-via/reader/blob/master/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 — and its list runs to 45 because it adds `flower_blooming`, `starlight`, `starlight_dual_hue`, `starlight_dual_sat` and `riverflow`, none of which the built-in list has. 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`](https://drive.wobkey.com/f/d/6BtO/Impact_80.JSON) | | VIA firmware image | [`impact_80.bin`](https://drive.wobkey.com/f/d/9yH8/impact_80.bin) | | Update instructions, vendor's warning against updating a working board | [Driver & Firmware page](https://wiki.wobkey.com/en/Products/PMOKEY-Impact-80/Driver-Firmware) | 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](https://codeberg.org/OpenRGB/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`](https://github.com/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. ## Brightness and Speed Are Not Applied Verbatim 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: ```console $ 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. ## Features - **Cross-platform** — Linux, macOS, Windows via [hidapi](https://github.com/libusb/hidapi) - **Channel-aware effects** — per-channel effect control with per-board effect catalogs, so a board without one is still driven - **Profile system** — save, load, list, and delete RGB presets as JSON files in the per-user `profiles/` directory, which is both where they are written and where they are read - **Effect names from VIA definition files** — the vendor's own names, read at runtime; no name is hand-written, so there is one source per board, and a file the user places overrides the one built in - **Compatibility aliases** — `off`, `breathe`, `rainbow`, `rainbow_wave`, `solid`, `static` resolve to correct effect IDs per channel - **Reactive & splash effects** — honor `color` and `speed` for key-press illumination - **Machine-parseable output** — text by default, JSON behind `--json` for `keyboard info`, `info`, `list`, `effect --list` and `keyboard definitions` - **Agent-friendly** — designed for automation, scripting, and CLI-first workflows - **Multiple devices** — `keyboard info` numbers each keyboard; `--device ` targets one ## Impact 80 Zones and Effects | 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: | 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` | | | | 46 | *unnamed* — the board takes it, the file stops at 45 | ## Effect Behavior 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 `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: - **Static** effects (`solid_color`, `none`) display a steady output. The color is set by `color` and is constant. - **Reactive** effects (`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** effects (`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. - **Dynamic/stream** effects (`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** effects (`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** effects (`gradient_up_down`, `gradient_left_right`) create a color gradient across the keyboard. They do not honor `color`. - **Alphas mods** (`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 `. 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. ## Compatibility Aliases 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`. `off` is accepted on every channel, 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 breathe --zone logo # the file's spelling qmk-rgb-tool effect breathing --zone logo # the tool's spelling qmk-rgb-tool effect fixed_wave --zone logo # the tool's spelling qmk-rgb-tool effect "fixed wave" --zone logo # the file's spelling ``` `info` and `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/`](https://github.com/qmk/qmk_firmware/tree/master/quantum/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. ## Platform Setup (udev rules and permissions) The tool uses [hidapi](https://github.com/libusb/hidapi) for cross-platform HID access. Most platforms need no configuration, but Linux requires extra setup. ### Linux To build, `libudev-dev` is required: ```bash 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. #### Option 1: Quick (Per Session) Grant your user group access to all HIDRAW devices: ```bash sudo chown root:adm /dev/hidraw* sudo chmod 660 /dev/hidraw* ``` This works for the current session only. Permissions reset after reboot. #### Option 2: Permanent (udev Rule) Create a persistent rule for the Impact 80: ```bash 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. ### macOS No setup required. macOS applications access HID devices directly through IOKit. ### Windows 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). ## Finding a Keyboard 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`, all of `--zone backlight`, `--zone Backlight` and `--zone rgb_matrix` reach the same channel: the label is the name, 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. ```console $ 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: ```json {"devices": [{"index": 1, "path": "/dev/hidraw7", "vendorId": 14000, "productId": 12447, "name": "Impact 80", "effects": true}], "total": 1} ``` ## CLI Reference | 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 | | `qmk-rgb-tool effect ` | 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` | With no argument, list every effect per channel | | `qmk-rgb-tool effect --list` | The same list, as a flag | | `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 ` | Set brightness (0–255) on selected zones, verified by read-back | | `qmk-rgb-tool speed ` | 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 ` | Set color (e.g. `ff0000`) on selected zones | | `qmk-rgb-tool color rgb:` | The same hex color, written out | | `qmk-rgb-tool color hsv:,,` | Set hue and saturation (0–255) and write `v` to the brightness of the same zones | | `qmk-rgb-tool --zone ...` | Target one channel: `backlight`, `rgblight`, `rgb_matrix`, `audio` or `led_matrix`, or the name the board's definition gives it | | `qmk-rgb-tool --device ...` | Target keyboard by number (see `keyboard info`) | | `qmk-rgb-tool --definition ` | 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`, `effect --list` and `keyboard definitions` | | `qmk-rgb-tool -v`, `--version` | Print the version | | `qmk-rgb-tool save [name]` | Save current RGB state as `.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` | | `qmk-rgb-tool load [name]` | Load and apply a profile by name from the per-user `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 ` | 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` and `color` require exactly one argument. `effect` takes one argument too, and accepts either a name or a raw effect ID. `keyboard info`, `info`, `list`, `keyboard definitions` and `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: ```bash qmk-rgb-tool info qmk-rgb-tool --json info qmk-rgb-tool info --json # the flag is persistent, both spellings work ``` `--definition ` names the definition file to read instead of the one in the data directory, and applies to every command that resolves effect names. ## Profiles Profiles store RGB state (effect, brightness, speed, color) per channel as JSON files, one per profile, in the per-user `profiles/` directory. They are written there and read there, so `save ` and `load ` always agree and a checkout's own `profiles/` directory is not used. A profile also records the keyboard it was saved from, because the effect names in it are that board's: ```json "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. 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. ## Architecture ``` cmd/qmk-rgb-tool/ # Cobra-based CLI internal/device/ # HID discovery internal/hid/ # Cross-platform HID access (hidapi) internal/rgb/ # Effects, colors, state internal/via/ # VIA protocol implementation ``` ## Protocol Communicates via the QMK Raw HID interface (`Usage Page 0xFF60`, `Usage 0x61`) with 32-byte reports, report number `0`. - `0x07` — Custom set value - `0x08` — Custom get value - Lighting channels are QMK's `id_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. - RGB values are identical for every lighting subsystem: brightness `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.