package rgb import ( "fmt" "netdome.biz/paul/qmk-rgb/internal/via" ) // EffectTarget is one effect ID to write to one channel. type EffectTarget struct { Channel via.Channel ID uint8 } const unknownEffectName = "unknown" // Catalog is one board's effect names, per channel. The keyboard holds numbers, // not names, so `effect ` needs a catalog and a board without one is // driven through raw IDs. type Catalog struct { board string names map[via.Channel][]string aliases map[via.Channel]map[string]string defaults map[via.Channel]uint8 } // CatalogFor returns the effect catalog of a board, and whether one exists. // // Provenance, because these lists look invented and are not: // // - The 46 backlight names are QMK's rgb_matrix_effects.inc in order, each ID // verified against the live register of an Impact 80. // - The 7 logo and 7 side names are this board's vendor VIA definition, whose // dropdowns read "fixed wave" and "breathe". The tool spells them // fixed_wave and breathing and accepts the vendor's spellings as aliases. // - The brightness and speed transforms documented in README.md were measured // on the unit, not read from anywhere. func CatalogFor(vendorID, productID uint16) (*Catalog, bool) { if vendorID != 0x36B0 || productID != 0x309F { return nil, false } return &Catalog{ board: "impact80", names: map[via.Channel][]string{ via.ChannelRgblight: impact80LogoEffects[:], via.ChannelRgbMatrix: impact80BacklightEffects[:], via.ChannelAudio: impact80SideEffects[:], }, aliases: map[via.Channel]map[string]string{ via.ChannelRgblight: { "off": "none", "breathe": "breathing", "rainbow": "spectrum", "rainbow_wave": "wave", "solid": "light", "static": "solid", }, via.ChannelRgbMatrix: { "off": "none", "breathe": "breathing", "rainbow": "rainbow_moving_chevron", "solid": "solid_color", "static": "solid", }, via.ChannelAudio: { "off": "none", "breathe": "breathing", "rainbow": "spectrum", "rainbow_wave": "wave", "solid": "light", "static": "solid", }, }, defaults: map[via.Channel]uint8{ via.ChannelRgblight: 4, via.ChannelRgbMatrix: 5, via.ChannelAudio: 4, }, }, true } // Name returns the catalog's board name, which `effect --list` reports. func (c *Catalog) Name() string { if c == nil { return "" } return c.board } // Names returns the effect names of a channel, or nil when the catalog says // nothing about it. func (c *Catalog) Names(ch via.Channel) []string { if c == nil { return nil } return c.names[ch] } // EffectName returns the name of an effect ID, or "unknown" when the catalog has // no entry for it. func (c *Catalog) EffectName(ch via.Channel, id uint8) string { if c == nil { return unknownEffectName } names := c.names[ch] if int(id) >= len(names) { return unknownEffectName } return names[id] } // EffectID resolves an effect name on a channel, following one level of alias. func (c *Catalog) EffectID(ch via.Channel, name string) (uint8, bool) { if c == nil { return 0, false } for id, candidate := range c.names[ch] { if candidate == name { return uint8(id), true } } if canonical, ok := c.aliases[ch][name]; ok { for id, candidate := range c.names[ch] { if candidate == canonical { return uint8(id), true } } } return 0, false } // DefaultEffect returns the effect ID enable writes when turning a channel on. func (c *Catalog) DefaultEffect(ch via.Channel) (uint8, bool) { if c == nil { return 0, false } id, ok := c.defaults[ch] return id, ok } // ResolveEffect turns an effect name into one target per channel that supports // it, plus the subsystem names of those that do not. A skip is an error rather // than a warning when the caller asked for one channel explicitly, because a // command that silently did nothing looks like a command that worked. That is // the caller's knowledge to pass: a board with a single lighting channel is not // an explicit request for it. func ResolveEffect(catalog *Catalog, name string, channels []via.Channel, explicit bool) ([]EffectTarget, []string, error) { if catalog == nil { return nil, nil, fmt.Errorf("no effect catalog for this keyboard; set an effect by number with `mode `") } // The compatibility spelling every catalog shares, so a caller cannot // resolve "static" differently from another. if name == "static" { name = "solid" } known := false for _, ch := range channels { if _, ok := catalog.EffectID(ch, name); ok { known = true break } } if !known { return nil, nil, fmt.Errorf("unknown effect: %s", name) } var targets []EffectTarget var skipped []string for _, ch := range channels { id, ok := catalog.EffectID(ch, name) if !ok { if explicit { return nil, nil, fmt.Errorf("effect %s is not supported on %s", name, ch.Subsystem()) } skipped = append(skipped, ch.Subsystem()) continue } targets = append(targets, EffectTarget{Channel: ch, ID: id}) } return targets, skipped, nil }