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, 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 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. 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 _, ch := range via.LightingChannels { label := labels[uint16(ch)] if label == "" { label = ch.Subsystem() } display[uint16(ch)] = label if label != ch.Subsystem() { alternatives[uint16(ch)] = []string{ch.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 }