definition.go 11 KB

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