catalog.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. package rgb
  2. import (
  3. "fmt"
  4. "strings"
  5. "netdome.biz/paul/qmk-rgb/internal/via"
  6. )
  7. // EffectTarget is one effect ID to write to one channel.
  8. type EffectTarget struct {
  9. Channel via.Channel
  10. ID uint8
  11. }
  12. const unknownEffectName = "unknown"
  13. // Effect is one named effect on a channel: the ID the keyboard uses and the name
  14. // a definition gives it. The pair is explicit because a VIA definition may attach
  15. // a number to an option that is not its position in the list, so a board's effect
  16. // ID is not an index into its names.
  17. type Effect struct {
  18. ID uint8
  19. Name string
  20. // Source says whether the name is the manufacturer's or the user's. It is
  21. // carried per effect because one board's names can come from both, and a name
  22. // is the one value this tool cannot read back off a keyboard to check: an
  23. // empty source means the definition file the vendor published said this, and
  24. // SourceUser means a person typed it.
  25. Source NameSource
  26. }
  27. // Catalog is one board's effect names, per channel. The keyboard holds numbers,
  28. // not names, so `effect <name>` needs a catalog and a board without one is driven
  29. // through raw IDs.
  30. //
  31. // There is no compiled-in catalog for any board. A board's effect names come from
  32. // its VIA definition file, read at runtime from the data directory or from the file
  33. // --definition names, because a hand-written list and the vendor's file describing
  34. // the same board are two places to update one name, and that is how they drift
  35. // apart. A board with no definition has no names and is driven through raw effect
  36. // IDs.
  37. type Catalog struct {
  38. board string
  39. names map[via.Channel][]Effect
  40. aliases map[via.Channel]map[string]string
  41. }
  42. // NewCatalog returns a catalog for a board described by effect entries, as a VIA
  43. // definition file provides them. The board name is what the effect list reports;
  44. // the entries may leave gaps, because a definition names only the effects a board's
  45. // firmware implements.
  46. //
  47. // The tool's aliases come with it, because they are a spelling convenience and not
  48. // board knowledge: a definition writes the manufacturer's spelling, and the user
  49. // should not have to know which one it chose.
  50. func NewCatalog(board string, effects map[via.Channel][]Effect) *Catalog {
  51. return &Catalog{board: board, names: effects, aliases: defaultAliases}
  52. }
  53. // defaultAliases are the spellings every catalog accepts, whatever the source of
  54. // its names. They resolve per channel, so `rainbow` is the vendor's `spectrum`
  55. // where that is the name and the catalog's own on another channel.
  56. var defaultAliases = map[via.Channel]map[string]string{
  57. via.ChannelRgblight: {
  58. "off": "none",
  59. "breathe": "breathing",
  60. "rainbow": "spectrum",
  61. "rainbow_wave": "wave",
  62. "solid": "light",
  63. "static": "solid",
  64. },
  65. via.ChannelRgbMatrix: {
  66. "off": "none",
  67. "breathe": "breathing",
  68. "rainbow": "rainbow_moving_chevron",
  69. "solid": "solid_color",
  70. "static": "solid",
  71. },
  72. via.ChannelAudio: {
  73. "off": "none",
  74. "breathe": "breathing",
  75. "rainbow": "spectrum",
  76. "rainbow_wave": "wave",
  77. "solid": "light",
  78. "static": "solid",
  79. },
  80. }
  81. // Name returns the catalog's board name, which the effect list reports.
  82. func (c *Catalog) Name() string {
  83. if c == nil {
  84. return ""
  85. }
  86. return c.board
  87. }
  88. // Effects returns a channel's named effects, or nil when the catalog says nothing
  89. // about it.
  90. func (c *Catalog) Effects(ch via.Channel) []Effect {
  91. if c == nil {
  92. return nil
  93. }
  94. return c.names[ch]
  95. }
  96. // HasNames reports whether the catalog names any effect at all, on any channel. A
  97. // definition can be present and name nothing, which is not the same as having no
  98. // definition: a board whose file refers to VIA's built-in lighting menu by name
  99. // rather than listing its effects carries no names, because the names for that menu
  100. // live in VIA's own code. The two situations are told apart in a message, because
  101. // the fix for one is no use at all for the other.
  102. func (c *Catalog) HasNames() bool {
  103. if c == nil {
  104. return false
  105. }
  106. return len(c.names) > 0
  107. }
  108. // Names returns the effect names of a channel in effect-ID order, or nil when the
  109. // catalog says nothing about it.
  110. func (c *Catalog) Names(ch via.Channel) []string {
  111. effects := c.Effects(ch)
  112. if effects == nil {
  113. return nil
  114. }
  115. names := make([]string, 0, len(effects))
  116. for _, e := range effects {
  117. names = append(names, e.Name)
  118. }
  119. return names
  120. }
  121. // EffectName returns the name of an effect ID, or "unknown" when the catalog has no
  122. // entry for it. A board may take an effect ID that no definition names, and then
  123. // this says so rather than inventing a label for it.
  124. func (c *Catalog) EffectName(ch via.Channel, id uint8) string {
  125. if c == nil {
  126. return unknownEffectName
  127. }
  128. for _, e := range c.names[ch] {
  129. if e.ID == id {
  130. return e.Name
  131. }
  132. }
  133. return unknownEffectName
  134. }
  135. // EffectID resolves an effect name on a channel, following one level of alias.
  136. func (c *Catalog) EffectID(ch via.Channel, name string) (uint8, bool) {
  137. if c == nil {
  138. return 0, false
  139. }
  140. if id, found := c.byName(ch, name); found {
  141. return id, true
  142. }
  143. if canonical, ok := c.aliases[ch][name]; ok {
  144. if id, found := c.EffectID(ch, canonical); found {
  145. return id, true
  146. }
  147. }
  148. // A definition may carry the other spelling of the same pair: the tool's alias
  149. // table says "breathe" for "breathing", and a manufacturer's file may use
  150. // either. So a name that is the target of an alias resolves to that alias where
  151. // the channel has it.
  152. for alias, canonical := range c.aliases[ch] {
  153. if canonical != name {
  154. continue
  155. }
  156. if id, found := c.byName(ch, alias); found {
  157. return id, true
  158. }
  159. }
  160. return 0, false
  161. }
  162. // byName finds an effect by its exact name, without following an alias, then by the
  163. // same name with spaces written as underscores. A definition carries display
  164. // spellings where the tool carries identifiers, and the difference is whitespace,
  165. // not a different effect: "fixed wave" and "fixed_wave" name the same thing, and the
  166. // tool's documented spelling has to keep working while a definition file is present.
  167. func (c *Catalog) byName(ch via.Channel, name string) (uint8, bool) {
  168. for _, e := range c.names[ch] {
  169. if e.Name == name {
  170. return e.ID, true
  171. }
  172. }
  173. spaced := strings.ReplaceAll(name, "_", " ")
  174. if spaced != name {
  175. for _, e := range c.names[ch] {
  176. if e.Name == spaced {
  177. return e.ID, true
  178. }
  179. }
  180. }
  181. return 0, false
  182. }
  183. // DefaultEffect returns the effect enable writes when turning a channel on: the
  184. // first effect the board's own list names that is not the off entry, which is what
  185. // makes a channel light at all. A definition carries no notion of a default effect,
  186. // so the board's list is where the answer comes from, and a channel whose list holds
  187. // nothing but the off entry has none to give.
  188. func (c *Catalog) DefaultEffect(ch via.Channel) (uint8, bool) {
  189. if c == nil {
  190. return 0, false
  191. }
  192. for _, e := range c.names[ch] {
  193. if e.ID != 0 {
  194. return e.ID, true
  195. }
  196. }
  197. return 0, false
  198. }
  199. // ResolveEffect turns an effect name into one target per channel that supports it,
  200. // plus the subsystem names of those that do not. A skip is an error rather than a
  201. // warning when the caller asked for one channel explicitly, because a command that
  202. // silently did nothing looks like a command that worked. That is the caller's
  203. // knowledge to pass: a board with a single lighting channel is not an explicit
  204. // request for it.
  205. func ResolveEffect(catalog *Catalog, name string, channels []via.Channel, explicit bool) ([]EffectTarget, []string, error) {
  206. if catalog == nil {
  207. // Both ways out belong in the message: the board is still drivable by
  208. // number, and the names are one command away. A user who reads only the
  209. // error should not have to find either out elsewhere.
  210. return nil, nil, fmt.Errorf("no effect names for this keyboard: run `keyboard fetch` for its VIA definition, " +
  211. "or set an effect by number with `effect <zone> <index>`")
  212. }
  213. if !catalog.HasNames() {
  214. // The definition was found and it names nothing, so it is not a missing file
  215. // and fetching again changes nothing. Saying "unknown effect" here would
  216. // point at a typo in a name the tool holds none of.
  217. return nil, nil, fmt.Errorf("the VIA definition for %s names no effects, so no effect name can be resolved; "+
  218. "this board's names are not in the file, and an effect is set by number with `effect <zone> <index>`",
  219. catalog.Name())
  220. }
  221. // The compatibility spelling every catalog shares, so a caller cannot resolve
  222. // "static" differently from another.
  223. if name == "static" {
  224. name = "solid"
  225. }
  226. // Whether the board has the name at all is a different question from whether the
  227. // channels asked for can do it: naming a real effect on the wrong channel is a
  228. // different mistake, with a different fix, and gets its own message.
  229. knownAnywhere := false
  230. for ch := range catalog.names {
  231. if _, ok := catalog.EffectID(ch, name); ok {
  232. knownAnywhere = true
  233. break
  234. }
  235. }
  236. if !knownAnywhere {
  237. // A name nobody wrote is either a typo or a number that belongs in the
  238. // other form, and pointing at it saves the user from guessing. The ID form
  239. // is `effect <zone> <index>`: the zone is the command's first argument and
  240. // is required, so an index on its own is read as a channel name.
  241. return nil, nil, fmt.Errorf("unknown effect: %s (an effect ID from 0 to 255 is `effect <zone> <index>`)", name)
  242. }
  243. var targets []EffectTarget
  244. var skipped []string
  245. for _, ch := range channels {
  246. id, ok := catalog.EffectID(ch, name)
  247. if !ok {
  248. if explicit {
  249. return nil, nil, fmt.Errorf("effect %s is not supported on %s", name, ch.Subsystem())
  250. }
  251. skipped = append(skipped, ch.Subsystem())
  252. continue
  253. }
  254. targets = append(targets, EffectTarget{Channel: ch, ID: id})
  255. }
  256. return targets, skipped, nil
  257. }