catalog.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "netdome.biz/paul/qmk-rgb/definitions"
  6. intdevice "netdome.biz/paul/qmk-rgb/internal/device"
  7. intrgb "netdome.biz/paul/qmk-rgb/internal/rgb"
  8. "netdome.biz/paul/qmk-rgb/internal/via"
  9. )
  10. // definitionFlag names a definition file to use instead of looking one up.
  11. var definitionFlag string
  12. // builtInDefinitions are the definition files compiled into the binary, parsed
  13. // once. They are the last resort in a lookup and not a catalog of their own: the
  14. // files are the vendor's own, kept as served, and one a user has placed in a
  15. // definitions directory takes precedence over the copy here. Parsing them per
  16. // lookup would be wasted work on every command that names an effect.
  17. var builtInDefinitions = sync.OnceValue(func() []*intrgb.Definition {
  18. defs, err := intrgb.LoadDefinitionsFS(definitions.Vendored, "definitions")
  19. if err != nil {
  20. // A vendored file that does not parse is a build defect, not something
  21. // a user can fix by putting a file somewhere. Carry on without them:
  22. // the board is then driven through raw effect IDs, which is worse but
  23. // still works, and a lookup must not fail because of it.
  24. return nil
  25. }
  26. return defs
  27. })
  28. // candidateDefinitions returns every definition a lookup may match, in the order
  29. // the tool trusts: the file in the per-user directory first, so one placed there
  30. // or fetched for the board overrides the copy built into the binary, and the
  31. // built-in files last so that an installed tool has the boards whose definitions
  32. // cannot be fetched.
  33. func candidateDefinitions() []*intrgb.Definition {
  34. var defs []*intrgb.Definition
  35. if fromDir, err := intrgb.LoadDefinitionsDir(definitionsPath()); err == nil {
  36. defs = append(defs, fromDir...)
  37. }
  38. return append(defs, builtInDefinitions()...)
  39. }
  40. // resolveCatalog returns the effect catalog for a board, in the order the tool
  41. // trusts: the file named by --definition, then the file in the per-user
  42. // definitions directory that matches the board, then the one built into the
  43. // binary. The second return value says which of them it was, because a board
  44. // that has no names at all is a different situation from one whose names came
  45. // from somewhere the user can see.
  46. //
  47. // This is the one place a catalog is looked up. A command that reached for a
  48. // catalog itself would silently ignore a definition file the user had placed,
  49. // which is the whole point of having one.
  50. func resolveCatalog(target targetDeviceData) (*intrgb.Catalog, string, error) {
  51. if definitionFlag != "" {
  52. def, err := intrgb.LoadDefinition(definitionFlag)
  53. if err != nil {
  54. return nil, "", err
  55. }
  56. if !def.Matches(target.Device.VendorID, target.Device.ProductID) {
  57. return nil, "", fmt.Errorf("%s is a definition for %s (0x%04X/0x%04X), not for this keyboard (0x%04X/0x%04X)",
  58. definitionFlag, def.Name, def.VendorID, def.ProductID,
  59. target.Device.VendorID, target.Device.ProductID)
  60. }
  61. return def.Catalog, def.Path, nil
  62. }
  63. catalog, source, err := resolveCatalogFor(target.Device.VendorID, target.Device.ProductID)
  64. return catalog, source, err
  65. }
  66. // resolveCatalogFor is the lookup without a target, for the commands that report
  67. // on a board rather than open it.
  68. func resolveCatalogFor(vendorID, productID uint16) (*intrgb.Catalog, string, error) {
  69. if definitionFlag != "" {
  70. def, err := intrgb.LoadDefinition(definitionFlag)
  71. if err != nil {
  72. return nil, "", err
  73. }
  74. if !def.Matches(vendorID, productID) {
  75. return nil, "", nil
  76. }
  77. return def.Catalog, def.Path, nil
  78. }
  79. if def := intrgb.FindDefinition(candidateDefinitions(), vendorID, productID); def != nil {
  80. return def.Catalog, def.Path, nil
  81. }
  82. // No definition for this board, not in a directory and not built in: the
  83. // keyboard holds numbers, not names, and a board with no names is driven
  84. // through raw IDs.
  85. return nil, "", nil
  86. }
  87. // loadedDefinition returns the definition file for a board, from the file
  88. // --definition names, from the per-user directory or from the binary, and nil
  89. // when there is none.
  90. func loadedDefinition(vendorID, productID uint16) *intrgb.Definition {
  91. if definitionFlag != "" {
  92. if def, err := intrgb.LoadDefinition(definitionFlag); err == nil && def.Matches(vendorID, productID) {
  93. return def
  94. }
  95. return nil
  96. }
  97. return intrgb.FindDefinition(candidateDefinitions(), vendorID, productID)
  98. }
  99. // definitionLabels returns the channel names a definition gives the board, which
  100. // are the names VIA shows. They are the only channel names there are: a board
  101. // without a definition is addressed by its QMK subsystem name.
  102. func definitionLabels(vendorID, productID uint16) map[uint16]string {
  103. def := loadedDefinition(vendorID, productID)
  104. if def == nil {
  105. return nil
  106. }
  107. return def.Labels
  108. }
  109. // applyDefinitionLabels returns the display names a board answers to and, beside
  110. // them, the alternatives each channel keeps. The definition's label is the name
  111. // the board is called in VIA; the QMK subsystem name stays an accepted
  112. // alternative, because it follows from the channel number and is the one a
  113. // document can promise without knowing a board.
  114. //
  115. // Only a channel the definition names gets a display name, and that is
  116. // deliberate. Adding an entry for every QMK lighting channel would put
  117. // "backlight" on channel 1 of a board that has none, where the same word is also
  118. // the board's name for channel 3 — and a name that reaches two channels is
  119. // refused. A channel the definition does not name is addressed by its subsystem
  120. // name, which is what the channel number alone tells us.
  121. func applyDefinitionLabels(vendorID, productID uint16) (map[uint16]string, map[uint16][]string) {
  122. labels := definitionLabels(vendorID, productID)
  123. display := make(map[uint16]string, len(labels))
  124. alternatives := make(map[uint16][]string, len(labels))
  125. for number, label := range labels {
  126. display[number] = label
  127. if subsystem := via.Channel(number).Subsystem(); subsystem != label {
  128. alternatives[number] = []string{subsystem}
  129. }
  130. }
  131. return display, alternatives
  132. }
  133. // boardName is what a board is called: the name its definition gives it, else the
  134. // USB product string the keyboard itself reports, else an honest placeholder.
  135. func boardName(dev intdevice.Device, _ map[uint16]string) string {
  136. if def := loadedDefinition(dev.VendorID, dev.ProductID); def != nil && def.Name != "" {
  137. return def.Name
  138. }
  139. if dev.Name != "" {
  140. return dev.Name
  141. }
  142. return "unknown model"
  143. }
  144. // ensureDefinitionsDir returns the data directory, creating it if it is not
  145. // there yet, so a fetch has somewhere to write to.
  146. func ensureDefinitionsDir() (string, error) {
  147. dir, err := ensureDataDir(definitionsPath())
  148. if err != nil {
  149. return "", fmt.Errorf("create %s: %w", definitionsPath(), err)
  150. }
  151. return dir, nil
  152. }