zones.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "sort"
  6. "strings"
  7. "netdome.biz/paul/qmk-rgb/internal/via"
  8. )
  9. // zoneAll is the zone name that means every channel the keyboard reports.
  10. const zoneAll = "all"
  11. // resolveZoneName maps a zone argument to the channels it may mean, without
  12. // opening the keyboard. A display name of the connected board wins over a
  13. // subsystem name, and a value that would mean two channels is refused rather
  14. // than resolved to one of them.
  15. //
  16. // The argument is one name or several, comma separated, and the channels come
  17. // back in channel order however they were written, so a caller sees the same
  18. // order for every spelling of the same selection.
  19. //
  20. // An empty argument means every channel and is reported as a nil slice, which is
  21. // how a command that only reads reports the whole keyboard.
  22. func resolveZoneName(name string, display map[uint16]string, alternatives map[uint16][]string) ([]via.Channel, error) {
  23. if name == "" {
  24. return nil, nil
  25. }
  26. parts := strings.Split(name, ",")
  27. var channels []via.Channel
  28. for _, part := range parts {
  29. part = strings.TrimSpace(part)
  30. if part == "" {
  31. return nil, fmt.Errorf("zone %q names an empty channel: write several channels comma separated, like side,logo", name)
  32. }
  33. // all means every channel, so naming it next to one more channel is the
  34. // same request as naming it alone: the union is the whole keyboard.
  35. if strings.EqualFold(part, zoneAll) {
  36. return nil, nil
  37. }
  38. found, err := resolveOneZoneName(part, display, alternatives)
  39. if errors.Is(err, errNotAZoneName) {
  40. return nil, unknownZoneError(part, name)
  41. }
  42. if err != nil {
  43. return nil, err
  44. }
  45. channels = append(channels, found...)
  46. }
  47. return sortChannels(dedupeChannels(channels)), nil
  48. }
  49. // errNotAZoneName says the argument is not a name this board or this tool knows.
  50. // The caller turns it into the message, because the message has to quote the
  51. // whole list when there is one: "unknown zone" in a list of four is otherwise a
  52. // claim about a word the user never wrote on its own.
  53. var errNotAZoneName = errors.New("not a zone name")
  54. // unknownZoneError names the part that could not be placed, the list it came
  55. // from, and the two vocabularies that would have placed it.
  56. func unknownZoneError(part, whole string) error {
  57. quoted := fmt.Sprintf("unknown zone %q", part)
  58. if part != whole {
  59. quoted += fmt.Sprintf(" in %q", whole)
  60. }
  61. return fmt.Errorf("%s: use a channel name such as rgb_matrix, the name this keyboard's definition gives it, or %s for all of them", quoted, zoneAll)
  62. }
  63. // resolveOneZoneName maps a single channel name to the channels it may mean.
  64. func resolveOneZoneName(name string, display map[uint16]string, alternatives map[uint16][]string) ([]via.Channel, error) {
  65. if channels := dedupeChannels(append(displayNameChannels(display, name), alternateNameChannels(alternatives, name)...)); len(channels) > 1 {
  66. return nil, fmt.Errorf("zone %q matches several channels of this keyboard; its definition gives the same name to more than one", name)
  67. } else if len(channels) == 1 {
  68. return channels, nil
  69. }
  70. for c := via.Channel(1); c <= via.AssignedChannelMax; c++ {
  71. if c.Subsystem() == name {
  72. return []via.Channel{c}, nil
  73. }
  74. }
  75. return nil, errNotAZoneName
  76. }
  77. // displayNameChannels returns every channel the board names displayName. The
  78. // comparison ignores case, because a definition writes "Backlight" where a user
  79. // types "backlight", and the two mean the same channel.
  80. func displayNameChannels(display map[uint16]string, displayName string) []via.Channel {
  81. var channels []via.Channel
  82. for number, name := range display {
  83. if strings.EqualFold(name, displayName) {
  84. channels = append(channels, via.Channel(number))
  85. }
  86. }
  87. sort.Slice(channels, func(i, j int) bool { return channels[i] < channels[j] })
  88. return channels
  89. }
  90. // alternateNameChannels returns the channels a name reaches through the
  91. // alternatives a channel carries, which is how a QMK subsystem name keeps
  92. // working for a board whose definition calls that channel something else.
  93. func alternateNameChannels(alternatives map[uint16][]string, displayName string) []via.Channel {
  94. var channels []via.Channel
  95. for number, names := range alternatives {
  96. for _, name := range names {
  97. if strings.EqualFold(name, displayName) {
  98. channels = append(channels, via.Channel(number))
  99. break
  100. }
  101. }
  102. }
  103. sort.Slice(channels, func(i, j int) bool { return channels[i] < channels[j] })
  104. return channels
  105. }
  106. // displayNameConflicts rejects a board whose display name is also the subsystem
  107. // name of a different channel the keyboard actually has. A name that means two
  108. // channels is a silent retarget, so the tool refuses it.
  109. //
  110. // Presence is only known after the probe, which is why this is checked here
  111. // rather than when the file is read: a static check would reject the Impact 80,
  112. // which calls channel 3 "backlight" while channel 1 is absent.
  113. func displayNameConflicts(display map[uint16]string, present []via.Channel) error {
  114. names := make([]string, 0, len(display))
  115. for _, name := range display {
  116. names = append(names, name)
  117. }
  118. sort.Strings(names)
  119. for _, name := range names {
  120. named := via.Channel(0)
  121. for _, ch := range displayNameChannels(display, name) {
  122. named = ch
  123. break
  124. }
  125. for _, other := range present {
  126. if other != named && other.Subsystem() == name {
  127. return fmt.Errorf("channel %d is named %q, which is also the subsystem name of channel %d", named, name, other)
  128. }
  129. }
  130. }
  131. return nil
  132. }
  133. // channelName is how a channel is printed: the board's display name when it has
  134. // one, otherwise the QMK subsystem name.
  135. func channelName(c via.Channel, display map[uint16]string) string {
  136. if name, ok := display[uint16(c)]; ok {
  137. return name
  138. }
  139. return c.Subsystem()
  140. }
  141. // sortChannels puts channels in channel order, which is the order the tool
  142. // reports them in everywhere else. A list of names is written in whatever order
  143. // suits the user; the channels it resolves to still come back in channel order,
  144. // so a script sees one order for every spelling of the same selection.
  145. func sortChannels(channels []via.Channel) []via.Channel {
  146. if len(channels) < 2 {
  147. return channels
  148. }
  149. sort.Slice(channels, func(i, j int) bool { return channels[i] < channels[j] })
  150. return channels
  151. }
  152. // dedupeChannels drops repeats, keeping the order. A name can reach the same
  153. // channel twice — once as the definition's label and once as the subsystem name
  154. // it replaces — and that is one channel, not a conflict.
  155. func dedupeChannels(channels []via.Channel) []via.Channel {
  156. seen := make(map[via.Channel]bool, len(channels))
  157. out := make([]via.Channel, 0, len(channels))
  158. for _, ch := range channels {
  159. if seen[ch] {
  160. continue
  161. }
  162. seen[ch] = true
  163. out = append(out, ch)
  164. }
  165. return out
  166. }