definition.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. package rgb
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io/fs"
  6. "os"
  7. "path/filepath"
  8. "sort"
  9. "strconv"
  10. "strings"
  11. "netdome.biz/paul/qmk-rgb/internal/via"
  12. )
  13. // DefinitionsDir is the name of the directory definition files are kept in. It
  14. // is a name and not a location: the tool resolves where that directory is, and
  15. // where it is written to when created, in definitionsPath. The keyboard is
  16. // identified first, so a file is only ever read for a board whose vendor and
  17. // product ID match.
  18. const DefinitionsDir = "definitions"
  19. // VendorProductID is the key a definition is filed under, and the key the
  20. // keyboard is looked up with: vendorID * 65536 + productID. The multiplication
  21. // rather than a shift is what VIA uses, and the two must stay equal.
  22. func VendorProductID(vendorID, productID uint16) int64 {
  23. return int64(vendorID)*65536 + int64(productID)
  24. }
  25. // effectValueKeys are the value keys whose control is a list of effects. A
  26. // definition that invents its own key for effects does not name them, because
  27. // nothing distinguishes such a control from any other dropdown.
  28. var effectValueKeys = map[string]via.Channel{
  29. "id_qmk_backlight_effect": via.ChannelBacklight,
  30. "id_qmk_rgblight_effect": via.ChannelRgblight,
  31. "id_qmk_rgb_matrix_effect": via.ChannelRgbMatrix,
  32. "id_qmk_audio_effect": via.ChannelAudio,
  33. "id_qmk_led_matrix_effect": via.ChannelLedMatrix,
  34. }
  35. // Definition is one keyboard definition file: the board it describes, and the
  36. // effect names it holds per lighting channel.
  37. type Definition struct {
  38. Path string
  39. Name string
  40. VendorID uint16
  41. ProductID uint16
  42. Catalog *Catalog
  43. // Labels is the name each lighting channel has in VIA's own interface, taken
  44. // from the sub-menu the definition puts it under. It is board data the file
  45. // already carries, and using it means the tool and VIA call the same channel
  46. // the same thing instead of the tool adding a third name of its own.
  47. Labels map[uint16]string
  48. }
  49. // definitionFile is the part of a VIA definition this tool reads. VIA serves
  50. // built definitions that carry vendorProductId as a number and no vendorId and
  51. // productId pair, so both spellings are optional and at least one is required.
  52. type definitionFile struct {
  53. Name string `json:"name"`
  54. VendorID string `json:"vendorId"`
  55. ProductID string `json:"productId"`
  56. VendorProductID int64 `json:"vendorProductId"`
  57. Menus json.RawMessage `json:"menus"`
  58. }
  59. // Matches reports whether a definition is the one for a board. A definition
  60. // for another board must not be used for this one, however it was found.
  61. func (d *Definition) Matches(vendorID, productID uint16) bool {
  62. return d.VendorID == vendorID && d.ProductID == productID
  63. }
  64. // LoadDefinition reads one definition file.
  65. func LoadDefinition(path string) (*Definition, error) {
  66. data, err := os.ReadFile(path)
  67. if err != nil {
  68. return nil, fmt.Errorf("read %s: %w", path, err)
  69. }
  70. def, err := ParseDefinition(path, data)
  71. if err != nil {
  72. return nil, err
  73. }
  74. return def, nil
  75. }
  76. // ParseDefinition reads a definition from bytes, and is what LoadDefinition
  77. // hands them to. A file fetched from a server is not a file on disk, and this
  78. // is also the check that rejects a page which is not a definition: VIA answers
  79. // an unknown board with its own web page and a success status, so a fetched
  80. // document has to be parsed and matched before it is believed.
  81. func ParseDefinition(source string, data []byte) (*Definition, error) {
  82. var file definitionFile
  83. if err := json.Unmarshal(data, &file); err != nil {
  84. return nil, fmt.Errorf("parse %s: not a keyboard definition: %w", source, err)
  85. }
  86. vendorID, productID, err := file.identifiers()
  87. if err != nil {
  88. return nil, fmt.Errorf("%s: %w", source, err)
  89. }
  90. board := file.Name
  91. if board == "" {
  92. board = filepath.Base(source)
  93. }
  94. return &Definition{
  95. Path: source,
  96. Name: board,
  97. VendorID: vendorID,
  98. ProductID: productID,
  99. Catalog: NewCatalog(board, parseEffects(file.Menus)),
  100. Labels: parseLabels(file.Menus),
  101. }, nil
  102. }
  103. // lightingValueKeys are the value keys a lighting channel binds. A control on one
  104. // of them says which channel the sub-menu around it is about.
  105. var lightingValueKeys = map[string]bool{
  106. "id_qmk_backlight_brightness": true,
  107. "id_qmk_backlight_effect": true,
  108. "id_qmk_rgblight_brightness": true,
  109. "id_qmk_rgblight_effect": true,
  110. "id_qmk_rgblight_effect_speed": true,
  111. "id_qmk_rgblight_color": true,
  112. "id_qmk_rgb_matrix_brightness": true,
  113. "id_qmk_rgb_matrix_effect": true,
  114. "id_qmk_rgb_matrix_effect_speed": true,
  115. "id_qmk_rgb_matrix_color": true,
  116. "id_qmk_audio_brightness": true,
  117. "id_qmk_audio_effect": true,
  118. "id_qmk_audio_effect_speed": true,
  119. "id_qmk_audio_color": true,
  120. "id_qmk_led_matrix_brightness": true,
  121. "id_qmk_led_matrix_effect": true,
  122. "id_qmk_led_matrix_color": true,
  123. }
  124. // parseLabels returns the sub-menu label per lighting channel: the name VIA
  125. // shows for that channel. A definition that names no channel yields nothing, so
  126. // the name a keyboard has elsewhere stays in charge.
  127. func parseLabels(menus json.RawMessage) map[uint16]string {
  128. if len(menus) == 0 {
  129. return nil
  130. }
  131. var decoded any
  132. if err := json.Unmarshal(menus, &decoded); err != nil {
  133. return nil
  134. }
  135. labels := make(map[uint16]string)
  136. var walk func(node any, submenu string)
  137. walk = func(node any, submenu string) {
  138. switch v := node.(type) {
  139. case []any:
  140. for _, child := range v {
  141. walk(child, submenu)
  142. }
  143. case map[string]any:
  144. label, _ := v["label"].(string)
  145. // A sub-menu is an object holding controls; a control is an object
  146. // holding a value binding. Which one this is tells us whether the
  147. // label around a control is the channel's name.
  148. if controls, ok := v["content"].([]any); ok && len(controls) > 0 {
  149. if _, isControl := controls[0].(map[string]any); isControl && label != "" {
  150. submenu = label
  151. }
  152. }
  153. if c, ok := v["content"].([]any); ok && len(c) >= 2 {
  154. if key, isString := c[0].(string); isString && lightingValueKeys[key] {
  155. if number, isNumber := c[1].(float64); isNumber && submenu != "" {
  156. labels[uint16(number)] = submenu
  157. }
  158. }
  159. }
  160. for _, child := range v {
  161. walk(child, submenu)
  162. }
  163. }
  164. }
  165. walk(decoded, "")
  166. if len(labels) == 0 {
  167. return nil
  168. }
  169. return labels
  170. }
  171. // identifiers returns the board a definition is for, from whichever of the two
  172. // spellings the file carries.
  173. func (f definitionFile) identifiers() (uint16, uint16, error) {
  174. if f.VendorProductID != 0 {
  175. return uint16(f.VendorProductID / 65536), uint16(f.VendorProductID % 65536), nil
  176. }
  177. if f.VendorID == "" || f.ProductID == "" {
  178. return 0, 0, fmt.Errorf("neither vendorProductId nor vendorId/productId; not a keyboard definition")
  179. }
  180. vendorID, err := parseHexID(f.VendorID)
  181. if err != nil {
  182. return 0, 0, fmt.Errorf("vendorId %q: %w", f.VendorID, err)
  183. }
  184. productID, err := parseHexID(f.ProductID)
  185. if err != nil {
  186. return 0, 0, fmt.Errorf("productId %q: %w", f.ProductID, err)
  187. }
  188. return vendorID, productID, nil
  189. }
  190. func parseHexID(s string) (uint16, error) {
  191. v, err := strconv.ParseUint(strings.TrimPrefix(strings.TrimSpace(s), "0x"), 16, 16)
  192. if err != nil {
  193. return 0, fmt.Errorf("not a hexadecimal USB ID: %w", err)
  194. }
  195. return uint16(v), nil
  196. }
  197. // parseEffects walks the menus of a definition and returns the effect list of
  198. // every lighting channel it names. The walk is generic because a definition
  199. // nests its controls as it likes; what identifies an effect list is the value
  200. // key and an options array beside it.
  201. func parseEffects(menus json.RawMessage) map[via.Channel][]Effect {
  202. if len(menus) == 0 {
  203. return nil
  204. }
  205. var found map[via.Channel][]Effect
  206. var walk func(node any)
  207. walk = func(node any) {
  208. switch v := node.(type) {
  209. case map[string]any:
  210. if effects, ok := effectList(v); ok {
  211. if found == nil {
  212. found = make(map[via.Channel][]Effect)
  213. }
  214. found[effects.channel] = append(found[effects.channel], effects.list...)
  215. }
  216. for _, child := range v {
  217. walk(child)
  218. }
  219. case []any:
  220. for _, child := range v {
  221. walk(child)
  222. }
  223. }
  224. }
  225. var decoded any
  226. if err := json.Unmarshal(menus, &decoded); err != nil {
  227. return nil
  228. }
  229. walk(decoded)
  230. return found
  231. }
  232. type channelEffects struct {
  233. channel via.Channel
  234. list []Effect
  235. }
  236. // effectList reads one UI control and reports the effects it offers, if it is an
  237. // effect list at all.
  238. func effectList(control map[string]any) (channelEffects, bool) {
  239. content, ok := control["content"].([]any)
  240. if !ok || len(content) < 2 {
  241. return channelEffects{}, false
  242. }
  243. valueKey, ok := content[0].(string)
  244. if !ok {
  245. return channelEffects{}, false
  246. }
  247. if _, known := effectValueKeys[valueKey]; !known {
  248. return channelEffects{}, false
  249. }
  250. channelNumber, ok := content[1].(float64)
  251. if !ok {
  252. return channelEffects{}, false
  253. }
  254. options, ok := control["options"].([]any)
  255. if !ok {
  256. return channelEffects{}, false
  257. }
  258. out := channelEffects{channel: via.Channel(int(channelNumber))}
  259. for position, option := range options {
  260. name, id, hasID, ok := optionName(option)
  261. if !ok {
  262. // An option that is neither a string nor a name/number pair has
  263. // no name to report; skipping it keeps the rest of the list.
  264. continue
  265. }
  266. if strings.TrimSpace(name) == "" {
  267. // An option with an empty name states a slot without naming it. That
  268. // is what a scaffold a user is meant to fill in looks like, and a
  269. // board that has one is not a board with an effect called "": the
  270. // slot is known and the name is not, so the channel reports no
  271. // effects until the name is there.
  272. continue
  273. }
  274. if !hasID {
  275. id = uint8(position)
  276. }
  277. out.list = append(out.list, Effect{ID: id, Name: name})
  278. }
  279. if len(out.list) == 0 {
  280. return channelEffects{}, false
  281. }
  282. sort.Slice(out.list, func(i, j int) bool { return out.list[i].ID < out.list[j].ID })
  283. return out, true
  284. }
  285. // optionName reads one dropdown option. VIA allows a bare string, which takes
  286. // its number from the position, or a name and number pair, whose number is the
  287. // value and need not be the position. The second is why an index into a name
  288. // list is not an effect ID.
  289. func optionName(option any) (name string, id uint8, hasID bool, ok bool) {
  290. switch v := option.(type) {
  291. case string:
  292. return v, 0, false, true
  293. case []any:
  294. if len(v) == 0 {
  295. return "", 0, false, false
  296. }
  297. label, isString := v[0].(string)
  298. if !isString {
  299. return "", 0, false, false
  300. }
  301. if len(v) < 2 {
  302. return label, 0, false, true
  303. }
  304. number, isNumber := v[1].(float64)
  305. if !isNumber {
  306. return label, 0, false, true
  307. }
  308. return label, uint8(number), true, true
  309. default:
  310. return "", 0, false, false
  311. }
  312. }
  313. // LoadDefinitionsDir reads every definition file in a directory. A file it
  314. // cannot use is an error rather than a silent skip: a definition the user
  315. // placed there and that does not work is worth saying out loud.
  316. func LoadDefinitionsDir(dir string) ([]*Definition, error) {
  317. entries, err := os.ReadDir(dir)
  318. if err != nil {
  319. return nil, fmt.Errorf("read %s: %w", dir, err)
  320. }
  321. var defs []*Definition
  322. for _, entry := range entries {
  323. if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
  324. continue
  325. }
  326. def, err := LoadDefinition(filepath.Join(dir, entry.Name()))
  327. if err != nil {
  328. return nil, err
  329. }
  330. defs = append(defs, def)
  331. }
  332. sort.Slice(defs, func(i, j int) bool { return defs[i].Path < defs[j].Path })
  333. return defs, nil
  334. }
  335. // LoadDefinitionsFS reads every definition file in a filesystem, which is what a
  336. // directory read and the set built into the binary have in common. The path is
  337. // only used to name the files in errors, so a caller that has an embed.FS can
  338. // pass the directory it presents itself as.
  339. //
  340. // A file it cannot use is an error rather than a silent skip, for the same
  341. // reason as in a directory: a definition that does not work is worth saying out
  342. // loud. A vendored file that fails this is a build defect, not a user's file, so
  343. // the caller decides what an error here means.
  344. func LoadDefinitionsFS(fsys fs.FS, dir string) ([]*Definition, error) {
  345. entries, err := fs.ReadDir(fsys, ".")
  346. if err != nil {
  347. return nil, fmt.Errorf("read %s: %w", dir, err)
  348. }
  349. var defs []*Definition
  350. for _, entry := range entries {
  351. if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
  352. continue
  353. }
  354. data, err := fs.ReadFile(fsys, entry.Name())
  355. if err != nil {
  356. return nil, fmt.Errorf("read %s: %w", filepath.Join(dir, entry.Name()), err)
  357. }
  358. def, err := ParseDefinition(filepath.Join(dir, entry.Name()), data)
  359. if err != nil {
  360. return nil, err
  361. }
  362. defs = append(defs, def)
  363. }
  364. sort.Slice(defs, func(i, j int) bool { return defs[i].Path < defs[j].Path })
  365. return defs, nil
  366. }
  367. // FindDefinition returns the definition for a board, or nil when none of the
  368. // definitions is for it. The first match wins, so the order the definitions are
  369. // passed in is the order the tool trusts: a file in a directory a user controls
  370. // before one built into the binary.
  371. func FindDefinition(defs []*Definition, vendorID, productID uint16) *Definition {
  372. for _, def := range defs {
  373. if def.VendorID == vendorID && def.ProductID == productID {
  374. return def
  375. }
  376. }
  377. return nil
  378. }
  379. // EffectValueKey returns the VIA value key a channel's effect list is filed
  380. // under, and whether one is registered for it. The lookup goes through
  381. // effectValueKeys rather than a second table, because that map is where a value
  382. // key is registered and a second one would fall behind it.
  383. func EffectValueKey(ch via.Channel) (string, bool) {
  384. for key, channel := range effectValueKeys {
  385. if channel == ch {
  386. return key, true
  387. }
  388. }
  389. return "", false
  390. }