oh-my-posh/src/environment/shell.go

787 lines
19 KiB
Go
Raw Normal View History

package environment
2019-03-13 04:14:30 -07:00
import (
2021-12-19 07:42:39 -08:00
"bytes"
"context"
"errors"
"fmt"
2022-02-25 03:51:38 -08:00
"io"
2020-10-07 04:32:42 -07:00
"io/ioutil"
"log"
"net/http"
2022-02-25 03:51:38 -08:00
"net/url"
"oh-my-posh/regex"
2019-03-13 04:14:30 -07:00
"os"
"os/exec"
2020-10-01 11:57:02 -07:00
"path/filepath"
2019-03-13 04:14:30 -07:00
"runtime"
"strings"
"time"
2019-03-13 04:14:30 -07:00
"github.com/distatus/battery"
process "github.com/shirou/gopsutil/v3/process"
2019-03-13 04:14:30 -07:00
)
const (
Unknown = "unknown"
WindowsPlatform = "windows"
DarwinPlatform = "darwin"
LinuxPlatform = "linux"
)
2022-03-12 13:04:08 -08:00
type Flags struct {
ErrorCode int
Config string
Shell string
PWD string
PSWD string
ExecutionTime float64
Eval bool
StackCount int
Migrate bool
TerminalWidth int
// PrintInit *bool
// PrintConfig *bool
// PrintShell *bool
// PrintTransient *bool
// PrintSecondary *bool
// PrintValid *bool
// PrintError *bool
// ConfigFormat *string
// Version *bool
// Millis *bool
// Init *bool
// ExportPNG *bool
// Author *string
// CursorPadding *int
// RPromptOffset *int
// RPrompt *bool
// BGColor *string
// Command *string
// CachePath *bool
// Write *bool
}
type CommandError struct {
Err string
ExitCode int
}
func (e *CommandError) Error() string {
return e.Err
}
type NoBatteryError struct{}
func (m *NoBatteryError) Error() string {
return "no battery"
}
type FileInfo struct {
ParentFolder string
Path string
IsDir bool
}
type Cache interface {
Init(home string)
Close()
Get(key string) (string, bool)
// ttl in minutes
Set(key, value string, ttl int)
2021-09-21 11:22:59 -07:00
}
2021-12-14 23:49:32 -08:00
type HTTPRequestModifier func(request *http.Request)
type WindowsRegistryValueType int
const (
RegQword WindowsRegistryValueType = iota
RegDword
RegString
)
type WindowsRegistryValue struct {
ValueType WindowsRegistryValueType
Qword uint64
Dword uint32
Str string
}
type WifiType string
type WifiInfo struct {
SSID string
Interface string
RadioType WifiType
PhysType WifiType
Authentication WifiType
Cipher WifiType
Channel int
ReceiveRate int
TransmitRate int
Signal int
Error string
}
type TemplateCache struct {
Root bool
PWD string
Folder string
Shell string
UserName string
HostName string
Code int
Env map[string]string
OS string
WSL bool
}
2022-01-01 11:09:52 -08:00
type Environment interface {
Getenv(key string) string
Pwd() string
Home() string
User() string
Root() bool
Host() (string, error)
GOOS() string
Shell() string
Platform() string
ErrorCode() int
2022-02-19 10:04:06 -08:00
PathSeparator() string
HasFiles(pattern string) bool
HasFilesInDir(dir, pattern string) bool
HasFolder(folder string) bool
HasParentFilePath(path string) (fileInfo *FileInfo, err error)
2022-02-13 23:41:33 -08:00
HasFileInParentDirs(pattern string, depth uint) bool
HasCommand(command string) bool
FileContent(file string) string
FolderList(path string) []string
RunCommand(command string, args ...string) (string, error)
RunShellCommand(shell, command string) string
ExecutionTime() float64
2022-03-12 13:04:08 -08:00
Flags() *Flags
BatteryInfo() ([]*battery.Battery, error)
QueryWindowTitles(processName, windowTitleRegex string) (string, error)
WindowsRegistryKeyValue(path string) (*WindowsRegistryValue, error)
2022-01-07 10:41:58 -08:00
HTTPRequest(url string, timeout int, requestModifiers ...HTTPRequestModifier) ([]byte, error)
IsWsl() bool
IsWsl2() bool
StackCount() int
TerminalWidth() (int, error)
CachePath() string
Cache() Cache
Close()
Logs() string
InWSLSharedDrive() bool
ConvertToLinuxPath(path string) string
ConvertToWindowsPath(path string) string
WifiNetwork() (*WifiInfo, error)
TemplateCache() *TemplateCache
2019-03-13 04:14:30 -07:00
}
type commandCache struct {
commands *concurrentMap
}
func (c *commandCache) set(command, path string) {
c.commands.set(command, path)
}
func (c *commandCache) get(command string) (string, bool) {
2021-09-21 22:53:59 -07:00
cmd, found := c.commands.get(command)
if !found {
return "", false
}
2021-09-23 13:57:38 -07:00
command, ok := cmd.(string)
return command, ok
}
type logType string
const (
Error logType = "error"
Debug logType = "debug"
)
type ShellEnvironment struct {
2022-03-12 13:04:08 -08:00
CmdFlags *Flags
Version string
2022-01-18 00:48:47 -08:00
cwd string
cmdCache *commandCache
fileCache *fileCache
tmplCache *TemplateCache
2022-01-18 00:48:47 -08:00
logBuilder strings.Builder
debug bool
}
2022-03-12 13:04:08 -08:00
func (env *ShellEnvironment) Init(debug bool) {
if env.CmdFlags == nil {
env.CmdFlags = &Flags{}
}
2022-01-18 12:25:18 -08:00
env.fileCache = &fileCache{}
env.fileCache.Init(env.CachePath())
env.resolveConfigPath()
2021-11-16 22:16:43 -08:00
env.cmdCache = &commandCache{
commands: newConcurrentMap(),
}
2022-03-12 13:04:08 -08:00
if debug {
2021-11-16 22:16:43 -08:00
env.debug = true
log.SetOutput(&env.logBuilder)
}
}
func (env *ShellEnvironment) resolveConfigPath() {
if len(env.CmdFlags.Config) == 0 {
env.CmdFlags.Config = env.Getenv("POSH_THEME")
2022-02-25 03:51:38 -08:00
}
if len(env.CmdFlags.Config) == 0 {
2022-03-16 06:03:33 -07:00
env.CmdFlags.Config = fmt.Sprintf("https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/v%s/themes/default.omp.json", env.Version)
}
2022-03-12 13:04:08 -08:00
location, err := url.ParseRequestURI(env.CmdFlags.Config)
2022-02-25 03:51:38 -08:00
if err == nil {
env.getConfigPath(location.String())
return
}
2022-01-13 23:39:08 -08:00
// Cygwin path always needs the full path as we're on Windows but not really.
// Doing filepath actions will convert it to a Windows path and break the init script.
if env.Platform() == WindowsPlatform && env.Shell() == "bash" {
2022-01-13 23:39:08 -08:00
return
}
2022-03-12 13:04:08 -08:00
configFile := env.CmdFlags.Config
if strings.HasPrefix(configFile, "~") {
configFile = strings.TrimPrefix(configFile, "~")
configFile = filepath.Join(env.Home(), configFile)
}
if !filepath.IsAbs(configFile) {
if absConfigFile, err := filepath.Abs(configFile); err == nil {
configFile = absConfigFile
}
}
2022-03-12 13:04:08 -08:00
env.CmdFlags.Config = filepath.Clean(configFile)
}
func (env *ShellEnvironment) getConfigPath(location string) {
cfg, err := env.HTTPRequest(location, 5000)
if err != nil {
return
}
configPath := filepath.Join(env.CachePath(), "config.omp.json")
out, err := os.Create(configPath)
if err != nil {
return
}
defer out.Close()
_, err = io.Copy(out, bytes.NewReader(cfg))
if err != nil {
return
}
env.CmdFlags.Config = configPath
}
func (env *ShellEnvironment) trace(start time.Time, function string, args ...string) {
2021-11-16 22:16:43 -08:00
if !env.debug {
return
}
elapsed := time.Since(start)
2021-08-04 03:52:54 -07:00
trace := fmt.Sprintf("%s duration: %s, args: %s", function, elapsed, strings.Trim(fmt.Sprint(args), "[]"))
log.Println(trace)
}
func (env *ShellEnvironment) log(lt logType, function, message string) {
2021-11-16 22:16:43 -08:00
if !env.debug {
return
}
trace := fmt.Sprintf("%s: %s\n%s", lt, function, message)
log.Println(trace)
}
func (env *ShellEnvironment) Getenv(key string) string {
defer env.trace(time.Now(), "Getenv", key)
val := os.Getenv(key)
env.log(Debug, "Getenv", val)
return val
2019-03-13 04:14:30 -07:00
}
func (env *ShellEnvironment) Pwd() string {
defer env.trace(time.Now(), "Pwd")
defer func() {
env.log(Debug, "Pwd", env.cwd)
}()
2020-10-12 00:04:37 -07:00
if env.cwd != "" {
return env.cwd
}
correctPath := func(pwd string) string {
2021-01-07 10:29:34 -08:00
// on Windows, and being case sensitive and not consistent and all, this gives silly issues
driveLetter := regex.GetCompiledRegex(`^[a-z]:`)
return driveLetter.ReplaceAllStringFunc(pwd, strings.ToUpper)
}
2022-03-12 13:04:08 -08:00
if env.CmdFlags != nil && env.CmdFlags.PWD != "" {
env.cwd = correctPath(env.CmdFlags.PWD)
2020-10-12 00:04:37 -07:00
return env.cwd
}
dir, err := os.Getwd()
if err != nil {
env.log(Error, "Pwd", err.Error())
return ""
}
2020-10-12 00:04:37 -07:00
env.cwd = correctPath(dir)
return env.cwd
}
func (env *ShellEnvironment) HasFiles(pattern string) bool {
defer env.trace(time.Now(), "HasFiles", pattern)
cwd := env.Pwd()
2022-02-19 10:04:06 -08:00
pattern = cwd + env.PathSeparator() + pattern
2020-10-01 11:57:02 -07:00
matches, err := filepath.Glob(pattern)
if err != nil {
env.log(Error, "HasFiles", err.Error())
2020-10-01 11:57:02 -07:00
return false
}
return len(matches) > 0
}
func (env *ShellEnvironment) HasFilesInDir(dir, pattern string) bool {
defer env.trace(time.Now(), "HasFilesInDir", pattern)
2022-02-19 10:04:06 -08:00
pattern = dir + env.PathSeparator() + pattern
matches, err := filepath.Glob(pattern)
if err != nil {
env.log(Error, "HasFilesInDir", err.Error())
return false
}
return len(matches) > 0
}
2022-02-13 23:41:33 -08:00
func (env *ShellEnvironment) HasFileInParentDirs(pattern string, depth uint) bool {
defer env.trace(time.Now(), "HasFileInParent", pattern, fmt.Sprint(depth))
currentFolder := env.Pwd()
for c := 0; c < int(depth); c++ {
if env.HasFilesInDir(currentFolder, pattern) {
return true
}
if dir := filepath.Dir(currentFolder); dir != currentFolder {
currentFolder = dir
} else {
return false
}
}
return false
}
func (env *ShellEnvironment) HasFolder(folder string) bool {
defer env.trace(time.Now(), "HasFolder", folder)
2020-10-07 04:32:42 -07:00
_, err := os.Stat(folder)
return !os.IsNotExist(err)
}
func (env *ShellEnvironment) FileContent(file string) string {
defer env.trace(time.Now(), "FileContent", file)
2020-10-07 04:32:42 -07:00
content, err := ioutil.ReadFile(file)
if err != nil {
env.log(Error, "FileContent", err.Error())
2020-10-07 04:32:42 -07:00
return ""
}
return string(content)
}
func (env *ShellEnvironment) FolderList(path string) []string {
defer env.trace(time.Now(), "FolderList", path)
content, err := os.ReadDir(path)
if err != nil {
env.log(Error, "FolderList", err.Error())
return nil
}
var folderNames []string
for _, s := range content {
if s.IsDir() {
folderNames = append(folderNames, s.Name())
}
}
return folderNames
}
2022-02-19 10:04:06 -08:00
func (env *ShellEnvironment) PathSeparator() string {
defer env.trace(time.Now(), "PathSeparator")
2019-03-13 04:14:30 -07:00
return string(os.PathSeparator)
}
func (env *ShellEnvironment) User() string {
defer env.trace(time.Now(), "User")
user := os.Getenv("USER")
if user == "" {
user = os.Getenv("USERNAME")
}
return user
2019-03-13 04:14:30 -07:00
}
func (env *ShellEnvironment) Host() (string, error) {
defer env.trace(time.Now(), "Host")
2019-03-13 04:14:30 -07:00
hostName, err := os.Hostname()
if err != nil {
env.log(Error, "Host", err.Error())
2019-03-13 04:14:30 -07:00
return "", err
}
return cleanHostName(hostName), nil
}
func (env *ShellEnvironment) GOOS() string {
defer env.trace(time.Now(), "GOOS")
2019-03-13 04:14:30 -07:00
return runtime.GOOS
}
func (env *ShellEnvironment) RunCommand(command string, args ...string) (string, error) {
defer env.trace(time.Now(), "RunCommand", append([]string{command}, args...)...)
if cmd, ok := env.cmdCache.get(command); ok {
command = cmd
}
2021-08-03 23:46:59 -07:00
cmd := exec.Command(command, args...)
2021-12-19 07:42:39 -08:00
var out bytes.Buffer
var err bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &err
cmdErr := cmd.Run()
if cmdErr != nil {
output := err.String()
errorStr := fmt.Sprintf("cmd.Start() failed with '%s'", output)
env.log(Error, "RunCommand", errorStr)
2021-12-19 07:42:39 -08:00
return output, cmdErr
2021-08-03 23:46:59 -07:00
}
// some silly commands return 0 and the output is in stderr instead of stdout
result := out.String()
if len(result) == 0 {
result = err.String()
}
output := strings.TrimSpace(result)
env.log(Debug, "RunCommand", output)
return output, nil
2019-03-13 04:14:30 -07:00
}
func (env *ShellEnvironment) RunShellCommand(shell, command string) string {
defer env.trace(time.Now(), "RunShellCommand", shell, command)
if out, err := env.RunCommand(shell, "-c", command); err == nil {
return out
}
return ""
2019-03-13 04:14:30 -07:00
}
func (env *ShellEnvironment) HasCommand(command string) bool {
defer env.trace(time.Now(), "HasCommand", command)
if _, ok := env.cmdCache.get(command); ok {
return true
}
path, err := exec.LookPath(command)
if err == nil {
env.cmdCache.set(command, path)
return true
}
path, err = env.LookWinAppPath(command)
if err == nil {
env.cmdCache.set(command, path)
return true
}
env.log(Error, "HasCommand", err.Error())
return false
2019-03-13 04:14:30 -07:00
}
func (env *ShellEnvironment) ErrorCode() int {
defer env.trace(time.Now(), "ErrorCode")
2022-03-12 13:04:08 -08:00
return env.CmdFlags.ErrorCode
2019-03-13 04:14:30 -07:00
}
func (env *ShellEnvironment) ExecutionTime() float64 {
defer env.trace(time.Now(), "ExecutionTime")
2022-03-12 13:04:08 -08:00
if env.CmdFlags.ExecutionTime < 0 {
return 0
}
2022-03-12 13:04:08 -08:00
return env.CmdFlags.ExecutionTime
2020-12-06 13:03:40 -08:00
}
2022-03-12 13:04:08 -08:00
func (env *ShellEnvironment) Flags() *Flags {
defer env.trace(time.Now(), "Flags")
return env.CmdFlags
2019-03-13 04:14:30 -07:00
}
func (env *ShellEnvironment) BatteryInfo() ([]*battery.Battery, error) {
defer env.trace(time.Now(), "BatteryInfo")
batteries, err := battery.GetAll()
// actual error, return it
if err != nil && len(batteries) == 0 {
env.log(Error, "BatteryInfo", err.Error())
return nil, err
}
// there are no batteries found
if len(batteries) == 0 {
return nil, &NoBatteryError{}
}
// some batteries fail to get retrieved, filter them out if present
validBatteries := []*battery.Battery{}
for _, batt := range batteries {
if batt != nil {
validBatteries = append(validBatteries, batt)
}
}
// clean minor errors
unableToRetrieveBatteryInfo := "A device which does not exist was specified."
unknownChargeRate := "Unknown value received"
var fatalErr battery.Errors
ignoreErr := func(err error) bool {
if e, ok := err.(battery.ErrPartial); ok {
// ignore unknown charge rate value error
if e.Current == nil &&
e.Design == nil &&
e.DesignVoltage == nil &&
e.Full == nil &&
e.State == nil &&
e.Voltage == nil &&
e.ChargeRate != nil &&
e.ChargeRate.Error() == unknownChargeRate {
return true
}
}
return false
}
if batErr, ok := err.(battery.Errors); ok {
for _, err := range batErr {
if !ignoreErr(err) {
fatalErr = append(fatalErr, err)
}
}
}
// when battery info fails to get retrieved but there is at least one valid battery, return it without error
if len(validBatteries) > 0 && fatalErr != nil && strings.Contains(fatalErr.Error(), unableToRetrieveBatteryInfo) {
return validBatteries, nil
}
// another error occurred (possibly unmapped use-case), return it
if fatalErr != nil {
env.log(Error, "BatteryInfo", fatalErr.Error())
return nil, fatalErr
}
// everything is fine
return validBatteries, nil
2019-03-13 04:14:30 -07:00
}
func (env *ShellEnvironment) Shell() string {
defer env.trace(time.Now(), "Shell")
2022-03-12 13:04:08 -08:00
if env.CmdFlags.Shell != "" {
return env.CmdFlags.Shell
2020-12-27 05:59:40 -08:00
}
2020-09-15 04:44:53 -07:00
pid := os.Getppid()
2020-10-23 07:36:40 -07:00
p, _ := process.NewProcess(int32(pid))
name, err := p.Name()
2020-09-24 10:11:56 -07:00
if err != nil {
env.log(Error, "Shell", err.Error())
return Unknown
2020-09-24 10:11:56 -07:00
}
if name == "cmd.exe" {
p, _ = p.Parent()
name, err = p.Name()
}
if err != nil {
env.log(Error, "Shell", err.Error())
return Unknown
}
// Cache the shell value to speed things up.
2022-03-12 13:04:08 -08:00
env.CmdFlags.Shell = strings.Trim(strings.Replace(name, ".exe", "", 1), " ")
return env.CmdFlags.Shell
2020-09-15 04:44:53 -07:00
}
2022-02-25 03:51:38 -08:00
func (env *ShellEnvironment) HTTPRequest(targetURL string, timeout int, requestModifiers ...HTTPRequestModifier) ([]byte, error) {
defer env.trace(time.Now(), "HTTPRequest", targetURL)
ctx, cncl := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
defer cncl()
2022-02-25 03:51:38 -08:00
request, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil)
if err != nil {
return nil, err
}
2021-12-14 23:49:32 -08:00
for _, modifier := range requestModifiers {
modifier(request)
}
response, err := client.Do(request)
if err != nil {
2022-01-07 10:41:58 -08:00
env.log(Error, "HTTPRequest", err.Error())
return nil, err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
2022-01-07 10:41:58 -08:00
env.log(Error, "HTTPRequest", err.Error())
return nil, err
}
2022-03-19 09:26:52 -07:00
env.log(Debug, "HTTPRequest", string(body))
return body, nil
}
func (env *ShellEnvironment) HasParentFilePath(path string) (*FileInfo, error) {
defer env.trace(time.Now(), "HasParentFilePath", path)
currentFolder := env.Pwd()
for {
searchPath := filepath.Join(currentFolder, path)
info, err := os.Stat(searchPath)
if err == nil {
return &FileInfo{
ParentFolder: currentFolder,
Path: searchPath,
IsDir: info.IsDir(),
}, nil
}
if !os.IsNotExist(err) {
return nil, err
}
if dir := filepath.Dir(currentFolder); dir != currentFolder {
currentFolder = dir
continue
}
env.log(Error, "HasParentFilePath", err.Error())
return nil, errors.New("no match at root level")
}
}
func (env *ShellEnvironment) StackCount() int {
defer env.trace(time.Now(), "StackCount")
2022-03-12 13:04:08 -08:00
if env.CmdFlags.StackCount < 0 {
return 0
}
2022-03-12 13:04:08 -08:00
return env.CmdFlags.StackCount
}
func (env *ShellEnvironment) Cache() Cache {
2021-09-21 11:22:59 -07:00
return env.fileCache
}
func (env *ShellEnvironment) Close() {
env.fileCache.Close()
}
func (env *ShellEnvironment) Logs() string {
2021-11-16 22:16:43 -08:00
return env.logBuilder.String()
2021-09-21 11:22:59 -07:00
}
func (env *ShellEnvironment) TemplateCache() *TemplateCache {
defer env.trace(time.Now(), "TemplateCache")
2022-01-18 00:48:47 -08:00
if env.tmplCache != nil {
return env.tmplCache
}
tmplCache := &TemplateCache{
Root: env.Root(),
Shell: env.Shell(),
Code: env.ErrorCode(),
WSL: env.IsWsl(),
2022-01-18 00:48:47 -08:00
}
tmplCache.Env = make(map[string]string)
2022-01-18 00:48:47 -08:00
const separator = "="
values := os.Environ()
for value := range values {
splitted := strings.Split(values[value], separator)
if len(splitted) != 2 {
continue
}
key := splitted[0]
val := splitted[1:]
tmplCache.Env[key] = strings.Join(val, separator)
2022-01-18 00:48:47 -08:00
}
pwd := env.Pwd()
pwd = strings.Replace(pwd, env.Home(), "~", 1)
tmplCache.PWD = pwd
tmplCache.Folder = Base(env, pwd)
tmplCache.UserName = env.User()
if host, err := env.Host(); err == nil {
tmplCache.HostName = host
2022-01-18 00:48:47 -08:00
}
goos := env.GOOS()
tmplCache.OS = goos
if goos == LinuxPlatform {
tmplCache.OS = env.Platform()
}
env.tmplCache = tmplCache
return tmplCache
2022-01-18 00:48:47 -08:00
}
func DirMatchesOneOf(env Environment, dir string, regexes []string) bool {
normalizedCwd := strings.ReplaceAll(dir, "\\", "/")
normalizedHomeDir := strings.ReplaceAll(env.Home(), "\\", "/")
for _, element := range regexes {
normalizedElement := strings.ReplaceAll(element, "\\\\", "/")
if strings.HasPrefix(normalizedElement, "~") {
normalizedElement = strings.Replace(normalizedElement, "~", normalizedHomeDir, 1)
}
pattern := fmt.Sprintf("^%s$", normalizedElement)
goos := env.GOOS()
if goos == WindowsPlatform || goos == DarwinPlatform {
pattern = "(?i)" + pattern
}
matched := regex.MatchString(pattern, normalizedCwd)
if matched {
return true
}
}
return false
}
// Base returns the last element of path.
// Trailing path separators are removed before extracting the last element.
// If the path consists entirely of separators, Base returns a single separator.
func Base(env Environment, path string) string {
if path == "/" {
return path
}
volumeName := filepath.VolumeName(path)
// Strip trailing slashes.
2022-02-19 10:04:06 -08:00
for len(path) > 0 && string(path[len(path)-1]) == env.PathSeparator() {
path = path[0 : len(path)-1]
}
if volumeName == path {
return path
}
// Throw away volume name
path = path[len(filepath.VolumeName(path)):]
// Find the last element
i := len(path) - 1
2022-02-19 10:04:06 -08:00
for i >= 0 && string(path[i]) != env.PathSeparator() {
i--
}
if i >= 0 {
path = path[i+1:]
}
// If empty now, it had only slashes.
if path == "" {
2022-02-19 10:04:06 -08:00
return env.PathSeparator()
}
return path
}
2019-03-13 04:14:30 -07:00
func cleanHostName(hostName string) string {
garbage := []string{
".lan",
".local",
".localdomain",
2019-03-13 04:14:30 -07:00
}
for _, g := range garbage {
if strings.HasSuffix(hostName, g) {
hostName = strings.Replace(hostName, g, "", 1)
}
2019-03-13 04:14:30 -07:00
}
return hostName
}
func returnOrBuildCachePath(path string) string {
// validate root path
if _, err := os.Stat(path); err != nil {
return ""
}
// validate oh-my-posh folder, if non existent, create it
2022-02-28 03:55:16 -08:00
cachePath := filepath.Join(path, "oh-my-posh")
if _, err := os.Stat(cachePath); err == nil {
return cachePath
}
if err := os.Mkdir(cachePath, 0755); err != nil {
return ""
}
return cachePath
}