names.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. package rgb
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "os"
  6. "sort"
  7. "strconv"
  8. "strings"
  9. "netdome.biz/paul/qmk-rgb/internal/via"
  10. )
  11. // NameSource says where an effect name came from. It is on the effect rather
  12. // than on the file because one board's names can come from both: a definition
  13. // the manufacturer wrote and a name the user wrote over the top of one of its
  14. // entries. The tool's rule is that it never reports a value it has not verified,
  15. // and a name is the one value it cannot read back off a keyboard, so which of
  16. // the two a name is has to be visible wherever the name is.
  17. type NameSource string
  18. const (
  19. // SourceVendor is a name from the board's own definition file. It is what the
  20. // manufacturer wrote, so it is a fact about the board.
  21. SourceVendor NameSource = ""
  22. // SourceUser is a name from the per-user names file. It is what the user
  23. // called the effect, which is a fact about them and not about the board.
  24. SourceUser NameSource = "you"
  25. )
  26. // namesDocument is the file as it is stored. The channel is keyed by its QMK
  27. // subsystem name and the effect by its number, both as strings because that is
  28. // what a JSON object key is, and both are read back through the one vocabulary in
  29. // internal/via rather than a second table here.
  30. type namesDocument struct {
  31. Name string `json:"name,omitempty"`
  32. VendorID string `json:"vendorId"`
  33. ProductID string `json:"productId"`
  34. Channels map[string]map[string]string `json:"channels"`
  35. Aliases map[string]map[string]string `json:"aliases,omitempty"`
  36. }
  37. // Names is one board's effect names a user wrote, or overrode.
  38. //
  39. // It exists because a VIA definition file belongs to the manufacturer. The tool
  40. // used to write one of its own, which meant authoring a menu description for
  41. // someone else's keyboard and handing the user a nested JSON array to type a
  42. // name into. The names are the part that is missing and the part that is
  43. // anyone's to supply, so they are stored on their own and merged over whatever
  44. // the definition says.
  45. type Names struct {
  46. Path string
  47. Name string
  48. VendorID uint16
  49. ProductID uint16
  50. // effects and aliases are keyed by channel, then by effect ID or by spelling.
  51. effects map[via.Channel]map[uint8]string
  52. aliases map[via.Channel]map[string]string
  53. }
  54. // LoadNamesFile reads one names file. A file that is not a names file is an
  55. // error rather than nothing: the user put it there, and a silent "no names" would
  56. // read as the keyboard having none.
  57. func LoadNamesFile(path string) (*Names, error) {
  58. raw, err := os.ReadFile(path)
  59. if err != nil {
  60. return nil, err
  61. }
  62. var doc namesDocument
  63. if err := json.Unmarshal(raw, &doc); err != nil {
  64. return nil, fmt.Errorf("parse %s: %w", path, err)
  65. }
  66. vendorID, productID, err := namesIdentifiers(doc.VendorID, doc.ProductID)
  67. if err != nil {
  68. return nil, fmt.Errorf("parse %s: %w", path, err)
  69. }
  70. names := &Names{
  71. Path: path,
  72. Name: doc.Name,
  73. VendorID: vendorID,
  74. ProductID: productID,
  75. effects: map[via.Channel]map[uint8]string{},
  76. aliases: map[via.Channel]map[string]string{},
  77. }
  78. for key, byID := range doc.Channels {
  79. ch, ok := via.ChannelFromSubsystem(key)
  80. if !ok {
  81. return nil, fmt.Errorf("parse %s: %q is not a QMK lighting channel", path, key)
  82. }
  83. for rawID, name := range byID {
  84. id, err := parseEffectID(rawID)
  85. if err != nil {
  86. return nil, fmt.Errorf("parse %s: %s channel %q: %w", path, key, rawID, err)
  87. }
  88. if names.effects[ch] == nil {
  89. names.effects[ch] = map[uint8]string{}
  90. }
  91. names.effects[ch][id] = name
  92. }
  93. }
  94. for key, byName := range doc.Aliases {
  95. ch, ok := via.ChannelFromSubsystem(key)
  96. if !ok {
  97. return nil, fmt.Errorf("parse %s: %q is not a QMK lighting channel", path, key)
  98. }
  99. if names.aliases[ch] == nil {
  100. names.aliases[ch] = map[string]string{}
  101. }
  102. for spelling, name := range byName {
  103. names.aliases[ch][spelling] = name
  104. }
  105. }
  106. return names, nil
  107. }
  108. // Matches reports whether these names are the ones for a board.
  109. func (n *Names) Matches(vendorID, productID uint16) bool {
  110. return n.VendorID == vendorID && n.ProductID == productID
  111. }
  112. // Apply returns a catalog with these names over the top of the ones it holds.
  113. //
  114. // It works on a nil catalog, because a board with no definition file is exactly
  115. // the board a user writes names for: the file the manufacturer would have
  116. // published is the one that is missing, not the names. A name here replaces the
  117. // vendor's for that one effect ID and leaves every other entry alone, so
  118. // overriding one name does not mean restating the ones that were right.
  119. func (n *Names) Apply(base *Catalog) *Catalog {
  120. merged := make(map[via.Channel][]Effect)
  121. var aliases = map[via.Channel]map[string]string{}
  122. board := ""
  123. // A nil catalog is the normal case here, not a degenerate one: it is what a
  124. // board with no definition file gives, and that board is the one a user writes
  125. // names for.
  126. if base != nil {
  127. board = base.board
  128. for ch, effects := range base.names {
  129. merged[ch] = append([]Effect(nil), effects...)
  130. }
  131. for ch, byName := range base.aliases {
  132. copied := make(map[string]string, len(byName))
  133. for k, v := range byName {
  134. copied[k] = v
  135. }
  136. aliases[ch] = copied
  137. }
  138. }
  139. // An effect the definition does not have is added, because the definition may
  140. // stop short of the board's highest ID and a user naming that one is telling
  141. // us something the file did not.
  142. for ch, byID := range n.effects {
  143. for id, name := range byID {
  144. merged[ch] = upsertEffect(merged[ch], Effect{ID: id, Name: name, Source: SourceUser})
  145. }
  146. }
  147. for ch, byName := range n.aliases {
  148. if aliases[ch] == nil {
  149. aliases[ch] = map[string]string{}
  150. }
  151. for spelling, name := range byName {
  152. // Stored as written. An alias is looked up by the exact string the
  153. // catalog already uses, so normalising it here would make a spelling
  154. // resolve that the definition's own aliases do not.
  155. aliases[ch][spelling] = name
  156. }
  157. }
  158. if n.Name != "" {
  159. board = n.Name
  160. }
  161. out := &Catalog{board: board, names: merged, aliases: aliases}
  162. for ch := range merged {
  163. sort.Slice(out.names[ch], func(i, j int) bool { return out.names[ch][i].ID < out.names[ch][j].ID })
  164. }
  165. return out
  166. }
  167. // upsertEffect replaces the entry for an ID or adds one, keeping the list sorted.
  168. func upsertEffect(effects []Effect, want Effect) []Effect {
  169. for i := range effects {
  170. if effects[i].ID == want.ID {
  171. effects[i] = want
  172. return effects
  173. }
  174. }
  175. effects = append(effects, want)
  176. sort.Slice(effects, func(i, j int) bool { return effects[i].ID < effects[j].ID })
  177. return effects
  178. }
  179. // Channels returns the lighting channels this file names, in channel order, so a
  180. // command that reports on the file can walk it the way it walks a board.
  181. func (n *Names) Channels() []via.Channel {
  182. var out []via.Channel
  183. for _, ch := range via.LightingChannels {
  184. if len(n.effects[ch]) > 0 || len(n.aliases[ch]) > 0 {
  185. out = append(out, ch)
  186. }
  187. }
  188. return out
  189. }
  190. // Effects returns the names on one channel, in ID order.
  191. func (n *Names) Effects(ch via.Channel) []Effect {
  192. ids := make([]int, 0, len(n.effects[ch]))
  193. for id := range n.effects[ch] {
  194. ids = append(ids, int(id))
  195. }
  196. sort.Ints(ids)
  197. out := make([]Effect, 0, len(ids))
  198. for _, id := range ids {
  199. out = append(out, Effect{ID: uint8(id), Name: n.effects[ch][uint8(id)], Source: SourceUser})
  200. }
  201. return out
  202. }
  203. // parseEffectID reads an effect ID from a names file, where it is a JSON object
  204. // key and therefore a string. A plain number is what a person writes, and 0x is
  205. // accepted because both spellings appear in the tool's own output.
  206. func parseEffectID(raw string) (uint8, error) {
  207. var n uint64
  208. var err error
  209. if base, value, found := strings.Cut(raw, "0x"); found {
  210. _ = base
  211. n, err = strconv.ParseUint(value, 16, 16)
  212. } else {
  213. n, err = strconv.ParseUint(raw, 10, 16)
  214. }
  215. if err != nil {
  216. return 0, fmt.Errorf("%q is not an effect ID", raw)
  217. }
  218. return uint8(n), nil
  219. }
  220. // namesIdentifiers returns the board a names file is for. It is the same pair of
  221. // spellings a definition file carries, read through the same hex parser, so a
  222. // names file and a definition file for one board are recognisably the same board.
  223. func namesIdentifiers(vendorID, productID string) (uint16, uint16, error) {
  224. if vendorID == "" || productID == "" {
  225. return 0, 0, fmt.Errorf("neither vendorId nor productId; not a names file")
  226. }
  227. v, err := parseHexID(vendorID)
  228. if err != nil {
  229. return 0, 0, fmt.Errorf("vendorId %q: %w", vendorID, err)
  230. }
  231. p, err := parseHexID(productID)
  232. if err != nil {
  233. return 0, 0, fmt.Errorf("productId %q: %w", productID, err)
  234. }
  235. return v, p, nil
  236. }
  237. // EffectSource says whether the name for an effect ID on a channel is the
  238. // manufacturer's or the user's, which is what lets a command print where a name
  239. // came from instead of presenting both as the same kind of fact.
  240. func (c *Catalog) EffectSource(ch via.Channel, id uint8) NameSource {
  241. if c == nil {
  242. return SourceVendor
  243. }
  244. for _, e := range c.names[ch] {
  245. if e.ID == id {
  246. return e.Source
  247. }
  248. }
  249. return SourceVendor
  250. }