| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- package main
- import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "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 withNames(def.Catalog, def.VendorID, def.ProductID), 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 withNames(def.Catalog, def.VendorID, def.ProductID), def.Path, nil
- }
- // No definition for this board, not in a directory and not built in. A names
- // file may still hold names for it, and that is a board somebody wrote them
- // for; a board with neither holds numbers rather than names and is driven
- // through raw effect IDs.
- return withNames(nil, vendorID, productID), "", 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
- }
- // NamesDir is the name of the directory a user's own effect names are kept in. It
- // is not the definitions directory, because the two hold different things and only
- // one of them is the manufacturer's: a definition file is what a board's vendor
- // published, and a names file is what a person wrote. Merging them into one file
- // would make the tool author a document in someone else's name, and it would make
- // every name the user supplies indistinguishable from one the vendor published.
- const namesDir = "names"
- // namesPath is where a user's names for effect IDs live, and where a command that
- // writes one puts it.
- func namesPath() string {
- if namesDirOverride != "" {
- return namesDirOverride
- }
- return filepath.Join(userDataDir(), namesDir)
- }
- // ensureNamesDir returns the names directory, creating it if it is not there.
- func ensureNamesDir() (string, error) {
- dir, err := ensureDataDir(namesPath())
- if err != nil {
- return "", fmt.Errorf("create %s: %w", namesPath(), err)
- }
- return dir, nil
- }
- // candidateNames returns the names files in the per-user directory. A directory
- // that is not there yet is not an error: a user who has written none has none.
- func candidateNames() []*intrgb.Names {
- entries, err := os.ReadDir(namesPath())
- if err != nil {
- return nil
- }
- var out []*intrgb.Names
- for _, entry := range entries {
- if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
- continue
- }
- names, err := intrgb.LoadNamesFile(filepath.Join(namesPath(), entry.Name()))
- if err != nil {
- // A file the user wrote and that does not load is worth saying, but
- // not from here: a lookup is not the place to report it, and a broken
- // file for one board must not stop a name resolving for another. The
- // `names` command lists what is there and reports the broken one.
- continue
- }
- out = append(out, names)
- }
- return out
- }
- // withNames returns the catalog for a board with the user's names over the top of
- // whatever the definition said, and nil when there is neither. A names file is
- // consulted for every board, whether or not a definition exists, because the two
- // are independent: a user may override one name of a board whose definition the
- // tool has, and name a board whose definition nobody published.
- func withNames(catalog *intrgb.Catalog, vendorID, productID uint16) *intrgb.Catalog {
- for _, names := range candidateNames() {
- if !names.Matches(vendorID, productID) {
- continue
- }
- return names.Apply(catalog)
- }
- return catalog
- }
|