README.md is the single source of truth for this project's behaviour: the
command surface, the effect catalog, zone semantics, the protocol, the keyboard
list and platform setup. Read it in full before you work on this repository.
This file deliberately does not repeat any of it. A second copy is a second
thing that can go stale, and that has happened: a command prefix that had not
existed for many commits survived in eleven lines of prose, and a
keyboards.json field was documented here as load-bearing while the code never
read it. So the split is:
README.md — what the software does. Reference.AGENTS.md — how to work on it. Discipline..claude/skills/qmk-rgb/SKILL.md — the live-query workflow for the tool.For the effect catalog specifically, read it from no document at all. Run
qmk-rgb-tool effect --list and use the output. Logo and Side accept fewer
effects than Backlight, and only the tool knows what the current firmware
supports.
Cross-platform Go CLI library for programmatic/agent-friendly control of QMK keyboard RGB lighting.
cmd/ # Cobra-based CLI entrypoints
internal/device/ # QMK Raw HID discovery and keyboard numbering
internal/hid/ # Cross-platform HID access
internal/via/ # VIA protocol implementation + LED subsystem
internal/rgb/ # Effects, values, color definitions
keyboards.json # Optional name/metadata lookup for known keyboards
Discovery matches every connected keyboard exposing the QMK Raw HID signature
(Usage Page 0xFF60, Usage 0x61) — the same check qmk/qmk_udev performs.
keyboards.json is only consulted to attach a name to a recognized model and to
name its channels; an absent or malformed file costs names, not the ability to
drive the keyboard.
No command validates --zone or --device before the command that needs it
does. A root-level pre-run would make keyboard info, list, delete and
completion demand a keyboard, and the one you run to choose a keyboard cannot
require that you have chosen one. Where two errors apply, the board wins: a
keyboard that cannot be selected is reported before a zone name, because the
vocabulary of a board cannot be known without the board.
keyboard info numbers the connected keyboards from 1 in a stable order
(vendor ID, product ID, path). --device accepts that number, never a HID path.
Without --device, commands proceed only when exactly one keyboard is connected;
otherwise they fail and list the selectable numbers, so a command never targets
an unintended keyboard. Device numbers are stable for the current session only —
HID paths are reassigned on reboot and most keyboards report no serial number.
--zone takes a VIA lighting channel, named by its QMK subsystem, and a board
in keyboards.json may give a channel a display name. README.md carries the
vocabulary. It is a persistent flag in cobra's sense only — accepted on the
root and inherited by subcommands — and nothing is remembered between runs, so
it must be repeated on every invocation. Without --zone, commands target every
channel the keyboard reports, in channel order; the channels are discovered by
asking the keyboard, not assumed. With --zone, commands target exactly one
channel. A default command skips a target that does not support an effect and
warns on stderr; an explicitly named channel that does not support it, and an
unknown name, fail before the device is opened.
github.com/sstallion/go-hid for HID accessgithub.com/spf13/cobra for CLI frameworkDocumentation describes real, verifiable behavior. Names are only the start — behavior counts too. Before any edit:
go build -o target (usually $GOBIN/$module_name) must match every example in README, AGENTS.md, comments, and docs.qmk-rgb-tool) is the binary name; every usage example, CLI reference table, and doc must use the same string.go.mod module path is the source of truth for go install targets.logo, backlight, side must be identical in code, docs, and examples.Documented behavior must also match the code:
--device 1 must not accept a path.null. Agents and scripts parse this.The Impact 80 firmware rescales or clamps brightness and speed per channel, so
an accepted 0–255 request is not the value the keyboard holds. logo and side
cap brightness at 160 and have only three reachable speeds — 0 freezes the
animation, 1 is the slowest movement, anything above 1 becomes 4 — while
backlight scales brightness up to 255 and applies speed as given. README.md
tabulates the per-channel behaviour; the rule that follows from it is the part
that matters when you write code here:
brightness and speed read every selected zone back and print what was
actually applied; where all zones match they print Brightness set to N or
Speed set to N, and where any zone differs they print one summary line naming
each zone's real value and the request. Never let a command report success for a
value the keyboard did not accept. Both read back through the shared
setValueVerified, so a future parameter needs no new read-back path.
The effect names are a board's catalog in internal/rgb/catalog.go, transcribed
from the two sources its comment names. They look like data the tool invented and
are not; do not edit a list on a hunch, and do not add a second one.
When you see a name or a behavior in one file, grep for it across the whole repo before deciding if a change is consistent.
Unclear firmware behaviour is answered by reading source, not by inferring it from names, from a general QMK assumption, or from a doc line that nobody has checked. Two sources settle nearly everything, and both are cheap to reach.
Upstream QMK is public: quantum/rgb_matrix/rgb_matrix.h for the value API,
quantum/rgb_matrix/animations/ for what an effect actually does to hue,
saturation and value. The vendor's VIA definition JSON for the board settles
what the interface exposes at all — which custom value IDs exist, which ranges
they take, and for which effects a color control is offered. It is the authority
on the wire format, more than upstream is, because the ID mapping is the
vendor's.
The board's own firmware is a vendor binary with no public source, so both sources describe the family, not the unit on the desk. Confirm against the hardware where that is cheap, then put the observation in README.md.
One trap this session paid for: the vendor channels are not all the same QMK
subsystem. One is rgb_matrix, one is the lightweight rgblight, and one is
the audio effect subsystem. An upstream file that governs one channel does not
govern another, so "the QMK source says" has to name the channel it was read
for. A formula from the wrong subsystem is how this repo ended up claiming that
speed 0 and 1 behaved identically.
All documentation (README.md, comments, AGENTS.md) must stay in sync with the code. When code and documentation conflict, ask the user before deciding which one to change. Do not silently pick a winner — explicitly state the conflict and get direction.
gopls — Go Language Server, installed separately with go install golang.org/x/tools/gopls@latest (LSP, diagnostics, go-to-def, rename, references)gofmt / goimports — formatting. goimports adds/removes imports automatically. Always run before committing.go test — built-in test framework. Tests live in *_test.go files alongside source.go vet — static analysis. Run before committing.go build — compiles without installing. Fast, cached.go mod tidy — adds missing deps, removes unused ones. Run after every import change.gofmt or goimports. Tabs for indentation, no line-length limit but avoid uncomfortably long lines.MixedCaps / mixedCaps, no underscores. Short local variables (i, c, r). Descriptive for globals.URL, ID, HTTP → appID, urlPony, ServeHTTP.// Package rgb provides RGB effect handling.panic for normal control flow.go
if err != nil {
return err
}
// normal code
- Error strings: lowercase, no period — fmt.Errorf("device not found").
- Do not discard errors with _.
### Interfaces & Types
- Define interfaces in the consumer package, not the producer.
- Use pointer receivers when in doubt (mutation, large structs, sync fields).
- Return concrete types from constructors — let consumers mock if needed.
- Empty slices: var t []string (nil), not t := []string{} (non-nil), unless JSON encoding requires [].
### Concurrency
- Pass context.Context as first parameter. Never store in structs.
- Clear goroutine lifetimes — document when/why they exit. Avoid leaks via channels.
- Prefer synchronous functions — callers can add concurrency, not remove it.
### Testing
- Table-driven tests preferred for multiple cases.
- Failure messages: t.Errorf("Effect(%q) = %d, want %d", input, got, want).
- Use *_test.go files. Test package should match source package (internal tests).
- Example functions (func Example...) double as docs and tests.
### Modules & Packages
- Module path: github.com/<user>/<project> (when published).
- Package names: short, single-word, lowercase. No util, common, api, types.
- Zero values should be useful (e.g. bytes.Buffer, sync.Mutex).
## graphify
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
When the user types /graphify, use the installed graphify skill or instructions before doing anything else.
Rules:
- For codebase questions, first run graphify query "<question>" when graphify-out/graph.json exists. Use graphify path "<A>" "<B>" for relationships and graphify explain "<concept>" for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
- Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it.
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run graphify update . to keep the graph current (AST-only, no API cost).