oh-my-posh/src/environment.go

618 lines
15 KiB
Go
Raw Normal View History

2019-03-13 04:14:30 -07:00
package main
import (
2021-12-19 07:42:39 -08:00
"bytes"
"context"
"errors"
"fmt"
2020-10-07 04:32:42 -07:00
"io/ioutil"
"log"
"net/http"
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"
2021-04-24 12:31:56 -07:00
darwinPlatform = "darwin"
linuxPlatform = "linux"
)
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
}
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
PathSeperator() string
HasFiles(pattern string) bool
HasFilesInDir(dir, pattern string) bool
HasFolder(folder string) bool
HasParentFilePath(path string) (fileInfo *FileInfo, err error)
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
Args() *Args
BatteryInfo() ([]*battery.Battery, error)
WindowTitle(imageName, 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"
)
2021-11-16 22:16:43 -08:00
type environment struct {
args *Args
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
}
func (env *environment) init(args *Args) {
2021-11-16 22:16:43 -08:00
env.args = args
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(),
}
2021-11-16 22:16:43 -08:00
if env.args != nil && *env.args.Debug {
env.debug = true
log.SetOutput(&env.logBuilder)
}
}
func (env *environment) resolveConfigPath() {
if env.args == nil || env.args.Config == nil || len(*env.args.Config) == 0 {
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
}
configFile := *env.args.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
}
}
*env.args.Config = filepath.Clean(configFile)
}
2021-11-16 22:16:43 -08:00
func (env *environment) trace(start time.Time, function string, args ...string) {
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)
}
2021-11-16 22:16:43 -08:00
func (env *environment) log(lt logType, function, message string) {
if !env.debug {
return
}
trace := fmt.Sprintf("%s: %s\n%s", lt, function, message)
log.Println(trace)
}
func (env *environment) 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 *environment) 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 := getCompiledRegex(`^[a-z]:`)
return driveLetter.ReplaceAllStringFunc(pwd, strings.ToUpper)
}
2020-10-10 10:16:58 -07:00
if env.args != nil && *env.args.PWD != "" {
2020-10-12 00:04:37 -07:00
env.cwd = correctPath(*env.args.PWD)
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 *environment) HasFiles(pattern string) bool {
defer env.trace(time.Now(), "HasFiles", pattern)
cwd := env.Pwd()
pattern = cwd + env.PathSeperator() + 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 *environment) HasFilesInDir(dir, pattern string) bool {
defer env.trace(time.Now(), "HasFilesInDir", pattern)
pattern = dir + env.PathSeperator() + pattern
matches, err := filepath.Glob(pattern)
if err != nil {
env.log(Error, "HasFilesInDir", err.Error())
return false
}
return len(matches) > 0
}
func (env *environment) 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 *environment) 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 *environment) 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
}
func (env *environment) PathSeperator() string {
defer env.trace(time.Now(), "PathSeperator")
2019-03-13 04:14:30 -07:00
return string(os.PathSeparator)
}
func (env *environment) 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 *environment) 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 *environment) GOOS() string {
defer env.trace(time.Now(), "GOOS")
2019-03-13 04:14:30 -07:00
return runtime.GOOS
}
func (env *environment) 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
}
2021-12-19 07:42:39 -08:00
output := strings.TrimSuffix(out.String(), "\n")
env.log(Debug, "RunCommand", output)
return output, nil
2019-03-13 04:14:30 -07:00
}
func (env *environment) RunShellCommand(shell, command string) string {
defer env.trace(time.Now(), "RunShellCommand", shell, command)
out, _ := env.RunCommand(shell, "-c", command)
return out
2019-03-13 04:14:30 -07:00
}
func (env *environment) 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
}
env.log(Error, "HasCommand", err.Error())
return false
2019-03-13 04:14:30 -07:00
}
func (env *environment) ErrorCode() int {
defer env.trace(time.Now(), "ErrorCode")
2019-03-13 04:14:30 -07:00
return *env.args.ErrorCode
}
func (env *environment) ExecutionTime() float64 {
defer env.trace(time.Now(), "ExecutionTime")
if *env.args.ExecutionTime < 0 {
return 0
}
2020-12-06 13:03:40 -08:00
return *env.args.ExecutionTime
}
func (env *environment) Args() *Args {
defer env.trace(time.Now(), "Args")
2019-03-13 04:14:30 -07:00
return env.args
}
func (env *environment) 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 *environment) Shell() string {
defer env.trace(time.Now(), "Shell")
2020-12-27 05:59:40 -08:00
if *env.args.Shell != "" {
return *env.args.Shell
}
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.
*env.args.Shell = strings.Trim(strings.Replace(name, ".exe", "", 1), " ")
return *env.args.Shell
2020-09-15 04:44:53 -07:00
}
2022-01-07 10:41:58 -08:00
func (env *environment) HTTPRequest(url string, timeout int, requestModifiers ...HTTPRequestModifier) ([]byte, error) {
defer env.trace(time.Now(), "HTTPRequest", url)
ctx, cncl := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
defer cncl()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, 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
}
return body, nil
}
func (env *environment) 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 *environment) StackCount() int {
defer env.trace(time.Now(), "StackCount")
if *env.args.StackCount < 0 {
return 0
}
return *env.args.StackCount
}
func (env *environment) Cache() Cache {
2021-09-21 11:22:59 -07:00
return env.fileCache
}
func (env *environment) Close() {
env.fileCache.Close()
}
func (env *environment) Logs() string {
2021-11-16 22:16:43 -08:00
return env.logBuilder.String()
2021-09-21 11:22:59 -07:00
}
func (env *environment) 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)
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)
}
pwd := env.Pwd()
pwd = strings.Replace(pwd, env.Home(), "~", 1)
2022-01-18 00:48:47 -08:00
tmplCache.PWD = pwd
tmplCache.Folder = base(pwd, env)
tmplCache.UserName = env.User()
if host, err := env.Host(); err == nil {
2022-01-18 00:48:47 -08:00
tmplCache.HostName = host
}
goos := env.GOOS()
2022-01-18 00:48:47 -08:00
tmplCache.OS = goos
env.tmplCache = tmplCache
return env.tmplCache
}
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
cachePath := path + "/oh-my-posh"
if _, err := os.Stat(cachePath); err == nil {
return cachePath
}
if err := os.Mkdir(cachePath, 0755); err != nil {
return ""
}
return cachePath
}