catalog.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. intrgb "netdome.biz/paul/qmk-rgb/internal/rgb"
  7. "netdome.biz/paul/qmk-rgb/internal/via"
  8. )
  9. // definitionFlag names a definition file to use instead of looking one up.
  10. var definitionFlag string
  11. // resolveCatalog returns the effect catalog for a board, in the order the tool
  12. // trusts: the file named by --definition, then the definition in the data
  13. // directory that matches the board, then the catalog compiled in for a known
  14. // board. The second return value says which of the three it was, because a
  15. // board that has no names at all is a different situation from one whose names
  16. // came from somewhere the user can see.
  17. //
  18. // This is the one place a catalog is looked up. A command that reached for a
  19. // catalog itself would silently ignore a definition file the user had placed,
  20. // which is the whole point of having one.
  21. func resolveCatalog(target targetDeviceData) (*intrgb.Catalog, string, error) {
  22. if definitionFlag != "" {
  23. def, err := intrgb.LoadDefinition(definitionFlag)
  24. if err != nil {
  25. return nil, "", err
  26. }
  27. if !def.Matches(target.Device.VendorID, target.Device.ProductID) {
  28. return nil, "", fmt.Errorf("%s is a definition for %s (0x%04X/0x%04X), not for this keyboard (0x%04X/0x%04X)",
  29. definitionFlag, def.Name, def.VendorID, def.ProductID,
  30. target.Device.VendorID, target.Device.ProductID)
  31. }
  32. return def.Catalog, def.Path, nil
  33. }
  34. catalog, source, err := resolveCatalogFor(target.Device.VendorID, target.Device.ProductID)
  35. return catalog, source, err
  36. }
  37. // resolveCatalogFor is the lookup without a target, for the commands that report
  38. // on a board rather than open it.
  39. func resolveCatalogFor(vendorID, productID uint16) (*intrgb.Catalog, string, error) {
  40. if definitionFlag != "" {
  41. def, err := intrgb.LoadDefinition(definitionFlag)
  42. if err != nil {
  43. return nil, "", err
  44. }
  45. if !def.Matches(vendorID, productID) {
  46. return nil, "", nil
  47. }
  48. return def.Catalog, def.Path, nil
  49. }
  50. if dir, err := definitionsDir(); err == nil {
  51. if defs, loadErr := intrgb.LoadDefinitionsDir(dir); loadErr == nil {
  52. if def := intrgb.FindDefinition(defs, vendorID, productID); def != nil {
  53. return def.Catalog, def.Path, nil
  54. }
  55. }
  56. }
  57. catalog, ok := intrgb.CatalogFor(vendorID, productID)
  58. if !ok {
  59. return nil, "", nil
  60. }
  61. return catalog, fmt.Sprintf("built in (%s)", catalog.Name()), nil
  62. }
  63. // lookupPaths returns the directories a data file is looked for in: the one
  64. // holding the executable, then the working directory, which is the order the
  65. // profile directory uses too.
  66. func lookupPaths() []string {
  67. var paths []string
  68. if exe, err := os.Executable(); err == nil {
  69. paths = append(paths, filepath.Dir(exe))
  70. }
  71. if cwd, err := os.Getwd(); err == nil {
  72. paths = append(paths, cwd)
  73. }
  74. return paths
  75. }
  76. // definitionLabels returns the channel names a definition gives the board, which
  77. // are the names VIA shows. They are the only channel names there are: a board
  78. // without a definition is addressed by its QMK subsystem name.
  79. func definitionLabels(vendorID, productID uint16) map[uint16]string {
  80. def := loadedDefinition(vendorID, productID)
  81. if def == nil {
  82. return nil
  83. }
  84. return def.Labels
  85. }
  86. // loadedDefinition returns the definition file for a board, from the data
  87. // directory or from the file --definition names, and nil when there is none.
  88. func loadedDefinition(vendorID, productID uint16) *intrgb.Definition {
  89. if definitionFlag != "" {
  90. if def, err := intrgb.LoadDefinition(definitionFlag); err == nil && def.Matches(vendorID, productID) {
  91. return def
  92. }
  93. return nil
  94. }
  95. dir, err := definitionsDir()
  96. if err != nil {
  97. return nil
  98. }
  99. defs, err := intrgb.LoadDefinitionsDir(dir)
  100. if err != nil {
  101. return nil
  102. }
  103. return intrgb.FindDefinition(defs, vendorID, productID)
  104. }
  105. // applyDefinitionLabels returns the display names with the definition's names
  106. // laid over them, and the names each channel keeps as alternatives. A definition
  107. // names a channel as VIA does, and the keyboards.json name and the QMK subsystem
  108. // name both stay accepted, so a command written before the definition arrived
  109. // keeps working.
  110. func applyDefinitionLabels(display map[uint16]string, vendorID, productID uint16) (map[uint16]string, map[uint16][]string) {
  111. labels := definitionLabels(vendorID, productID)
  112. merged := make(map[uint16]string, len(display)+len(labels))
  113. for number, name := range display {
  114. merged[number] = name
  115. }
  116. alternatives := make(map[uint16][]string, len(display))
  117. for number, label := range labels {
  118. if existing, ok := merged[number]; ok && existing != label {
  119. alternatives[number] = append(alternatives[number], existing)
  120. }
  121. merged[number] = label
  122. }
  123. for number, name := range merged {
  124. if subsystem := via.Channel(number).Subsystem(); subsystem != "" && subsystem != name {
  125. alternatives[number] = append(alternatives[number], subsystem)
  126. }
  127. }
  128. return merged, alternatives
  129. }
  130. // definitionsDir returns the data directory, looked for next to the executable
  131. // first and then in the working directory.
  132. var definitionsDir = func() (string, error) {
  133. paths := lookupPaths()
  134. for _, p := range paths {
  135. dir := filepath.Join(p, intrgb.DefinitionsDir)
  136. if info, err := os.Stat(dir); err == nil && info.IsDir() {
  137. return dir, nil
  138. }
  139. }
  140. if len(paths) == 0 {
  141. return intrgb.DefinitionsDir, nil
  142. }
  143. return filepath.Join(paths[0], intrgb.DefinitionsDir), nil
  144. }
  145. // ensureDefinitionsDir returns the data directory, creating it if it is not
  146. // there yet, so a fetch has somewhere to write to.
  147. func ensureDefinitionsDir() (string, error) {
  148. dir, err := definitionsDir()
  149. if err != nil {
  150. return "", err
  151. }
  152. if err := os.MkdirAll(dir, 0o755); err != nil {
  153. return "", fmt.Errorf("create %s: %w", dir, err)
  154. }
  155. return dir, nil
  156. }