| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- package rgb
- import (
- "fmt"
- "strings"
- "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"
- // Effect is one named effect on a channel: the ID the keyboard uses and the name
- // a definition gives it. The pair is explicit because a VIA definition may attach
- // a number to an option that is not its position in the list, so a board's effect
- // ID is not an index into its names.
- type Effect struct {
- ID uint8
- Name string
- }
- // Catalog is one board's effect names, per channel. The keyboard holds numbers,
- // not names, so `effect <name>` needs a catalog and a board without one is driven
- // through raw IDs.
- //
- // There is no compiled-in catalog for any board. A board's effect names come from
- // its VIA definition file, read at runtime from the data directory or from the file
- // --definition names, because a hand-written list and the vendor's file describing
- // the same board are two places to update one name, and that is how they drift
- // apart. A board with no definition has no names and is driven through raw effect
- // IDs.
- type Catalog struct {
- board string
- names map[via.Channel][]Effect
- aliases map[via.Channel]map[string]string
- }
- // NewCatalog returns a catalog for a board described by effect entries, as a VIA
- // definition file provides them. The board name is what the effect list reports;
- // the entries may leave gaps, because a definition names only the effects a board's
- // firmware implements.
- //
- // The tool's aliases come with it, because they are a spelling convenience and not
- // board knowledge: a definition writes the manufacturer's spelling, and the user
- // should not have to know which one it chose.
- func NewCatalog(board string, effects map[via.Channel][]Effect) *Catalog {
- return &Catalog{board: board, names: effects, aliases: defaultAliases}
- }
- // defaultAliases are the spellings every catalog accepts, whatever the source of
- // its names. They resolve per channel, so `rainbow` is the vendor's `spectrum`
- // where that is the name and the catalog's own on another channel.
- var defaultAliases = 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",
- },
- }
- // Name returns the catalog's board name, which the effect list reports.
- func (c *Catalog) Name() string {
- if c == nil {
- return ""
- }
- return c.board
- }
- // Effects returns a channel's named effects, or nil when the catalog says nothing
- // about it.
- func (c *Catalog) Effects(ch via.Channel) []Effect {
- if c == nil {
- return nil
- }
- return c.names[ch]
- }
- // Names returns the effect names of a channel in effect-ID order, or nil when the
- // catalog says nothing about it.
- func (c *Catalog) Names(ch via.Channel) []string {
- effects := c.Effects(ch)
- if effects == nil {
- return nil
- }
- names := make([]string, 0, len(effects))
- for _, e := range effects {
- names = append(names, e.Name)
- }
- return names
- }
- // EffectName returns the name of an effect ID, or "unknown" when the catalog has no
- // entry for it. A board may take an effect ID that no definition names, and then
- // this says so rather than inventing a label for it.
- func (c *Catalog) EffectName(ch via.Channel, id uint8) string {
- if c == nil {
- return unknownEffectName
- }
- for _, e := range c.names[ch] {
- if e.ID == id {
- return e.Name
- }
- }
- return unknownEffectName
- }
- // 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
- }
- if id, found := c.byName(ch, name); found {
- return id, true
- }
- if canonical, ok := c.aliases[ch][name]; ok {
- if id, found := c.EffectID(ch, canonical); found {
- return id, true
- }
- }
- // A definition may carry the other spelling of the same pair: the tool's alias
- // table says "breathe" for "breathing", and a manufacturer's file may use
- // either. So a name that is the target of an alias resolves to that alias where
- // the channel has it.
- for alias, canonical := range c.aliases[ch] {
- if canonical != name {
- continue
- }
- if id, found := c.byName(ch, alias); found {
- return id, true
- }
- }
- return 0, false
- }
- // byName finds an effect by its exact name, without following an alias, then by the
- // same name with spaces written as underscores. A definition carries display
- // spellings where the tool carries identifiers, and the difference is whitespace,
- // not a different effect: "fixed wave" and "fixed_wave" name the same thing, and the
- // tool's documented spelling has to keep working while a definition file is present.
- func (c *Catalog) byName(ch via.Channel, name string) (uint8, bool) {
- for _, e := range c.names[ch] {
- if e.Name == name {
- return e.ID, true
- }
- }
- spaced := strings.ReplaceAll(name, "_", " ")
- if spaced != name {
- for _, e := range c.names[ch] {
- if e.Name == spaced {
- return e.ID, true
- }
- }
- }
- return 0, false
- }
- // DefaultEffect returns the effect enable writes when turning a channel on: the
- // first effect the board's own list names that is not the off entry, which is what
- // makes a channel light at all. A definition carries no notion of a default effect,
- // so the board's list is where the answer comes from, and a channel whose list holds
- // nothing but the off entry has none to give.
- func (c *Catalog) DefaultEffect(ch via.Channel) (uint8, bool) {
- if c == nil {
- return 0, false
- }
- for _, e := range c.names[ch] {
- if e.ID != 0 {
- return e.ID, true
- }
- }
- return 0, false
- }
- // 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 {
- // Both ways out belong in the message: the board is still drivable by
- // number, and the names are one command away. A user who reads only the
- // error should not have to find either out elsewhere.
- return nil, nil, fmt.Errorf("no effect names for this keyboard: run `keyboard fetch` for its VIA definition, " +
- "or set an effect by number with `effect <zone> <index>`")
- }
- // The compatibility spelling every catalog shares, so a caller cannot resolve
- // "static" differently from another.
- if name == "static" {
- name = "solid"
- }
- // Whether the board has the name at all is a different question from whether the
- // channels asked for can do it: naming a real effect on the wrong channel is a
- // different mistake, with a different fix, and gets its own message.
- knownAnywhere := false
- for ch := range catalog.names {
- if _, ok := catalog.EffectID(ch, name); ok {
- knownAnywhere = true
- break
- }
- }
- if !knownAnywhere {
- // A name nobody wrote is either a typo or a number that belongs in the
- // other form, and pointing at it saves the user from guessing. The ID form
- // is `effect <zone> <index>`: the zone is the command's first argument and
- // is required, so an index on its own is read as a channel name.
- return nil, nil, fmt.Errorf("unknown effect: %s (an effect ID from 0 to 255 is `effect <zone> <index>`)", 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
- }
|