definition.go 11 KB

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