package main import ( "fmt" "os" "path/filepath" 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 dir, err := definitionsDir(); err == nil { if defs, loadErr := intrgb.LoadDefinitionsDir(dir); loadErr == 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 } // lookupPaths returns the directories a data file is looked for in: the one // holding the executable, then the working directory, which is the order the // profile directory uses too. func lookupPaths() []string { var paths []string if exe, err := os.Executable(); err == nil { paths = append(paths, filepath.Dir(exe)) } if cwd, err := os.Getwd(); err == nil { paths = append(paths, cwd) } return paths } // 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 } // 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 } dir, err := definitionsDir() if err != nil { return nil } defs, err := intrgb.LoadDefinitionsDir(dir) if err != nil { return nil } return intrgb.FindDefinition(defs, vendorID, productID) } // 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 the 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" } // definitionsDir returns the data directory, looked for next to the executable // first and then in the working directory. var definitionsDir = func() (string, error) { paths := lookupPaths() for _, p := range paths { dir := filepath.Join(p, intrgb.DefinitionsDir) if info, err := os.Stat(dir); err == nil && info.IsDir() { return dir, nil } } if len(paths) == 0 { return intrgb.DefinitionsDir, nil } return filepath.Join(paths[0], intrgb.DefinitionsDir), nil } // 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 := definitionsDir() if err != nil { return "", err } if err := os.MkdirAll(dir, 0o755); err != nil { return "", fmt.Errorf("create %s: %w", dir, err) } return dir, nil }