| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- package main
- import (
- "fmt"
- "sync"
- "netdome.biz/paul/qmk-rgb/definitions"
- intdevice "netdome.biz/paul/qmk-rgb/internal/device"
- intrgb "netdome.biz/paul/qmk-rgb/internal/rgb"
- "netdome.biz/paul/qmk-rgb/internal/via"
- )
- // definitionFlag names a definition file to use instead of looking one up.
- var definitionFlag string
- // builtInDefinitions are the definition files compiled into the binary, parsed
- // once. They are the last resort in a lookup and not a catalog of their own: the
- // files are the vendor's own, kept as served, and one a user has placed in a
- // definitions directory takes precedence over the copy here. Parsing them per
- // lookup would be wasted work on every command that names an effect.
- var builtInDefinitions = sync.OnceValue(func() []*intrgb.Definition {
- defs, err := intrgb.LoadDefinitionsFS(definitions.Vendored, "definitions")
- if err != nil {
- // A vendored file that does not parse is a build defect, not something
- // a user can fix by putting a file somewhere. Carry on without them:
- // the board is then driven through raw effect IDs, which is worse but
- // still works, and a lookup must not fail because of it.
- return nil
- }
- return defs
- })
- // candidateDefinitions returns every definition a lookup may match, in the order
- // the tool trusts: the file in the per-user directory first, so one placed there
- // or fetched for the board overrides the copy built into the binary, and the
- // built-in files last so that an installed tool has the boards whose definitions
- // cannot be fetched.
- func candidateDefinitions() []*intrgb.Definition {
- var defs []*intrgb.Definition
- if fromDir, err := intrgb.LoadDefinitionsDir(definitionsPath()); err == nil {
- defs = append(defs, fromDir...)
- }
- return append(defs, builtInDefinitions()...)
- }
- // resolveCatalog returns the effect catalog for a board, in the order the tool
- // trusts: the file named by --definition, then the file in the per-user
- // definitions directory that matches the board, then the one built into the
- // binary. The second return value says which of them it was, because a board
- // that has no names at all is a different situation from one whose names came
- // from somewhere the user can see.
- //
- // This is the one place a catalog is looked up. A command that reached for a
- // catalog itself would silently ignore a definition file the user had placed,
- // which is the whole point of having one.
- func resolveCatalog(target targetDeviceData) (*intrgb.Catalog, string, error) {
- if definitionFlag != "" {
- def, err := intrgb.LoadDefinition(definitionFlag)
- if err != nil {
- return nil, "", err
- }
- if !def.Matches(target.Device.VendorID, target.Device.ProductID) {
- return nil, "", fmt.Errorf("%s is a definition for %s (0x%04X/0x%04X), not for this keyboard (0x%04X/0x%04X)",
- definitionFlag, def.Name, def.VendorID, def.ProductID,
- target.Device.VendorID, target.Device.ProductID)
- }
- return def.Catalog, def.Path, nil
- }
- catalog, source, err := resolveCatalogFor(target.Device.VendorID, target.Device.ProductID)
- return catalog, source, err
- }
- // resolveCatalogFor is the lookup without a target, for the commands that report
- // on a board rather than open it.
- func resolveCatalogFor(vendorID, productID uint16) (*intrgb.Catalog, string, error) {
- if definitionFlag != "" {
- def, err := intrgb.LoadDefinition(definitionFlag)
- if err != nil {
- return nil, "", err
- }
- if !def.Matches(vendorID, productID) {
- return nil, "", nil
- }
- return def.Catalog, def.Path, nil
- }
- if def := intrgb.FindDefinition(candidateDefinitions(), vendorID, productID); def != nil {
- return def.Catalog, def.Path, nil
- }
- // No definition for this board, not in a directory and not built in: the
- // keyboard holds numbers, not names, and a board with no names is driven
- // through raw IDs.
- return nil, "", nil
- }
- // loadedDefinition returns the definition file for a board, from the file
- // --definition names, from the per-user directory or from the binary, and nil
- // when there is none.
- func loadedDefinition(vendorID, productID uint16) *intrgb.Definition {
- if definitionFlag != "" {
- if def, err := intrgb.LoadDefinition(definitionFlag); err == nil && def.Matches(vendorID, productID) {
- return def
- }
- return nil
- }
- return intrgb.FindDefinition(candidateDefinitions(), vendorID, productID)
- }
- // definitionLabels returns the channel names a definition gives the board, which
- // are the names VIA shows. They are the only channel names there are: a board
- // without a definition is addressed by its QMK subsystem name.
- func definitionLabels(vendorID, productID uint16) map[uint16]string {
- def := loadedDefinition(vendorID, productID)
- if def == nil {
- return nil
- }
- return def.Labels
- }
- // applyDefinitionLabels returns the display names a board answers to and, beside
- // them, the alternatives each channel keeps. The definition's label is the name
- // the board is called in VIA; the QMK subsystem name stays an accepted
- // alternative, because it follows from the channel number and is the one a
- // document can promise without knowing a board.
- //
- // Only a channel the definition names gets a display name, and that is
- // deliberate. Adding an entry for every QMK lighting channel would put
- // "backlight" on channel 1 of a board that has none, where the same word is also
- // the board's name for channel 3 — and a name that reaches two channels is
- // refused. A channel the definition does not name is addressed by its subsystem
- // name, which is what the channel number alone tells us.
- func applyDefinitionLabels(vendorID, productID uint16) (map[uint16]string, map[uint16][]string) {
- labels := definitionLabels(vendorID, productID)
- display := make(map[uint16]string, len(labels))
- alternatives := make(map[uint16][]string, len(labels))
- for number, label := range labels {
- display[number] = label
- if subsystem := via.Channel(number).Subsystem(); subsystem != label {
- alternatives[number] = []string{subsystem}
- }
- }
- return display, alternatives
- }
- // boardName is what a board is called: the name its definition gives it, else the
- // USB product string the keyboard itself reports, else an honest placeholder.
- func boardName(dev intdevice.Device, _ map[uint16]string) string {
- if def := loadedDefinition(dev.VendorID, dev.ProductID); def != nil && def.Name != "" {
- return def.Name
- }
- if dev.Name != "" {
- return dev.Name
- }
- return "unknown model"
- }
- // ensureDefinitionsDir returns the data directory, creating it if it is not
- // there yet, so a fetch has somewhere to write to.
- func ensureDefinitionsDir() (string, error) {
- dir, err := ensureDataDir(definitionsPath())
- if err != nil {
- return "", fmt.Errorf("create %s: %w", definitionsPath(), err)
- }
- return dir, nil
- }
|