package main import ( "fmt" "os" "path/filepath" 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, then the catalog compiled in for a known // board. The second return value says which of the three 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 } } } catalog, ok := intrgb.CatalogFor(vendorID, productID) if !ok { return nil, "", nil } return catalog, fmt.Sprintf("built in (%s)", catalog.Name()), 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 with the definition's names // laid over them, and the names each channel keeps as alternatives. A definition // names a channel as VIA does, and the keyboards.json name and the QMK subsystem // name both stay accepted, so a command written before the definition arrived // keeps working. func applyDefinitionLabels(display map[uint16]string, vendorID, productID uint16) (map[uint16]string, map[uint16][]string) { labels := definitionLabels(vendorID, productID) merged := make(map[uint16]string, len(display)+len(labels)) for number, name := range display { merged[number] = name } alternatives := make(map[uint16][]string, len(display)) for number, label := range labels { if existing, ok := merged[number]; ok && existing != label { alternatives[number] = append(alternatives[number], existing) } merged[number] = label } for number, name := range merged { if subsystem := via.Channel(number).Subsystem(); subsystem != "" && subsystem != name { alternatives[number] = append(alternatives[number], subsystem) } } return merged, alternatives } // 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 }