names.go 9.3 KB

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