catalog.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "sync"
  8. "netdome.biz/paul/qmk-rgb/definitions"
  9. intdevice "netdome.biz/paul/qmk-rgb/internal/device"
  10. intrgb "netdome.biz/paul/qmk-rgb/internal/rgb"
  11. "netdome.biz/paul/qmk-rgb/internal/via"
  12. )
  13. // definitionFlag names a definition file to use instead of looking one up.
  14. var definitionFlag string
  15. // builtInDefinitions are the definition files compiled into the binary, parsed
  16. // once. They are the last resort in a lookup and not a catalog of their own: the
  17. // files are the vendor's own, kept as served, and one a user has placed in a
  18. // definitions directory takes precedence over the copy here. Parsing them per
  19. // lookup would be wasted work on every command that names an effect.
  20. var builtInDefinitions = sync.OnceValue(func() []*intrgb.Definition {
  21. defs, err := intrgb.LoadDefinitionsFS(definitions.Vendored, "definitions")
  22. if err != nil {
  23. // A vendored file that does not parse is a build defect, not something
  24. // a user can fix by putting a file somewhere. Carry on without them:
  25. // the board is then driven through raw effect IDs, which is worse but
  26. // still works, and a lookup must not fail because of it.
  27. return nil
  28. }
  29. return defs
  30. })
  31. // candidateDefinitions returns every definition a lookup may match, in the order
  32. // the tool trusts: the file in the per-user directory first, so one placed there
  33. // or fetched for the board overrides the copy built into the binary, and the
  34. // built-in files last so that an installed tool has the boards whose definitions
  35. // cannot be fetched.
  36. func candidateDefinitions() []*intrgb.Definition {
  37. var defs []*intrgb.Definition
  38. if fromDir, err := intrgb.LoadDefinitionsDir(definitionsPath()); err == nil {
  39. defs = append(defs, fromDir...)
  40. }
  41. return append(defs, builtInDefinitions()...)
  42. }
  43. // resolveCatalog returns the effect catalog for a board, in the order the tool
  44. // trusts: the file named by --definition, then the file in the per-user
  45. // definitions directory that matches the board, then the one built into the
  46. // binary. The second return value says which of them it was, because a board
  47. // that has no names at all is a different situation from one whose names came
  48. // from somewhere the user can see.
  49. //
  50. // This is the one place a catalog is looked up. A command that reached for a
  51. // catalog itself would silently ignore a definition file the user had placed,
  52. // which is the whole point of having one.
  53. func resolveCatalog(target targetDeviceData) (*intrgb.Catalog, string, error) {
  54. if definitionFlag != "" {
  55. def, err := intrgb.LoadDefinition(definitionFlag)
  56. if err != nil {
  57. return nil, "", err
  58. }
  59. if !def.Matches(target.Device.VendorID, target.Device.ProductID) {
  60. return nil, "", fmt.Errorf("%s is a definition for %s (0x%04X/0x%04X), not for this keyboard (0x%04X/0x%04X)",
  61. definitionFlag, def.Name, def.VendorID, def.ProductID,
  62. target.Device.VendorID, target.Device.ProductID)
  63. }
  64. return withNames(def.Catalog, def.VendorID, def.ProductID), def.Path, nil
  65. }
  66. catalog, source, err := resolveCatalogFor(target.Device.VendorID, target.Device.ProductID)
  67. return catalog, source, err
  68. }
  69. // resolveCatalogFor is the lookup without a target, for the commands that report
  70. // on a board rather than open it.
  71. func resolveCatalogFor(vendorID, productID uint16) (*intrgb.Catalog, string, error) {
  72. if definitionFlag != "" {
  73. def, err := intrgb.LoadDefinition(definitionFlag)
  74. if err != nil {
  75. return nil, "", err
  76. }
  77. if !def.Matches(vendorID, productID) {
  78. return nil, "", nil
  79. }
  80. return def.Catalog, def.Path, nil
  81. }
  82. if def := intrgb.FindDefinition(candidateDefinitions(), vendorID, productID); def != nil {
  83. return withNames(def.Catalog, def.VendorID, def.ProductID), def.Path, nil
  84. }
  85. // No definition for this board, not in a directory and not built in. A names
  86. // file may still hold names for it, and that is a board somebody wrote them
  87. // for; a board with neither holds numbers rather than names and is driven
  88. // through raw effect IDs.
  89. return withNames(nil, vendorID, productID), "", nil
  90. }
  91. // loadedDefinition returns the definition file for a board, from the file
  92. // --definition names, from the per-user directory or from the binary, and nil
  93. // when there is none.
  94. func loadedDefinition(vendorID, productID uint16) *intrgb.Definition {
  95. if definitionFlag != "" {
  96. if def, err := intrgb.LoadDefinition(definitionFlag); err == nil && def.Matches(vendorID, productID) {
  97. return def
  98. }
  99. return nil
  100. }
  101. return intrgb.FindDefinition(candidateDefinitions(), vendorID, productID)
  102. }
  103. // definitionLabels returns the channel names a definition gives the board, which
  104. // are the names VIA shows. They are the only channel names there are: a board
  105. // without a definition is addressed by its QMK subsystem name.
  106. func definitionLabels(vendorID, productID uint16) map[uint16]string {
  107. def := loadedDefinition(vendorID, productID)
  108. if def == nil {
  109. return nil
  110. }
  111. return def.Labels
  112. }
  113. // applyDefinitionLabels returns the display names a board answers to and, beside
  114. // them, the alternatives each channel keeps. The definition's label is the name
  115. // the board is called in VIA; the QMK subsystem name stays an accepted
  116. // alternative, because it follows from the channel number and is the one a
  117. // document can promise without knowing a board.
  118. //
  119. // Only a channel the definition names gets a display name, and that is
  120. // deliberate. Adding an entry for every QMK lighting channel would put
  121. // "backlight" on channel 1 of a board that has none, where the same word is also
  122. // the board's name for channel 3 — and a name that reaches two channels is
  123. // refused. A channel the definition does not name is addressed by its subsystem
  124. // name, which is what the channel number alone tells us.
  125. func applyDefinitionLabels(vendorID, productID uint16) (map[uint16]string, map[uint16][]string) {
  126. labels := definitionLabels(vendorID, productID)
  127. display := make(map[uint16]string, len(labels))
  128. alternatives := make(map[uint16][]string, len(labels))
  129. for number, label := range labels {
  130. display[number] = label
  131. if subsystem := via.Channel(number).Subsystem(); subsystem != label {
  132. alternatives[number] = []string{subsystem}
  133. }
  134. }
  135. return display, alternatives
  136. }
  137. // boardName is what a board is called: the name its definition gives it, else the
  138. // USB product string the keyboard itself reports, else an honest placeholder.
  139. func boardName(dev intdevice.Device, _ map[uint16]string) string {
  140. if def := loadedDefinition(dev.VendorID, dev.ProductID); def != nil && def.Name != "" {
  141. return def.Name
  142. }
  143. if dev.Name != "" {
  144. return dev.Name
  145. }
  146. return "unknown model"
  147. }
  148. // ensureDefinitionsDir returns the data directory, creating it if it is not
  149. // there yet, so a fetch has somewhere to write to.
  150. func ensureDefinitionsDir() (string, error) {
  151. dir, err := ensureDataDir(definitionsPath())
  152. if err != nil {
  153. return "", fmt.Errorf("create %s: %w", definitionsPath(), err)
  154. }
  155. return dir, nil
  156. }
  157. // NamesDir is the name of the directory a user's own effect names are kept in. It
  158. // is not the definitions directory, because the two hold different things and only
  159. // one of them is the manufacturer's: a definition file is what a board's vendor
  160. // published, and a names file is what a person wrote. Merging them into one file
  161. // would make the tool author a document in someone else's name, and it would make
  162. // every name the user supplies indistinguishable from one the vendor published.
  163. const namesDir = "names"
  164. // namesPath is where a user's names for effect IDs live, and where a command that
  165. // writes one puts it.
  166. func namesPath() string {
  167. if namesDirOverride != "" {
  168. return namesDirOverride
  169. }
  170. return filepath.Join(userDataDir(), namesDir)
  171. }
  172. // ensureNamesDir returns the names directory, creating it if it is not there.
  173. func ensureNamesDir() (string, error) {
  174. dir, err := ensureDataDir(namesPath())
  175. if err != nil {
  176. return "", fmt.Errorf("create %s: %w", namesPath(), err)
  177. }
  178. return dir, nil
  179. }
  180. // candidateNames returns the names files in the per-user directory. A directory
  181. // that is not there yet is not an error: a user who has written none has none.
  182. func candidateNames() []*intrgb.Names {
  183. entries, err := os.ReadDir(namesPath())
  184. if err != nil {
  185. return nil
  186. }
  187. var out []*intrgb.Names
  188. for _, entry := range entries {
  189. if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
  190. continue
  191. }
  192. names, err := intrgb.LoadNamesFile(filepath.Join(namesPath(), entry.Name()))
  193. if err != nil {
  194. // A file the user wrote and that does not load is worth saying, but
  195. // not from here: a lookup is not the place to report it, and a broken
  196. // file for one board must not stop a name resolving for another. The
  197. // `names` command lists what is there and reports the broken one.
  198. continue
  199. }
  200. out = append(out, names)
  201. }
  202. return out
  203. }
  204. // withNames returns the catalog for a board with the user's names over the top of
  205. // whatever the definition said, and nil when there is neither. A names file is
  206. // consulted for every board, whether or not a definition exists, because the two
  207. // are independent: a user may override one name of a board whose definition the
  208. // tool has, and name a board whose definition nobody published.
  209. func withNames(catalog *intrgb.Catalog, vendorID, productID uint16) *intrgb.Catalog {
  210. for _, names := range candidateNames() {
  211. if !names.Matches(vendorID, productID) {
  212. continue
  213. }
  214. return names.Apply(catalog)
  215. }
  216. return catalog
  217. }