# Impact 80 Zone-Aware Lighting Effects Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Expose all official Impact 80 lighting effects with zone-aware CLI targeting, defaulting every command to Logo, Backlight, and Side. **Architecture:** Put protocol-neutral zone names, channel IDs, effect catalogs, aliases, and resolution in `internal/rgb`. Keep `internal/via` responsible only for QMK Raw HID channel/value transport. Resolve target zones in the Cobra command layer, emit warnings for unsupported default targets, and keep `rgb info` machine-readable with a per-zone array plus a compatibility summary. **Tech Stack:** Go 1.26+, Cobra, existing Linux hidraw/QMK v12 transport, table-driven Go tests, fake transport, hardware smoke tests. **Spec:** `docs/superpowers/specs/2026-09-25-impact80-effects-design.md` ## Global Constraints - Target zones are `logo`, `backlight`, and `side`, mapped to VIA channels 2, 3, and 4. - Without `--zone`, commands target all three zones in Logo, Backlight, Side order. - With `--zone`, commands target exactly one zone and invalid values fail before opening HID. - Unknown effects fail before opening HID; explicit unsupported effects fail without a report; default unsupported effects are skipped with a stderr warning. - All writes use QMK v12 custom set `0x07`, 32-byte payloads, and validated acknowledgements. - Existing top-level `rgb info` fields remain a summary of the first selected zone. - No EEPROM save command, macOS/Windows backend, or Rainy 75 profile is added. - Follow TDD: each behavior gets a failing test, a minimal implementation, a passing test, and a commit. ## Review Focus - A backlight-only effect sent without `--zone` must never be sent to Logo or Side. - An explicit unsupported zone/effect combination must produce zero HID writes. - A malformed `--zone` value must fail before device discovery. - `rgb info` must return valid JSON even when one selected zone cannot be queried. - Every canonical Backlight ID 0–45 must map to the documented name, including distinct IDs 39 `multisplash` and 41 `solid_multisplash`. --- ### Task 1: Build the Zone and Effect Profile Domain **Files:** - Create: `internal/rgb/impact80.go` - Create: `internal/rgb/impact80_test.go` - Modify: `internal/rgb/effects.go` (remove the partial `Impact80EffectName`/`ParseImpact80Effect` implementation after the new API owns it) - Modify: `internal/rgb/effects_test.go` (retain HSV/color/QMK value tests; remove `TestImpact80EffectName` and `TestParseImpact80Effect`, which move to `impact80_test.go`) **Interfaces:** - Produces `type Zone string`, `AllZones() []Zone`, `ParseZone(string) (Zone, error)`, `(Zone).Channel() uint8`. - Produces `type EffectTarget struct { Zone Zone; ID uint8 }`. - Produces `ResolveEffect(name string, zones []Zone) ([]EffectTarget, []Zone, error)`, `EffectName(zone Zone, id uint8) string`, and `DefaultEffect(zone Zone) uint8`. - The resolver returns supported targets in the caller's zone order, skipped zones separately, and an error for unknown names. A one-zone request is treated as explicit and errors when unsupported; a three-zone request is treated as the default and skips unsupported zones. - [ ] **Step 1: Write the failing zone/catalog tests** Create table-driven tests with literal expectations: ```go func TestAllImpact80Zones(t *testing.T) { want := []Zone{ZoneLogo, ZoneBacklight, ZoneSide} got := AllZones() if !reflect.DeepEqual(got, want) { t.Fatalf("AllZones() = %v, want %v", got, want) } } func TestImpact80ZoneChannels(t *testing.T) { cases := []struct { zone Zone; want uint8 }{ {ZoneLogo, 2}, {ZoneBacklight, 3}, {ZoneSide, 4}, } for _, tc := range cases { if got := tc.zone.Channel(); got != tc.want { t.Errorf("%s.Channel() = %d, want %d", tc.zone, got, tc.want) } } } func TestResolveBacklightOnlyEffect(t *testing.T) { targets, skipped, err := ResolveEffect("rainbow_moving_chevron", AllZones()) if err != nil { t.Fatal(err) } if len(targets) != 1 || targets[0] != (EffectTarget{ZoneBacklight, 17}) { ... } if !reflect.DeepEqual(skipped, []Zone{ZoneLogo, ZoneSide}) { ... } } ``` Add a complete 0–45 Backlight table test, the seven Logo/Side values, aliases (`off`, `breathe`, `rainbow`, `rainbow_wave`, `solid`), explicit unsupported behavior, and `EffectName`/`DefaultEffect` tests. - [ ] **Step 2: Run the focused tests and verify RED** Run: `go test ./internal/rgb -run 'Test(AllImpact80|Impact80|Resolve)' -count=1` Expected: compile failure because `Zone`, `ResolveEffect`, and related functions do not exist yet. - [ ] **Step 3: Implement the typed profile and resolver** Implement `Zone`, channel mapping, canonical profile tables, alias resolution, and `ResolveEffect`. Use a table for every Backlight ID 0–45; do not derive names from numeric ranges. `ResolveEffect` must return zero targets plus an error for an unknown name, and must return an error for an explicit zone that does not support the requested name. - [ ] **Step 4: Run focused tests and verify GREEN** Run: `go test ./internal/rgb -run 'Test(AllImpact80|Impact80|Resolve)' -count=1` Expected: PASS. - [ ] **Step 5: Commit the domain profile** ```bash git add internal/rgb/impact80.go internal/rgb/impact80_test.go internal/rgb/effects.go internal/rgb/effects_test.go git commit -m "Add Impact 80 zone effect profiles" ``` ### Task 2: Add Zone Selection to the RGB Command Layer **Files:** - Modify: `cmd/wobkey/rgb/rgb.go:12-73` - Modify: `cmd/wobkey/rgb/rgb_test.go` - Create: `cmd/wobkey/rgb/zones.go` (small target resolver helper) **Interfaces:** - Adds persistent `--zone` flag with values `logo`, `backlight`, and `side`. - Produces `selectedZones() ([]intrgb.Zone, error)` and `zoneChannels([]intrgb.Zone) []via.LEDType` for all commands. - Validation occurs before `OpenDevice()`. - [ ] **Step 1: Write failing target-selection tests** ```go func TestSelectedZonesDefaultsToAll(t *testing.T) { old := targetZone defer func() { targetZone = old }() targetZone = "" got, err := selectedZones() // assert Logo, Backlight, Side } func TestSelectedZonesRejectsUnknownZone(t *testing.T) { targetZone = "matrix" if _, err := selectedZones(); err == nil { t.Fatal(...) } } ``` Also test explicit Logo/Backlight/Side and channel conversion `[2,3,4]`. - [ ] **Step 2: Run the focused tests and verify RED** Run: `go test ./cmd/wobkey/rgb -run 'TestSelectedZones|TestZone' -count=1` Expected: compile failure because `selectedZones` and `zoneChannels` do not exist. - [ ] **Step 3: Implement the persistent flag and resolver** Add `targetZone`, register `PersistentFlags().StringVar`, parse through `intrgb.ParseZone`, return `intrgb.AllZones()` for an empty value, and convert channel IDs to `via.LEDType` without importing CLI policy into `internal/via`. - [ ] **Step 4: Run focused tests and verify GREEN** Run: `go test ./cmd/wobkey/rgb -run 'TestSelectedZones|TestZone|TestDeviceFlag' -count=1` Expected: PASS. - [ ] **Step 5: Commit zone selection** ```bash git add cmd/wobkey/rgb/rgb.go cmd/wobkey/rgb/zones.go cmd/wobkey/rgb/rgb_test.go git commit -m "Add RGB zone selection" ``` ### Task 3: Make Effect Resolution Zone-Aware **Files:** - Modify: `cmd/wobkey/rgb/effect.go` - Create: `cmd/wobkey/rgb/effect_test.go` - Modify: `internal/via/protocol_test.go` **Interfaces:** - Consumes `selectedZones`, `intrgb.ResolveEffect`, and `via.SetValue`. - Produces `resolveEffectTargets(name string, zones []intrgb.Zone) ([]intrgb.EffectTarget, []intrgb.Zone, error)` for command-policy tests and the Cobra command. - Produces one Effect report for each supported target; default unsupported zones generate a warning; explicit unsupported zones return an error before opening HID. - [ ] **Step 1: Write failing command-policy tests** Test the pure command policy with a fake resolver or extracted helper, not a real device. Assert: ```go targets, skipped, err := resolveEffectTargets("rainbow_moving_chevron", intrgb.AllZones()) // target channel 3/id 17, skipped Logo and Side ``` Also assert `breathing` targets IDs 4, 5, 4 and that explicit `backlight` rejects a Logo-only effect. - [ ] **Step 2: Run the focused tests and verify RED** Run: `go test ./cmd/wobkey/rgb -run 'TestResolveEffectTargets' -count=1` Expected: compile failure because the command policy helper is missing. - [ ] **Step 3: Implement effect command policy** Validate zones and effect names before `OpenDevice`. Print a warning such as `effect not supported on zone(s): logo, side` to stderr when a default target is skipped. For each `EffectTarget`, call `proto.SetValue(via.LEDType(target.Zone.Channel()), uint8(intrgb.EffectID), target.ID)`. Preserve the success message only after all writes succeed. - [ ] **Step 4: Run focused tests and verify GREEN** Run: `go test ./cmd/wobkey/rgb -run 'TestResolveEffectTargets' -count=1` Expected: PASS. - [ ] **Step 5: Commit effect policy** ```bash git add cmd/wobkey/rgb/effect.go cmd/wobkey/rgb/effect_test.go git commit -m "Apply effects per lighting zone" ``` ### Task 4: Route All RGB Commands Through the Selected Zones **Files:** - Modify: `cmd/wobkey/rgb/brightness.go` - Modify: `cmd/wobkey/rgb/speed.go` - Modify: `cmd/wobkey/rgb/color.go` - Modify: `cmd/wobkey/rgb/disable.go` - Modify: `cmd/wobkey/rgb/enable.go` - Modify: `cmd/wobkey/rgb/mode.go` - Modify: `cmd/wobkey/rgb/rgb.go` (shared iteration helper) - Create: `cmd/wobkey/rgb/commands_test.go` **Interfaces:** - `brightness`, `speed`, and `color` send to every selected channel. - `disable` sends Effect `0` then Brightness `0` to every selected channel. - `enable` sends `intrgb.DefaultEffect(zone)` then Brightness `160` to every selected channel. - `mode ` sends the raw numeric ID to every selected channel. - [ ] **Step 1: Write failing selected-channel tests** Use the existing fake transport pattern to assert that a default operation produces reports for channels 2, 3, and 4, while `--zone side` produces only channel 4. Cover brightness, speed, color, disable, enable, and mode with table-driven expectations. - [ ] **Step 2: Run the focused tests and verify RED** Run: `go test ./cmd/wobkey/rgb -run 'TestSelectedChannel' -count=1` Expected: failure because commands still hard-code `RGBLight` or the test helper is absent. - [ ] **Step 3: Implement selected-zone iteration** Replace hard-coded channel calls with `zoneChannels(selectedZones)` and a small helper that stops on the first error. Do not duplicate the channel list in each command. - [ ] **Step 4: Run focused tests and verify GREEN** Run: `go test ./cmd/wobkey/rgb -run 'TestSelectedChannel' -count=1` Expected: PASS. - [ ] **Step 5: Commit command routing** ```bash git add cmd/wobkey/rgb/brightness.go cmd/wobkey/rgb/speed.go cmd/wobkey/rgb/color.go cmd/wobkey/rgb/disable.go cmd/wobkey/rgb/enable.go cmd/wobkey/rgb/mode.go cmd/wobkey/rgb/rgb.go cmd/wobkey/rgb/commands_test.go git commit -m "Target all RGB commands by zone" ``` ### Task 5: Expand `rgb info` to Per-Zone State **Files:** - Modify: `cmd/wobkey/rgb/info.go` - Modify: `cmd/wobkey/rgb/info_test.go` (new) - Modify: `internal/via/protocol_test.go` (add `TestGetValueReturnsTwoByteColor`) **Interfaces:** - Produces JSON with `zones` entries containing `zone`, `channel`, `enabled`, `effect`, `effectId`, `brightness`, `speed`, and `color`. - Retains top-level `enabled`, `mode`, `brightness`, and `speed` from the first selected zone. - Emits an `error` field for a zone that cannot be queried and exits non-zero after serializing available results. - [ ] **Step 1: Write failing JSON-shape tests** Test a successful zone record and an error record with hand-written expected structs/JSON keys. Assert the top-level summary comes from the first selected zone, not an implicit hard-coded channel. - [ ] **Step 2: Run the focused tests and verify RED** Run: `go test ./cmd/wobkey/rgb -run 'TestInfo' -count=1` Expected: compile failure because the zone info model/helper does not exist. - [ ] **Step 3: Implement per-zone queries** For each selected channel, query Brightness, Effect, Speed, and Color (`0x01`, `0x02`, `0x03`, `0x04`). Use `intrgb.EffectName(zone, effectID)` and `lightingEnabled(effectID, brightness)`. Preserve valid JSON when one zone fails; collect errors, print JSON, then exit non-zero. - [ ] **Step 4: Run focused tests and verify GREEN** Run: `go test ./cmd/wobkey/rgb -run 'TestInfo' -count=1` Expected: PASS. - [ ] **Step 5: Commit info output** ```bash git add cmd/wobkey/rgb/info.go cmd/wobkey/rgb/info_test.go internal/via/protocol_test.go git commit -m "Report RGB state for each zone" ``` ### Task 6: Documentation, Full Verification, and Hardware Acceptance **Files:** - Modify: `README.md` - Modify: `PLAN.md` - Modify: `AGENTS.md` - No generated graph files are staged **Interfaces:** - Documents the three zones, default-all behavior, explicit-zone behavior, unsupported-effect warning semantics, and the complete effect-name families. - [ ] **Step 1: Update documentation examples** Document `rgb effect breathing`, `rgb effect rainbow_moving_chevron`, `rgb effect rainbow_moving_chevron --zone backlight`, and the default-all behavior. Remove any example that implies a generic effect ID is valid on every zone. - [ ] **Step 2: Run all automated checks** Run: ```bash gofmt -w . go test -count=1 ./... go vet ./... go build -o /tmp/wobkey ./cmd/wobkey/ ``` Expected: all commands exit 0 and `git diff --check` is empty. - [ ] **Step 3: Run hardware smoke tests** Run: ```bash /tmp/wobkey keyboard info /tmp/wobkey rgb effect breathing /tmp/wobkey rgb effect rainbow_moving_chevron /tmp/wobkey rgb effect rainbow_moving_chevron --zone backlight /tmp/wobkey rgb info /tmp/wobkey rgb color 00ff00 /tmp/wobkey rgb disable /tmp/wobkey rgb enable ``` Expected: the Impact 80 raw interface is selected, unsupported default targets produce warnings only, explicit Backlight works, JSON contains three zones, and the final state is enabled. - [ ] **Step 4: Commit documentation and final verified implementation** ```bash git add README.md PLAN.md AGENTS.md git commit -m "Document zone-aware RGB commands" ``` Do not stage `graphify-out/` unless the user explicitly requests generated graph artifacts. ## Execution Handoff — 2026-09-25 - Tasks 1–5 and the documentation/automated-verification portions of Task 6 are implemented in `feature/impact80-effects` through commit `62852e7`. - All task reviews and the final whole-branch code review are approved. - `go test -count=1 ./...`, `go vet ./...`, `go build`, `gofmt`, and `git diff --check` pass. - Task 6’s live hardware acceptance remains open. The last run discovered the Impact 80 at `/dev/hidraw7`, but the device was root-owned mode `600`; it later disappeared, so the next operator must discover the current path. - Continue with the hardware procedure in `PLAN.md`, record the result, and merge the feature branch into `main` only after the three-zone JSON and final enabled state have been verified.