catalog.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  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. // resolveCatalog returns the effect catalog for a board, in the order the tool
  13. // trusts: the file named by --definition, then the definition in the data
  14. // directory that matches the board. The second return value says which of the two
  15. // it was, because a board that has no names at all is a different situation from
  16. // one whose names 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. // No definition for this board, and nothing is compiled in: the keyboard holds
  58. // numbers, not names, and a board with no names is driven through raw IDs.
  59. return nil, "", nil
  60. }
  61. // lookupPaths returns the directories a data file is looked for in: the one
  62. // holding the executable, then the working directory, which is the order the
  63. // profile directory uses too.
  64. func lookupPaths() []string {
  65. var paths []string
  66. if exe, err := os.Executable(); err == nil {
  67. paths = append(paths, filepath.Dir(exe))
  68. }
  69. if cwd, err := os.Getwd(); err == nil {
  70. paths = append(paths, cwd)
  71. }
  72. return paths
  73. }
  74. // definitionLabels returns the channel names a definition gives the board, which
  75. // are the names VIA shows. They are the only channel names there are: a board
  76. // without a definition is addressed by its QMK subsystem name.
  77. func definitionLabels(vendorID, productID uint16) map[uint16]string {
  78. def := loadedDefinition(vendorID, productID)
  79. if def == nil {
  80. return nil
  81. }
  82. return def.Labels
  83. }
  84. // loadedDefinition returns the definition file for a board, from the data
  85. // directory or from the file --definition names, and nil when there is none.
  86. func loadedDefinition(vendorID, productID uint16) *intrgb.Definition {
  87. if definitionFlag != "" {
  88. if def, err := intrgb.LoadDefinition(definitionFlag); err == nil && def.Matches(vendorID, productID) {
  89. return def
  90. }
  91. return nil
  92. }
  93. dir, err := definitionsDir()
  94. if err != nil {
  95. return nil
  96. }
  97. defs, err := intrgb.LoadDefinitionsDir(dir)
  98. if err != nil {
  99. return nil
  100. }
  101. return intrgb.FindDefinition(defs, vendorID, productID)
  102. }
  103. // applyDefinitionLabels returns the display names a board answers to and, beside
  104. // them, the alternatives each channel keeps. The definition's label is the name
  105. // the board is called in VIA; the QMK subsystem name stays an accepted
  106. // alternative, because it follows from the channel number and is the one a
  107. // document can promise without knowing the board.
  108. // Only a channel the definition names gets a display name, and that is
  109. // deliberate. Adding an entry for every QMK lighting channel would put
  110. // "backlight" on channel 1 of a board that has none, where the same word is also
  111. // the board's name for channel 3 — and a name that reaches two channels is
  112. // refused. A channel the definition does not name is addressed by its subsystem
  113. // name, which is what the channel number alone tells us.
  114. func applyDefinitionLabels(vendorID, productID uint16) (map[uint16]string, map[uint16][]string) {
  115. labels := definitionLabels(vendorID, productID)
  116. display := make(map[uint16]string, len(labels))
  117. alternatives := make(map[uint16][]string, len(labels))
  118. for number, label := range labels {
  119. display[number] = label
  120. if subsystem := via.Channel(number).Subsystem(); subsystem != label {
  121. alternatives[number] = []string{subsystem}
  122. }
  123. }
  124. return display, alternatives
  125. }
  126. // boardName is what a board is called: the name its definition gives it, else the
  127. // USB product string the keyboard itself reports, else an honest placeholder.
  128. func boardName(dev intdevice.Device, _ map[uint16]string) string {
  129. if def := loadedDefinition(dev.VendorID, dev.ProductID); def != nil && def.Name != "" {
  130. return def.Name
  131. }
  132. if dev.Name != "" {
  133. return dev.Name
  134. }
  135. return "unknown model"
  136. }
  137. // definitionsDir returns the data directory, looked for next to the executable
  138. // first and then in the working directory.
  139. var definitionsDir = func() (string, error) {
  140. paths := lookupPaths()
  141. for _, p := range paths {
  142. dir := filepath.Join(p, intrgb.DefinitionsDir)
  143. if info, err := os.Stat(dir); err == nil && info.IsDir() {
  144. return dir, nil
  145. }
  146. }
  147. if len(paths) == 0 {
  148. return intrgb.DefinitionsDir, nil
  149. }
  150. return filepath.Join(paths[0], intrgb.DefinitionsDir), nil
  151. }
  152. // ensureDefinitionsDir returns the data directory, creating it if it is not
  153. // there yet, so a fetch has somewhere to write to.
  154. func ensureDefinitionsDir() (string, error) {
  155. dir, err := definitionsDir()
  156. if err != nil {
  157. return "", err
  158. }
  159. if err := os.MkdirAll(dir, 0o755); err != nil {
  160. return "", fmt.Errorf("create %s: %w", dir, err)
  161. }
  162. return dir, nil
  163. }