AGENTS.md 14 KB

Agent Guidelines — QMK RGB Tool

Read README.md First

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.

Objective

Cross-platform Go CLI library for programmatic/agent-friendly control of QMK keyboard RGB lighting.

Architecture

Device Selection

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.

Stack

  • Go 1.26+ (cross-platform: Linux, macOS, Windows)
  • github.com/sstallion/go-hid for HID access
  • github.com/spf13/cobra for CLI framework

Constraints

  • CLI-first, no GUI — designed for automation and agent consumption
  • Output should be machine-parseable (JSON when possible)
  • Cross-platform from day one

Consistency

Documentation describes real, verifiable behavior. Names are only the start — behavior counts too. Before any edit:

  1. Binary name — the go build -o target is qmk-rgb-tool, which is not the module's last path element. It must match every example in README, AGENTS.md, comments, and docs.
  2. CLI command — the root command name (e.g. qmk-rgb-tool) is the binary name; every usage example, CLI reference table, and doc must use the same string.
  3. Module path — the go.mod module path is the source of truth for go install targets.
  4. Zone names — the vocabulary is QMK's subsystem names, so they must be identical in code, docs, and examples: backlight, rgblight, rgb_matrix, audio, led_matrix. A board's own names (logo, backlight, side on the Impact 80) are data in keyboards.json, not code and not part of the vocabulary: do not hardcode one into Go, and do not put one in a --zone help string.

Documented behavior must also match the code:

  1. Flags and accepted values — what a flag accepts and rejects, its type, its default, and whether it persists across commands. A doc promising --device 1 must not accept a path.
  2. Error messages and exit codes — the exact strings a user is told to look for, which error takes precedence when several apply, and when JSON is still printed before a non-zero exit.
  3. JSON output shape — field names, types, and whether a field is always present, omitted, or null. Agents and scripts parse this.
  4. Ranges, defaults, and stability — accepted ranges, default values, boundary behavior, and any ordering or stability guarantee together with the scope it holds over (a device number is stable for one session, not across reboots).

Firmware Transforms Values

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, speed, color, effect and mode read every selected zone back and print what was actually applied; where all zones match they print the plain success line, 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. All of them read back through the shared readBackValues, so a future parameter needs no new read-back path — but the helper above it is not as shared as it looks. setValueVerified writes one value to every channel, which fits brightness and speed and fits nothing else: effect carries a different ID per channel, because one effect name is a different index on each subsystem, and so has its own setEffectVerified. Assume the next parameter needs its own wrapper, and share only the read.

A board can refuse an ID that looks valid. The Impact 80's logo and side channels read effect ID 0 as "lighting off" and leave the mode register alone, so effect none there does nothing to the effect and is reported as the effect still running. disable is the command that turns a channel off, because it also writes brightness 0. Above the top the behaviour is the opposite: the backlight channel does not refuse ID 47 or ID 99 but clamps it to the 46 it holds, so mode there is reported as the index the keyboard ended up with. Read the register back; never report the number that was asked for.

The effect names are a board's catalog in internal/rgb/catalog.go, which transcribes the arrays internal/rgb/impact80.go holds 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. The backlight list is the QMK catalog of the VIA era, not of current QMK master: the two differ in length, naming and numbering, so upstream is not a repair guide for it. The logo and side lists are not QMK's at all. The live register proves which IDs a board takes, never which name belongs to one, and the board takes one ID more (46 on the backlight) than the catalog names.

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.

Read the Firmware When Something Is Unclear

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. For the Impact 80 that JSON is https://drive.wobkey.com/f/d/6BtO/Impact_80.JSON, reached from the vendor's Driver & Firmware page; the page is the place to re-read, because it also carries what a wrong flash does to this tool: the vendor ships a proprietary firmware alongside the VIA one, it disables VIA completely, and a board on it is invisible rather than unsupported. It also documents that VIA only sees the board in wired mode. A keyboard the tool cannot find is therefore not yet evidence of a bug here.

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.

Code vs Documentation

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.

Go Development

Tooling

  • 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.

Style (Effective Go + Code Review Comments)

  • Formatting: always gofmt or goimports. Tabs for indentation, no line-length limit but avoid uncomfortably long lines.
  • Names: MixedCaps / mixedCaps, no underscores. Short local variables (i, c, r). Descriptive for globals.
  • Initialisms: consistent case — URL, ID, HTTP → appID, urlPony, ServeHTTP.
  • Comments: doc comments start with the name, end with period. // Package rgb provides RGB effect handling.
  • Imports: standard library first, blank line, then third-party. Grouped.

Error Handling

  • Return errors, never panic for normal control flow.
  • Indent error flow early — happy path at minimal indentation: 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).