package main import ( "os" "path/filepath" "strings" ) // Profiles and definitions are the tool's own files, and a binary installed with // `go install` sits in $GOPATH/bin where neither of them exists. They are looked // for next to the binary, then in the working directory, then under the platform's // per-user configuration directory. The first that exists wins, and the user // directory is where one is created. // // The order puts a repository checkout before the user directory on purpose: a // project that ships its own definitions, as this one does, must keep using them. // The other way round, a stale file in someone's home directory would silently // name a board they are working on. const dataDirName = "qmk-rgb-tool" // userConfigDir is a seam: os.UserConfigDir is the platform's own answer, and a // test cannot rely on which platform it runs on. var userConfigDir = os.UserConfigDir // executableDir and workingDir are seams for the same reason. var executableDir = func() string { if exe, err := os.Executable(); err == nil { return filepath.Dir(exe) } return "" } var workingDir = os.Getwd // The two overrides short-circuit the lookup, which is how the tests point the // lookup at a temporary directory. Empty means resolve it. var ( profilesDirOverride string definitionsDirOverride string ) // resolveDataDir returns the directory a named set of files lives in: the first // that exists, in the order next to the binary, then the working directory, then // the user's. When none exists the user's is returned, because that is where // something new belongs. func resolveDataDir(exeDir, cwd, name string) string { for _, dir := range []string{ filepath.Join(exeDir, name), filepath.Join(cwd, name), filepath.Join(userDataDir(), name), } { if dir == name { continue // an empty parent would resolve to a relative path } if info, err := os.Stat(dir); err == nil && info.IsDir() { return dir } } return filepath.Join(userDataDir(), name) } // userDataDir is this tool's directory under the platform's configuration // directory: ~/.config/qmk-rgb-tool on Linux, ~/Library/Application // Support/qmk-rgb-tool on macOS, %AppData%\qmk-rgb-tool on Windows. A hardcoded // ~/.config would be wrong on two of the three. func userDataDir() string { base, err := userConfigDir() if err != nil || base == "" { return dataDirName } return filepath.Join(base, dataDirName) } // definitionsPath is where the VIA definition files are read from and fetched to. func definitionsPath() string { if definitionsDirOverride != "" { return definitionsDirOverride } return resolveDataDir(executableDir(), workingDirResult(), "definitions") } // profilesPath is where the profiles are read from and written to. func profilesPath() string { if profilesDirOverride != "" { return profilesDirOverride } return resolveDataDir(executableDir(), workingDirResult(), "profiles") } // workingDirResult keeps a failing os.Getwd from turning into a relative path. func workingDirResult() string { cwd, err := workingDir() if err != nil { return "" } return cwd } // ensureDataDir returns a directory and creates it, for the commands that write. func ensureDataDir(path string) (string, error) { if err := os.MkdirAll(path, 0o755); err != nil { return "", err } return path, nil } // describeDataDir annotates a path only where the answer is not the obvious one: // the user directory is neither beside the binary nor in the directory the // command was run from, so a path pointing there is worth marking. The other two // are the two places a user would look, and the full path is printed either way. func describeDataDir(path string) string { if user := userDataDir(); path == user || strings.HasPrefix(path, user+string(filepath.Separator)) { return path + " (your user directory)" } return path }