catalog.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. package rgb
  2. import (
  3. "fmt"
  4. "netdome.biz/paul/qmk-rgb/internal/via"
  5. )
  6. // EffectTarget is one effect ID to write to one channel.
  7. type EffectTarget struct {
  8. Channel via.Channel
  9. ID uint8
  10. }
  11. const unknownEffectName = "unknown"
  12. // Catalog is one board's effect names, per channel. The keyboard holds numbers,
  13. // not names, so `effect <name>` needs a catalog and a board without one is
  14. // driven through raw IDs.
  15. type Catalog struct {
  16. board string
  17. names map[via.Channel][]string
  18. aliases map[via.Channel]map[string]string
  19. defaults map[via.Channel]uint8
  20. }
  21. // CatalogFor returns the effect catalog of a board, and whether one exists.
  22. //
  23. // Provenance, because these lists look invented and are not:
  24. //
  25. // - The 46 backlight names are QMK's rgb_matrix_effects.inc in order, each ID
  26. // verified against the live register of an Impact 80.
  27. // - The 7 logo and 7 side names are this board's vendor VIA definition, whose
  28. // dropdowns read "fixed wave" and "breathe". The tool spells them
  29. // fixed_wave and breathing and accepts the vendor's spellings as aliases.
  30. // - The brightness and speed transforms documented in README.md were measured
  31. // on the unit, not read from anywhere.
  32. func CatalogFor(vendorID, productID uint16) (*Catalog, bool) {
  33. if vendorID != 0x36B0 || productID != 0x309F {
  34. return nil, false
  35. }
  36. return &Catalog{
  37. board: "impact80",
  38. names: map[via.Channel][]string{
  39. via.ChannelRgblight: impact80LogoEffects[:],
  40. via.ChannelRgbMatrix: impact80BacklightEffects[:],
  41. via.ChannelAudio: impact80SideEffects[:],
  42. },
  43. aliases: map[via.Channel]map[string]string{
  44. via.ChannelRgblight: {
  45. "off": "none",
  46. "breathe": "breathing",
  47. "rainbow": "spectrum",
  48. "rainbow_wave": "wave",
  49. "solid": "light",
  50. "static": "solid",
  51. },
  52. via.ChannelRgbMatrix: {
  53. "off": "none",
  54. "breathe": "breathing",
  55. "rainbow": "rainbow_moving_chevron",
  56. "solid": "solid_color",
  57. "static": "solid",
  58. },
  59. via.ChannelAudio: {
  60. "off": "none",
  61. "breathe": "breathing",
  62. "rainbow": "spectrum",
  63. "rainbow_wave": "wave",
  64. "solid": "light",
  65. "static": "solid",
  66. },
  67. },
  68. defaults: map[via.Channel]uint8{
  69. via.ChannelRgblight: 4,
  70. via.ChannelRgbMatrix: 5,
  71. via.ChannelAudio: 4,
  72. },
  73. }, true
  74. }
  75. // Name returns the catalog's board name, which `effect --list` reports.
  76. func (c *Catalog) Name() string {
  77. if c == nil {
  78. return ""
  79. }
  80. return c.board
  81. }
  82. // Names returns the effect names of a channel, or nil when the catalog says
  83. // nothing about it.
  84. func (c *Catalog) Names(ch via.Channel) []string {
  85. if c == nil {
  86. return nil
  87. }
  88. return c.names[ch]
  89. }
  90. // EffectName returns the name of an effect ID, or "unknown" when the catalog has
  91. // no entry for it.
  92. func (c *Catalog) EffectName(ch via.Channel, id uint8) string {
  93. if c == nil {
  94. return unknownEffectName
  95. }
  96. names := c.names[ch]
  97. if int(id) >= len(names) {
  98. return unknownEffectName
  99. }
  100. return names[id]
  101. }
  102. // EffectID resolves an effect name on a channel, following one level of alias.
  103. func (c *Catalog) EffectID(ch via.Channel, name string) (uint8, bool) {
  104. if c == nil {
  105. return 0, false
  106. }
  107. for id, candidate := range c.names[ch] {
  108. if candidate == name {
  109. return uint8(id), true
  110. }
  111. }
  112. if canonical, ok := c.aliases[ch][name]; ok {
  113. for id, candidate := range c.names[ch] {
  114. if candidate == canonical {
  115. return uint8(id), true
  116. }
  117. }
  118. }
  119. return 0, false
  120. }
  121. // DefaultEffect returns the effect ID enable writes when turning a channel on.
  122. func (c *Catalog) DefaultEffect(ch via.Channel) (uint8, bool) {
  123. if c == nil {
  124. return 0, false
  125. }
  126. id, ok := c.defaults[ch]
  127. return id, ok
  128. }
  129. // ResolveEffect turns an effect name into one target per channel that supports
  130. // it, plus the subsystem names of those that do not. A skip is an error rather
  131. // than a warning when the caller asked for one channel explicitly, because a
  132. // command that silently did nothing looks like a command that worked. That is
  133. // the caller's knowledge to pass: a board with a single lighting channel is not
  134. // an explicit request for it.
  135. func ResolveEffect(catalog *Catalog, name string, channels []via.Channel, explicit bool) ([]EffectTarget, []string, error) {
  136. if catalog == nil {
  137. return nil, nil, fmt.Errorf("no effect catalog for this keyboard; set an effect by number with `mode <index>`")
  138. }
  139. // The compatibility spelling every catalog shares, so a caller cannot
  140. // resolve "static" differently from another.
  141. if name == "static" {
  142. name = "solid"
  143. }
  144. known := false
  145. for _, ch := range channels {
  146. if _, ok := catalog.EffectID(ch, name); ok {
  147. known = true
  148. break
  149. }
  150. }
  151. if !known {
  152. return nil, nil, fmt.Errorf("unknown effect: %s", name)
  153. }
  154. var targets []EffectTarget
  155. var skipped []string
  156. for _, ch := range channels {
  157. id, ok := catalog.EffectID(ch, name)
  158. if !ok {
  159. if explicit {
  160. return nil, nil, fmt.Errorf("effect %s is not supported on %s", name, ch.Subsystem())
  161. }
  162. skipped = append(skipped, ch.Subsystem())
  163. continue
  164. }
  165. targets = append(targets, EffectTarget{Channel: ch, ID: id})
  166. }
  167. return targets, skipped, nil
  168. }