package main import ( "fmt" 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 // resolveCatalog returns the effect catalog for a board, in the order the tool // trusts: the file named by --definition, then the definition in the data // directory that matches the board. The second return value says which of the two // 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 defs, err := intrgb.LoadDefinitionsDir(definitionsPath()); err == nil { if def := intrgb.FindDefinition(defs, vendorID, productID); def != nil { return def.Catalog, def.Path, nil } } // No definition for this board, and nothing is compiled 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 data // directory or from the file --definition names, 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 } defs, err := intrgb.LoadDefinitionsDir(definitionsPath()) if err != nil { return nil } return intrgb.FindDefinition(defs, 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 }