AGENTS.md 12 KB

Agent Guidelines — QMK RGB Tool

Objective

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

Architecture

cmd/                    # Cobra-based CLI entrypoints
internal/device/        # QMK Raw HID discovery and keyboard numbering
internal/via/           # VIA protocol implementation + LED subsystem
internal/rgb/           # Effects, values, color definitions
keyboards.json          # Optional name/metadata lookup for known keyboards

CLI Interface

qmk-rgb-tool keyboard info
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 <val>       # 0-255, verified by read-back
qmk-rgb-tool speed <val>            # 0-255, verified by read-back
qmk-rgb-tool color <hex>            # Six hexadecimal digits
qmk-rgb-tool mode <index>           # Raw zone-specific effect ID
qmk-rgb-tool enable
qmk-rgb-tool disable
qmk-rgb-tool info
qmk-rgb-tool save <name>        # Save current RGB state to a profile
qmk-rgb-tool load <name>        # Load a profile and apply it to the keyboard
qmk-rgb-tool delete <name>      # Delete a saved profile
qmk-rgb-tool list               # List saved profiles
qmk-rgb-tool --device <n> ...   # Target keyboard number from `keyboard info`

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; an absent or malformed file costs names, not the ability to drive the keyboard.

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 accepts logo, backlight, or side and may be given on any subcommand. 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 Logo, Backlight, and Side in that order. With --zone, commands target exactly one zone. Unsupported default targets are skipped with a stderr warning; unsupported explicit-zone effects and unknown names fail before the device is opened.

Impact 80 Effect Families

Logo and Side use IDs 0–6: none, wave, fixed_wave, spectrum, breathing, light, and shutdown.

Backlight uses the complete ID 0–45 family:

none, solid_color, alphas_mods, gradient_up_down, gradient_left_right,
breathing, band_sat, band_val, band_pinwheel_sat, band_pinwheel_val,
band_spiral_sat, band_spiral_val, cycle_all, cycle_left_right,
cycle_up_down, cycle_out_in, cycle_out_in_dual, rainbow_moving_chevron,
cycle_pinwheel, cycle_spiral, dual_beacon, rainbow_beacon,
rainbow_pinwheels, flower_blooming, raindrops, jellybean_raindrops,
hue_breathing, hue_pendulum, hue_wave, pixel_flow, digital_rain,
solid_reactive, solid_reactive_wide, solid_reactive_multiwide,
solid_reactive_cross, solid_reactive_multicross, solid_reactive_nexus,
solid_reactive_multinexus, splash, multisplash, solid_splash,
solid_multisplash, starlight, starlight_dual_hue, starlight_dual_sat,
riverflow

ID 39 is multisplash; ID 41 is solid_multisplash. Supported aliases are off → none, breathe → breathing, rainbow (zone-dependent), rainbow_wave (Logo/Side), solid (zone-dependent), and legacy static. Do not assume a numeric effect ID is valid on every zone; use the zone-aware name resolver or deliberately use mode as a raw escape hatch.

info emits a zones array containing each selected zone's channel, enabled state, effect name and ID, brightness, speed, and color. The top-level summary comes from the first selected zone. A failed zone includes an error field while other zone results remain available; the command prints JSON before returning non-zero.

VIA Protocol Essentials

  • Transport: QMK Raw HID over a 32-byte hidraw report, report number 0
  • Message types:
    • 0x07 — Custom set value
    • 0x08 — Custom get value
  • Impact 80 lighting channels: 0x02 logo, 0x03 backlight, 0x04 side
  • RGB value IDs: brightness 0x01, effect 0x02, speed 0x03, color 0x04
  • No separate enable handshake is used

Keyboards (Known VID/PID)

Keyboard VID PID
Wobkey Rainy 75 0x6666 0x0001
Wobkey Impact 80 0x36B0 0x309F

keyboards.json maps VID+PID → display name. Only name is read by the code; unknown fields in the file are ignored, so entries may carry extra keys without affecting discovery.

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 (usually $GOBIN/$module_name) 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 — logo, backlight, side must be identical in code, docs, and examples.

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 collapse any speed above 0 to 4, while backlight scales brightness up to 255 and applies speed as given.

brightness and speed therefore 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.

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.

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, built into Go toolchain (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).