oh-my-posh/src/environment.go

498 lines
12 KiB
Go
Raw Normal View History

2019-03-13 04:14:30 -07:00
package main
import (
"context"
"errors"
"fmt"
2021-08-03 23:46:59 -07:00
"io"
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"
"sync"
"time"
2019-03-13 04:14:30 -07:00
"github.com/distatus/battery"
2020-10-23 07:36:40 -07:00
"github.com/shirou/gopsutil/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
}
2021-09-21 11:22:59 -07:00
type cache interface {
init(home string)
close()
get(key string) (string, bool)
set(key, value string)
}
2019-03-13 04:14:30 -07:00
type environmentInfo interface {
getenv(key string) string
getcwd() string
homeDir() string
2020-10-01 11:57:02 -07:00
hasFiles(pattern string) bool
hasFilesInDir(dir, pattern string) bool
2020-10-07 04:32:42 -07:00
hasFolder(folder string) bool
getFileContent(file string) string
getFoldersList(path string) []string
2019-03-13 04:14:30 -07:00
getPathSeperator() string
getCurrentUser() string
2019-03-13 04:14:30 -07:00
isRunningAsRoot() bool
getHostName() (string, error)
getRuntimeGOOS() string
2020-10-21 19:49:14 -07:00
getPlatform() string
hasCommand(command string) bool
runCommand(command string, args ...string) (string, error)
runShellCommand(shell, command string) string
2019-03-13 04:14:30 -07:00
lastErrorCode() int
2020-12-06 13:03:40 -08:00
executionTime() float64
2019-03-13 04:14:30 -07:00
getArgs() *args
getBatteryInfo() ([]*battery.Battery, error)
2020-09-24 10:11:56 -07:00
getShellName() string
getWindowTitle(imageName, windowTitleRegex string) (string, error)
doGet(url string, timeout int) ([]byte, error)
hasParentFilePath(path string) (fileInfo *fileInfo, err error)
isWsl() bool
stackCount() int
getTerminalWidth() (int, error)
2021-09-21 11:22:59 -07:00
cache() cache
close()
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) {
return c.commands.get(command)
}
type tracer struct {
file *os.File
debug bool
}
func (t *tracer) init(home string) {
if !t.debug {
return
}
var err error
fileName := home + "/oh-my-posh.log"
t.file, err = os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("error opening file: %v", err)
}
log.SetOutput(t.file)
2021-08-03 23:46:59 -07:00
log.Println("#### start oh-my-posh run ####")
}
func (t *tracer) close() {
if !t.debug {
return
}
2021-08-03 23:46:59 -07:00
log.Println("#### end oh-my-posh run ####")
_ = t.file.Close()
}
func (t *tracer) trace(start time.Time, function string, args ...string) {
if !t.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)
}
2019-03-13 04:14:30 -07:00
type environment struct {
2021-09-21 11:22:59 -07:00
args *args
cwd string
cmdCache *commandCache
fileCache *fileCache
tracer *tracer
}
func (env *environment) init(args *args) {
env.args = args
env.cmdCache = &commandCache{
commands: newConcurrentMap(),
}
2021-09-21 11:22:59 -07:00
env.fileCache = &fileCache{}
env.fileCache.init(env.homeDir())
tracer := &tracer{
debug: *args.Debug,
}
tracer.init(env.homeDir())
env.tracer = tracer
}
2021-01-10 02:52:59 -08:00
2019-03-13 04:14:30 -07:00
func (env *environment) getenv(key string) string {
defer env.tracer.trace(time.Now(), "getenv", key)
2019-03-13 04:14:30 -07:00
return os.Getenv(key)
}
func (env *environment) getcwd() string {
defer env.tracer.trace(time.Now(), "getcwd")
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
return strings.Replace(pwd, "c:", "C:", 1)
}
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 {
return ""
}
2020-10-12 00:04:37 -07:00
env.cwd = correctPath(dir)
return env.cwd
}
2020-10-01 11:57:02 -07:00
func (env *environment) hasFiles(pattern string) bool {
defer env.tracer.trace(time.Now(), "hasFiles", pattern)
cwd := env.getcwd()
2020-10-01 11:57:02 -07:00
pattern = cwd + env.getPathSeperator() + pattern
matches, err := filepath.Glob(pattern)
if err != nil {
return false
}
return len(matches) > 0
}
func (env *environment) hasFilesInDir(dir, pattern string) bool {
defer env.tracer.trace(time.Now(), "hasFilesInDir", pattern)
pattern = dir + env.getPathSeperator() + pattern
matches, err := filepath.Glob(pattern)
if err != nil {
return false
}
return len(matches) > 0
}
2020-10-07 04:32:42 -07:00
func (env *environment) hasFolder(folder string) bool {
defer env.tracer.trace(time.Now(), "hasFolder", folder)
2020-10-07 04:32:42 -07:00
_, err := os.Stat(folder)
return !os.IsNotExist(err)
}
func (env *environment) getFileContent(file string) string {
defer env.tracer.trace(time.Now(), "getFileContent", file)
2020-10-07 04:32:42 -07:00
content, err := ioutil.ReadFile(file)
if err != nil {
return ""
}
return string(content)
}
func (env *environment) getFoldersList(path string) []string {
defer env.tracer.trace(time.Now(), "getFoldersList", path)
content, err := os.ReadDir(path)
if err != nil {
return nil
}
var folderNames []string
for _, s := range content {
if s.IsDir() {
folderNames = append(folderNames, s.Name())
}
}
return folderNames
}
2019-03-13 04:14:30 -07:00
func (env *environment) getPathSeperator() string {
defer env.tracer.trace(time.Now(), "getPathSeperator")
2019-03-13 04:14:30 -07:00
return string(os.PathSeparator)
}
func (env *environment) getCurrentUser() string {
defer env.tracer.trace(time.Now(), "getCurrentUser")
user := os.Getenv("USER")
if user == "" {
user = os.Getenv("USERNAME")
}
return user
2019-03-13 04:14:30 -07:00
}
func (env *environment) getHostName() (string, error) {
defer env.tracer.trace(time.Now(), "getHostName")
2019-03-13 04:14:30 -07:00
hostName, err := os.Hostname()
if err != nil {
return "", err
}
return cleanHostName(hostName), nil
}
func (env *environment) getRuntimeGOOS() string {
defer env.tracer.trace(time.Now(), "getRuntimeGOOS")
2019-03-13 04:14:30 -07:00
return runtime.GOOS
}
func (env *environment) runCommand(command string, args ...string) (string, error) {
defer env.tracer.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
copyAndCapture := func(r io.Reader) ([]byte, error) {
var out []byte
buf := make([]byte, 1024)
for {
n, err := r.Read(buf)
if n > 0 {
d := buf[:n]
out = append(out, d...)
}
if err == nil {
continue
}
// Read returns io.EOF at the end of file, which is not an error for us
if err == io.EOF {
err = nil
}
return out, err
}
}
normalizeOutput := func(out []byte) string {
return strings.TrimSuffix(string(out), "\n")
}
cmd := exec.Command(command, args...)
var stdout, stderr []byte
var stdoutErr, stderrErr error
stdoutIn, _ := cmd.StdoutPipe()
stderrIn, _ := cmd.StderrPipe()
err := cmd.Start()
if err != nil {
errorStr := fmt.Sprintf("cmd.Start() failed with '%s'", err)
return "", errors.New(errorStr)
}
// cmd.Wait() should be called only after we finish reading
// from stdoutIn and stderrIn.
// wg ensures that we finish
var wg sync.WaitGroup
wg.Add(1)
go func() {
stdout, stdoutErr = copyAndCapture(stdoutIn)
wg.Done()
}()
stderr, stderrErr = copyAndCapture(stderrIn)
wg.Wait()
err = cmd.Wait()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return "", &commandError{
err: exitErr.Error(),
exitCode: exitErr.ExitCode(),
}
}
2020-11-19 00:02:51 -08:00
}
2021-08-03 23:46:59 -07:00
if stdoutErr != nil || stderrErr != nil {
return "", errors.New("failed to capture stdout or stderr")
}
stderrStr := normalizeOutput(stderr)
if len(stderrStr) > 0 {
return stderrStr, nil
}
return normalizeOutput(stdout), nil
2019-03-13 04:14:30 -07:00
}
func (env *environment) runShellCommand(shell, command string) string {
defer env.tracer.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.tracer.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
}
return false
2019-03-13 04:14:30 -07:00
}
func (env *environment) lastErrorCode() int {
defer env.tracer.trace(time.Now(), "lastErrorCode")
2019-03-13 04:14:30 -07:00
return *env.args.ErrorCode
}
2020-12-06 13:03:40 -08:00
func (env *environment) executionTime() float64 {
defer env.tracer.trace(time.Now(), "executionTime")
if *env.args.ExecutionTime < 0 {
return 0
}
2020-12-06 13:03:40 -08:00
return *env.args.ExecutionTime
}
2019-03-13 04:14:30 -07:00
func (env *environment) getArgs() *args {
defer env.tracer.trace(time.Now(), "getArgs")
2019-03-13 04:14:30 -07:00
return env.args
}
func (env *environment) getBatteryInfo() ([]*battery.Battery, error) {
defer env.tracer.trace(time.Now(), "getBatteryInfo")
batteries, err := battery.GetAll()
// actual error, return it
if err != nil && len(batteries) == 0 {
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)
}
}
unableToRetrieveBatteryInfo := "A device which does not exist was specified."
// when battery info fails to get retrieved but there is at least one valid battery, return it without error
if len(validBatteries) > 0 && err != nil && strings.Contains(err.Error(), unableToRetrieveBatteryInfo) {
return validBatteries, nil
}
// another error occurred (possibly unmapped use-case), return it
if err != nil {
return nil, err
}
// everything is fine
return validBatteries, nil
2019-03-13 04:14:30 -07:00
}
2020-09-24 10:11:56 -07:00
func (env *environment) getShellName() string {
defer env.tracer.trace(time.Now(), "getShellName")
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 {
return unknown
2020-09-24 10:11:56 -07:00
}
if name == "cmd.exe" {
p, _ = p.Parent()
name, err = p.Name()
}
if err != nil {
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
}
func (env *environment) doGet(url string, timeout int) ([]byte, error) {
defer env.tracer.trace(time.Now(), "doGet", 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
}
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
return body, nil
}
func (env *environment) hasParentFilePath(path string) (*fileInfo, error) {
defer env.tracer.trace(time.Now(), "hasParentFilePath", path)
currentFolder := env.getcwd()
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
}
return nil, errors.New("no match at root level")
}
}
func (env *environment) stackCount() int {
defer env.tracer.trace(time.Now(), "stackCount")
if *env.args.StackCount < 0 {
return 0
}
return *env.args.StackCount
}
2021-09-21 11:22:59 -07:00
func (env *environment) cache() cache {
return env.fileCache
}
func (env *environment) close() {
env.fileCache.close()
env.tracer.close()
}
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
}