Repository: JanDeDobbeleer/oh-my-posh
Pull request #3108 (2022-11-21 14:51:59)

logs
### Prerequisites

- [x] I have read and understood the [contributing guide][CONTRIBUTING.md]
- [x] The commit message follows the [conventional commits][cc] guidelines
- [x] Tests for the changes have been added (for bug fixes/features)
- [x] Docs have been added/updated (for bug fixes/features)

### Description

- refactor(debug): extract log from platform
- fix(windows): add additional access types and logs
- chore: remove prefix and postfix

<!---

Tips:

If you're not comfortable with working with Git, we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/src/http/request.go b/src/http/request.go
index 9831d738..e68c6329 100644
--- a/src/http/request.go
+++ b/src/http/request.go
@@ -34,13 +34,13 @@ func getCacheValue[a any](r *Request, key string) (a, error) {
 	if val, found := r.env.Cache().Get(key); found {
 		err := json.Unmarshal([]byte(val), &data)
 		if err != nil {
-			r.env.Log(platform.Error, "OAuth", err.Error())
+			r.env.Error("OAuth", err)
 			return data, err
 		}
 		return data, nil
 	}
 	err := errors.New("no data in cache")
-	r.env.Log(platform.Error, "OAuth", err.Error())
+	r.env.Error("OAuth", err)
 	return data, err
 }
 
@@ -50,13 +50,13 @@ func do[a any](r *Request, url string, body io.Reader, requestModifiers ...platf
 
 	responseBody, err := r.env.HTTPRequest(url, body, httpTimeout, requestModifiers...)
 	if err != nil {
-		r.env.Log(platform.Error, "OAuth", err.Error())
+		r.env.Error("OAuth", err)
 		return data, err
 	}
 
 	err = json.Unmarshal(responseBody, &data)
 	if err != nil {
-		r.env.Log(platform.Error, "OAuth", err.Error())
+		r.env.Error("OAuth", err)
 		return data, err
 	}
 
diff --git a/src/log/entry.go b/src/log/entry.go
new file mode 100644
index 00000000..7514bab6
--- /dev/null
+++ b/src/log/entry.go
@@ -0,0 +1,24 @@
+package log
+
+type format byte
+
+const (
+	Red format = 1 << iota
+	Yellow
+)
+
+type entry string
+
+func (e *entry) Format(formats ...format) {
+	str := *e
+	for _, format := range formats {
+		switch format {
+		case Red:
+			str = "\033[31m" + str
+		case Yellow:
+			str = "\033[33m" + str
+		}
+		str += "\033[0m"
+	}
+	*e = str
+}
diff --git a/src/log/log.go b/src/log/log.go
new file mode 100644
index 00000000..f06ef154
--- /dev/null
+++ b/src/log/log.go
@@ -0,0 +1,67 @@
+package log
+
+import (
+	"fmt"
+	"log"
+	"strings"
+	"time"
+)
+
+var enabled bool
+var logBuilder strings.Builder
+
+func Enable() {
+	enabled = true
+	log.SetOutput(&logBuilder)
+}
+
+func Info(message string) {
+	if !enabled {
+		return
+	}
+	log.Println(message)
+}
+
+func Trace(start time.Time, function string, args ...string) {
+	if !enabled {
+		return
+	}
+	elapsed := time.Since(start)
+	var argString string
+	if len(args) > 0 {
+		argString = fmt.Sprintf(", args: %s", strings.Join(args, " "))
+	}
+	trace := entry(fmt.Sprintf("%s: %s%s", function, elapsed, argString))
+	log.Println(trace)
+}
+
+func Debug(funcName, message string) {
+	if !enabled {
+		return
+	}
+	trace := entry(fmt.Sprintf("%s\n%s", funcName, message))
+	trace.Format(Yellow)
+	log.Println(trace)
+}
+
+func DebugF(function string, fn func() string) {
+	if !enabled {
+		return
+	}
+	trace := entry(fmt.Sprintf("%s\n%s", function, fn()))
+	trace.Format(Yellow)
+	log.Println(trace)
+}
+
+func Error(funcName string, err error) {
+	if !enabled {
+		return
+	}
+	trace := entry(fmt.Sprintf("%s\n%s", funcName, err.Error()))
+	trace.Format(Red)
+	log.Println(trace)
+}
+
+func String() string {
+	return logBuilder.String()
+}
diff --git a/src/mock/environment.go b/src/mock/environment.go
index ab987986..10291e49 100644
--- a/src/mock/environment.go
+++ b/src/mock/environment.go
@@ -246,8 +246,12 @@ func (env *MockedEnvironment) Trace(start time.Time, function string, args ...st
 	_ = env.Called(start, function, args)
 }
 
-func (env *MockedEnvironment) Log(logType platform.LogType, funcName, message string) {
-	_ = env.Called(logType, funcName, message)
+func (env *MockedEnvironment) Debug(funcName, message string) {
+	_ = env.Called(funcName, message)
+}
+
+func (env *MockedEnvironment) Error(funcName string, err error) {
+	_ = env.Called(funcName, err)
 }
 
 func (env *MockedEnvironment) DirIsWritable(path string) bool {
diff --git a/src/platform/networks_windows.go b/src/platform/networks_windows.go
index cc07ea87..b38041c5 100644
--- a/src/platform/networks_windows.go
+++ b/src/platform/networks_windows.go
@@ -244,7 +244,7 @@ func (env *Shell) parseNetworkInterface(network *WLAN_INTERFACE_INFO, clientHand
 		uintptr(unsafe.Pointer(&wlanAttr)),
 		uintptr(unsafe.Pointer(nil)))
 	if e != 0 {
-		env.Log(Error, "parseNetworkInterface", "wlan_intf_opcode_current_connection error")
+		env.Error("parseNetworkInterface", err)
 		return &info, err
 	}
 
diff --git a/src/platform/shell.go b/src/platform/shell.go
index 4b2ac1c4..afb480ab 100644
--- a/src/platform/shell.go
+++ b/src/platform/shell.go
@@ -8,9 +8,9 @@ import (
 	"fmt"
 	"io"
 	"io/fs"
-	"log"
 	"net/http"
 	"net/http/httputil"
+	"oh-my-posh/log"
 	"oh-my-posh/platform/battery"
 	"oh-my-posh/platform/cmd"
 	"oh-my-posh/regex"
@@ -208,7 +208,8 @@ type Environment interface {
 	Connection(connectionType ConnectionType) (*Connection, error)
 	TemplateCache() *TemplateCache
 	LoadTemplateCache()
-	Log(logType LogType, funcName, message string)
+	Debug(funcName, message string)
+	Error(funcName string, err error)
 	Trace(start time.Time, function string, args ...string)
 }
 
@@ -229,23 +230,15 @@ func (c *commandCache) get(command string) (string, bool) {
 	return command, ok
 }
 
-type LogType string
-
-const (
-	Error LogType = "error"
-	Debug LogType = "debug"
-)
-
 type Shell struct {
 	CmdFlags *Flags
 	Version  string
 
-	cwd        string
-	cmdCache   *commandCache
-	fileCache  *fileCache
-	tmplCache  *TemplateCache
-	logBuilder strings.Builder
-	networks   []*Connection
+	cwd       string
+	cmdCache  *commandCache
+	fileCache *fileCache
+	tmplCache *TemplateCache
+	networks  []*Connection
 }
 
 func (env *Shell) Init() {
@@ -254,7 +247,7 @@ func (env *Shell) Init() {
 		env.CmdFlags = &Flags{}
 	}
 	if env.CmdFlags.Debug {
-		log.SetOutput(&env.logBuilder)
+		log.Enable()
 	}
 	env.fileCache = &fileCache{}
 	env.fileCache.Init(env.CachePath())
@@ -316,34 +309,25 @@ func (env *Shell) downloadConfig(location string) error {
 }
 
 func (env *Shell) Trace(start time.Time, function string, args ...string) {
-	if env.CmdFlags == nil || !env.CmdFlags.Debug {
-		return
-	}
-	elapsed := time.Since(start)
-	trace := fmt.Sprintf("%s duration: %s, args: %s", function, elapsed, strings.Trim(fmt.Sprint(args), "[]"))
-	log.Println(trace)
+	log.Trace(start, function, args...)
 }
 
-func (env *Shell) Log(logType LogType, funcName, message string) {
-	if !env.CmdFlags.Debug {
-		return
-	}
-	trace := fmt.Sprintf("%s: %s\n%s", logType, funcName, message)
-	log.Println(trace)
+func (env *Shell) Debug(funcName, message string) {
+	log.Debug(funcName, message)
+}
+
+func (env *Shell) Error(funcName string, err error) {
+	log.Error(funcName, err)
 }
 
 func (env *Shell) debugF(function string, fn func() string) {
-	if !env.CmdFlags.Debug {
-		return
-	}
-	trace := fmt.Sprintf("%s: %s\n%s", Debug, function, fn())
-	log.Println(trace)
+	log.DebugF(function, fn)
 }
 
 func (env *Shell) Getenv(key string) string {
 	defer env.Trace(time.Now(), "Getenv", key)
 	val := os.Getenv(key)
-	env.Log(Debug, "Getenv", val)
+	env.Debug("Getenv", val)
 	return val
 }
 
@@ -352,7 +336,7 @@ func (env *Shell) Pwd() string {
 	lock.Lock()
 	defer func() {
 		lock.Unlock()
-		env.Log(Debug, "Pwd", env.cwd)
+		env.Debug("Pwd", env.cwd)
 	}()
 	if env.cwd != "" {
 		return env.cwd
@@ -371,7 +355,7 @@ func (env *Shell) Pwd() string {
 	}
 	dir, err := os.Getwd()
 	if err != nil {
-		env.Log(Error, "Pwd", err.Error())
+		env.Error("Pwd", err)
 		return ""
 	}
 	env.cwd = correctPath(dir)
@@ -384,7 +368,7 @@ func (env *Shell) HasFiles(pattern string) bool {
 	fileSystem := os.DirFS(cwd)
 	matches, err := fs.Glob(fileSystem, pattern)
 	if err != nil {
-		env.Log(Error, "HasFiles", err.Error())
+		env.Error("HasFiles", err)
 		return false
 	}
 	for _, match := range matches {
@@ -402,7 +386,7 @@ func (env *Shell) HasFilesInDir(dir, pattern string) bool {
 	fileSystem := os.DirFS(dir)
 	matches, err := fs.Glob(fileSystem, pattern)
 	if err != nil {
-		env.Log(Error, "HasFilesInDir", err.Error())
+		env.Error("HasFilesInDir", err)
 		return false
 	}
 	hasFilesInDir := len(matches) > 0
@@ -416,18 +400,18 @@ func (env *Shell) HasFileInParentDirs(pattern string, depth uint) bool {
 
 	for c := 0; c < int(depth); c++ {
 		if env.HasFilesInDir(currentFolder, pattern) {
-			env.Log(Debug, "HasFileInParentDirs", "true")
+			env.Debug("HasFileInParentDirs", "true")
 			return true
 		}
 
 		if dir := filepath.Dir(currentFolder); dir != currentFolder {
 			currentFolder = dir
 		} else {
-			env.Log(Debug, "HasFileInParentDirs", "false")
+			env.Debug("HasFileInParentDirs", "false")
 			return false
 		}
 	}
-	env.Log(Debug, "HasFileInParentDirs", "false")
+	env.Debug("HasFileInParentDirs", "false")
 	return false
 }
 
@@ -435,7 +419,7 @@ func (env *Shell) HasFolder(folder string) bool {
 	defer env.Trace(time.Now(), "HasFolder", folder)
 	f, err := os.Stat(folder)
 	if err != nil {
-		env.Log(Debug, "HasFolder", "false")
+		env.Debug("HasFolder", "false")
 		return false
 	}
 	env.debugF("HasFolder", func() string { return strconv.FormatBool(f.IsDir()) })
@@ -446,10 +430,10 @@ func (env *Shell) ResolveSymlink(path string) (string, error) {
 	defer env.Trace(time.Now(), "ResolveSymlink", path)
 	link, err := filepath.EvalSymlinks(path)
 	if err != nil {
-		env.Log(Error, "ResolveSymlink", err.Error())
+		env.Error("ResolveSymlink", err)
 		return "", err
 	}
-	env.Log(Debug, "ResolveSymlink", link)
+	env.Debug("ResolveSymlink", link)
 	return link, nil
 }
 
@@ -460,11 +444,11 @@ func (env *Shell) FileContent(file string) string {
 	}
 	content, err := os.ReadFile(file)
 	if err != nil {
-		env.Log(Error, "FileContent", err.Error())
+		env.Error("FileContent", err)
 		return ""
 	}
 	fileContent := string(content)
-	env.Log(Debug, "FileContent", fileContent)
+	env.Debug("FileContent", fileContent)
 	return fileContent
 }
 
@@ -472,7 +456,7 @@ func (env *Shell) LsDir(path string) []fs.DirEntry {
 	defer env.Trace(time.Now(), "LsDir", path)
 	entries, err := os.ReadDir(path)
 	if err != nil {
-		env.Log(Error, "LsDir", err.Error())
+		env.Error("LsDir", err)
 		return nil
 	}
 	env.debugF("LsDir", func() string {
@@ -496,7 +480,7 @@ func (env *Shell) User() string {
 	if user == "" {
 		user = os.Getenv("USERNAME")
 	}
-	env.Log(Debug, "User", user)
+	env.Debug("User", user)
 	return user
 }
 
@@ -504,11 +488,11 @@ func (env *Shell) Host() (string, error) {
 	defer env.Trace(time.Now(), "Host")
 	hostName, err := os.Hostname()
 	if err != nil {
-		env.Log(Error, "Host", err.Error())
+		env.Error("Host", err)
 		return "", err
 	}
 	hostName = cleanHostName(hostName)
-	env.Log(Debug, "Host", hostName)
+	env.Debug("Host", hostName)
 	return hostName, nil
 }
 
@@ -524,9 +508,9 @@ func (env *Shell) RunCommand(command string, args ...string) (string, error) {
 	}
 	output, err := cmd.Run(command, args...)
 	if err != nil {
-		env.Log(Error, "RunCommand", "cmd.Run() failed")
+		env.Error("RunCommand", err)
 	}
-	env.Log(Debug, "RunCommand", output)
+	env.Debug("RunCommand", output)
 	return output, err
 }
 
@@ -541,22 +525,22 @@ func (env *Shell) RunShellCommand(shell, command string) string {
 func (env *Shell) CommandPath(command string) string {
 	defer env.Trace(time.Now(), "CommandPath", command)
 	if path, ok := env.cmdCache.get(command); ok {
-		env.Log(Debug, "CommandPath", path)
+		env.Debug("CommandPath", path)
 		return path
 	}
 	path, err := exec.LookPath(command)
 	if err == nil {
 		env.cmdCache.set(command, path)
-		env.Log(Debug, "CommandPath", path)
+		env.Debug("CommandPath", path)
 		return path
 	}
 	path, err = env.LookWinAppPath(command)
 	if err == nil {
 		env.cmdCache.set(command, path)
-		env.Log(Debug, "CommandPath", path)
+		env.Debug("CommandPath", path)
 		return path
 	}
-	env.Log(Error, "CommandPath", err.Error())
+	env.Error("CommandPath", err)
 	return ""
 }
 
@@ -595,20 +579,20 @@ func (env *Shell) Shell() string {
 	p, _ := process.NewProcess(int32(pid))
 	name, err := p.Name()
 	if err != nil {
-		env.Log(Error, "Shell", err.Error())
+		env.Error("Shell", err)
 		return UNKNOWN
 	}
-	env.Log(Debug, "Shell", "process name: "+name)
+	env.Debug("Shell", "process name: "+name)
 	// this is used for when scoop creates a shim, see
 	// https://github.com/JanDeDobbeleer/oh-my-posh/issues/2806
 	executable, _ := os.Executable()
 	if name == "cmd.exe" || name == executable {
 		p, _ = p.Parent()
 		name, err = p.Name()
-		env.Log(Debug, "Shell", "parent process name: "+name)
+		env.Debug("Shell", "parent process name: "+name)
 	}
 	if err != nil {
-		env.Log(Error, "Shell", err.Error())
+		env.Error("Shell", err)
 		return UNKNOWN
 	}
 	// Cache the shell value to speed things up.
@@ -642,27 +626,27 @@ func (env *Shell) HTTPRequest(targetURL string, body io.Reader, timeout int, req
 	}
 	if env.CmdFlags.Debug {
 		dump, _ := httputil.DumpRequestOut(request, true)
-		env.Log(Debug, "HTTPRequest", string(dump))
+		env.Debug("HTTPRequest", string(dump))
 	}
 	response, err := client.Do(request)
 	if err != nil {
-		env.Log(Error, "HTTPRequest", err.Error())
+		env.Error("HTTPRequest", err)
 		return nil, env.unWrapError(err)
 	}
 	// anything inside the range [200, 299] is considered a success
 	if response.StatusCode < 200 || response.StatusCode >= 300 {
 		message := "HTTP status code " + strconv.Itoa(response.StatusCode)
 		err := errors.New(message)
-		env.Log(Error, "HTTPRequest", message)
+		env.Error("HTTPRequest", err)
 		return nil, err
 	}
 	defer response.Body.Close()
 	responseBody, err := io.ReadAll(response.Body)
 	if err != nil {
-		env.Log(Error, "HTTPRequest", err.Error())
+		env.Error("HTTPRequest", err)
 		return nil, err
 	}
-	env.Log(Debug, "HTTPRequest", string(responseBody))
+	env.Debug("HTTPRequest", string(responseBody))
 	return responseBody, nil
 }
 
@@ -686,7 +670,7 @@ func (env *Shell) HasParentFilePath(path string) (*FileInfo, error) {
 			currentFolder = dir
 			continue
 		}
-		env.Log(Error, "HasParentFilePath", err.Error())
+		env.Error("HasParentFilePath", err)
 		return nil, errors.New("no match at root level")
 	}
 }
@@ -721,14 +705,14 @@ func (env *Shell) LoadTemplateCache() {
 	var templateCache TemplateCache
 	err := json.Unmarshal([]byte(val), &templateCache)
 	if err != nil {
-		env.Log(Error, "LoadTemplateCache", err.Error())
+		env.Error("LoadTemplateCache", err)
 		return
 	}
 	env.tmplCache = &templateCache
 }
 
 func (env *Shell) Logs() string {
-	return env.logBuilder.String()
+	return log.String()
 }
 
 func (env *Shell) TemplateCache() *TemplateCache {
@@ -782,8 +766,7 @@ func (env *Shell) DirMatchesOneOf(dir string, regexes []string) (match bool) {
 	// for the time being until we figure out what the actual root cause is
 	defer func() {
 		if err := recover(); err != nil {
-			message := fmt.Sprintf("%s", err)
-			env.Log(Error, "DirMatchesOneOf", message)
+			env.Error("DirMatchesOneOf", errors.New("panic"))
 			match = false
 		}
 	}()
diff --git a/src/platform/shell_darwin.go b/src/platform/shell_darwin.go
index 7f14e2a5..830c51b4 100644
--- a/src/platform/shell_darwin.go
+++ b/src/platform/shell_darwin.go
@@ -32,14 +32,14 @@ func mapMostLogicalState(state string) battery.State {
 func (env *Shell) parseBatteryOutput(output string) (*battery.Info, error) {
 	matches := regex.FindNamedRegexMatch(`(?P<PERCENTAGE>[0-9]{1,3})%; (?P<STATE>[a-zA-Z\s]+);`, output)
 	if len(matches) != 2 {
-		msg := "Unable to find battery state based on output"
-		env.Log(Error, "BatteryState", msg)
-		return nil, errors.New(msg)
+		err := errors.New("Unable to find battery state based on output")
+		env.Error("BatteryState", err)
+		return nil, err
 	}
 	var percentage int
 	var err error
 	if percentage, err = strconv.Atoi(matches["PERCENTAGE"]); err != nil {
-		env.Log(Error, "BatteryState", err.Error())
+		env.Error("BatteryState", err)
 		return nil, errors.New("Unable to parse battery percentage")
 	}
 	return &battery.Info{
@@ -52,7 +52,7 @@ func (env *Shell) BatteryState() (*battery.Info, error) {
 	defer env.Trace(time.Now(), "BatteryState")
 	output, err := env.RunCommand("pmset", "-g", "batt")
 	if err != nil {
-		env.Log(Error, "BatteryState", err.Error())
+		env.Error("BatteryState", err)
 		return nil, err
 	}
 	if !strings.Contains(output, "Battery") {
diff --git a/src/platform/shell_unix.go b/src/platform/shell_unix.go
index b8fe655c..632fc193 100644
--- a/src/platform/shell_unix.go
+++ b/src/platform/shell_unix.go
@@ -51,7 +51,7 @@ func (env *Shell) TerminalWidth() (int, error) {
 	}
 	width, err := terminal.Width()
 	if err != nil {
-		env.Log(Error, "TerminalWidth", err.Error())
+		env.Error("TerminalWidth", err)
 	}
 	return int(width), err
 }
diff --git a/src/platform/shell_windows.go b/src/platform/shell_windows.go
index fc1f2fb7..4a5b63ef 100644
--- a/src/platform/shell_windows.go
+++ b/src/platform/shell_windows.go
@@ -30,7 +30,7 @@ func (env *Shell) Root() bool {
 		0, 0, 0, 0, 0, 0,
 		&sid)
 	if err != nil {
-		env.Log(Error, "Root", err.Error())
+		env.Error("Root", err)
 		return false
 	}
 	defer func() {
@@ -44,7 +44,7 @@ func (env *Shell) Root() bool {
 
 	member, err := token.IsMember(sid)
 	if err != nil {
-		env.Log(Error, "Root", err.Error())
+		env.Error("Root", err)
 		return false
 	}
 
@@ -54,7 +54,7 @@ func (env *Shell) Root() bool {
 func (env *Shell) Home() string {
 	home := os.Getenv("HOME")
 	defer func() {
-		env.Log(Debug, "Home", home)
+		env.Debug("Home", home)
 	}()
 	if len(home) > 0 {
 		return home
@@ -71,7 +71,7 @@ func (env *Shell) QueryWindowTitles(processName, windowTitleRegex string) (strin
 	defer env.Trace(time.Now(), "WindowTitle", windowTitleRegex)
 	title, err := queryWindowTitles(processName, windowTitleRegex)
 	if err != nil {
-		env.Log(Error, "QueryWindowTitles", err.Error())
+		env.Error("QueryWindowTitles", err)
 	}
 	return title, err
 }
@@ -93,12 +93,12 @@ func (env *Shell) TerminalWidth() (int, error) {
 	}
 	handle, err := syscall.Open("CONOUT$", syscall.O_RDWR, 0)
 	if err != nil {
-		env.Log(Error, "TerminalWidth", err.Error())
+		env.Error("TerminalWidth", err)
 		return 0, err
 	}
 	info, err := winterm.GetConsoleScreenBufferInfo(uintptr(handle))
 	if err != nil {
-		env.Log(Error, "TerminalWidth", err.Error())
+		env.Error("TerminalWidth", err)
 		return 0, err
 	}
 	// return int(float64(info.Size.X) * 0.57), nil
@@ -157,9 +157,9 @@ func (env *Shell) WindowsRegistryKeyValue(path string) (*WindowsRegistryValue, e
 	//
 	rootKey, regPath, found := strings.Cut(path, `\`)
 	if !found {
-		errorLogMsg := fmt.Sprintf("Error, malformed registry path: '%s'", path)
-		env.Log(Error, "WindowsRegistryKeyValue", errorLogMsg)
-		return nil, errors.New(errorLogMsg)
+		err := fmt.Errorf("Error, malformed registry path: '%s'", path)
+		env.Error("WindowsRegistryKeyValue", err)
+		return nil, err
 	}
 
 	regKey := Base(env, regPath)
@@ -180,19 +180,19 @@ func (env *Shell) WindowsRegistryKeyValue(path string) (*WindowsRegistryValue, e
 	case "HKU", "HKEY_USERS":
 		key = windows.HKEY_USERS
 	default:
-		errorLogMsg := fmt.Sprintf("Error, unknown registry key: '%s'", rootKey)
-		env.Log(Error, "WindowsRegistryKeyValue", errorLogMsg)
-		return nil, errors.New(errorLogMsg)
+		err := fmt.Errorf("Error, unknown registry key: '%s", rootKey)
+		env.Error("WindowsRegistryKeyValue", err)
+		return nil, err
 	}
 
 	k, err := registry.OpenKey(key, regPath, registry.READ)
 	if err != nil {
-		env.Log(Error, "WindowsRegistryKeyValue", err.Error())
+		env.Error("WindowsRegistryKeyValue", err)
 		return nil, err
 	}
 	_, valType, err := k.GetValue(regKey, nil)
 	if err != nil {
-		env.Log(Error, "WindowsRegistryKeyValue", err.Error())
+		env.Error("WindowsRegistryKeyValue", err)
 		return nil, err
 	}
 
@@ -217,7 +217,7 @@ func (env *Shell) WindowsRegistryKeyValue(path string) (*WindowsRegistryValue, e
 		errorLogMsg := fmt.Sprintf("Error, no formatter for type: %d", valType)
 		return nil, errors.New(errorLogMsg)
 	}
-	env.Log(Debug, "WindowsRegistryKeyValue", fmt.Sprintf("%s(%s): %s", regKey, regValue.ValueType, regValue.String))
+	env.Debug("WindowsRegistryKeyValue", fmt.Sprintf("%s(%s): %s", regKey, regValue.ValueType, regValue.String))
 	return regValue, nil
 }
 
@@ -235,7 +235,7 @@ func (env *Shell) ConvertToLinuxPath(path string) string {
 
 func (env *Shell) DirIsWritable(path string) bool {
 	defer env.Trace(time.Now(), "DirIsWritable")
-	return isWriteable(path)
+	return env.isWriteable(path)
 }
 
 func (env *Shell) Connection(connectionType ConnectionType) (*Connection, error) {
@@ -251,6 +251,6 @@ func (env *Shell) Connection(connectionType ConnectionType) (*Connection, error)
 			return network, nil
 		}
 	}
-	env.Log(Error, "network", fmt.Sprintf("Network type '%s' not found", connectionType))
+	env.Error("network", fmt.Errorf("Network type '%s' not found", connectionType))
 	return nil, &NotImplemented{}
 }
diff --git a/src/platform/shell_windows_nix.go b/src/platform/shell_windows_nix.go
index f4a133e5..e84164c0 100644
--- a/src/platform/shell_windows_nix.go
+++ b/src/platform/shell_windows_nix.go
@@ -11,7 +11,7 @@ func (env *Shell) BatteryState() (*battery.Info, error) {
 	defer env.Trace(time.Now(), "BatteryState")
 	info, err := battery.Get()
 	if err != nil {
-		env.Log(Error, "BatteryState", err.Error())
+		env.Error("BatteryState", err)
 		return nil, err
 	}
 	return info, nil
diff --git a/src/platform/win32_windows.go b/src/platform/win32_windows.go
index 6534f6dc..54a257bb 100644
--- a/src/platform/win32_windows.go
+++ b/src/platform/win32_windows.go
@@ -205,11 +205,56 @@ const (
 	ACCESS_DENIED_ACE_TYPE = 1 //nolint: revive
 )
 
+type accessMask uint32
+
+func (m accessMask) canWrite() bool {
+	allowMask := ^(windows.GENERIC_WRITE | windows.GENERIC_ALL | windows.WRITE_DAC | windows.WRITE_OWNER)
+	return m&accessMask(allowMask) != 0
+}
+
+func (m accessMask) permissions() string {
+	var permissions []string
+	if m&windows.GENERIC_READ != 0 {
+		permissions = append(permissions, "GENERIC_READ")
+	}
+	if m&windows.GENERIC_WRITE != 0 {
+		permissions = append(permissions, "GENERIC_WRITE")
+	}
+	if m&windows.GENERIC_EXECUTE != 0 {
+		permissions = append(permissions, "GENERIC_EXECUTE")
+	}
+	if m&windows.GENERIC_ALL != 0 {
+		permissions = append(permissions, "GENERIC_ALL")
+	}
+	if m&windows.WRITE_DAC != 0 {
+		permissions = append(permissions, "WRITE_DAC")
+	}
+	if m&windows.WRITE_OWNER != 0 {
+		permissions = append(permissions, "WRITE_OWNER")
+	}
+	if m&windows.SYNCHRONIZE != 0 {
+		permissions = append(permissions, "SYNCHRONIZE")
+	}
+	if m&windows.DELETE != 0 {
+		permissions = append(permissions, "DELETE")
+	}
+	if m&windows.READ_CONTROL != 0 {
+		permissions = append(permissions, "READ_CONTROL")
+	}
+	if m&windows.ACCESS_SYSTEM_SECURITY != 0 {
+		permissions = append(permissions, "ACCESS_SYSTEM_SECURITY")
+	}
+	if m&windows.MAXIMUM_ALLOWED != 0 {
+		permissions = append(permissions, "MAXIMUM_ALLOWED")
+	}
+	return strings.Join(permissions, "\n")
+}
+
 type AccessAllowedAce struct {
 	AceType    uint8
 	AceFlags   uint8
 	AceSize    uint16
-	AccessMask uint32
+	AccessMask accessMask
 	SidStart   uint32
 }
 
@@ -222,21 +267,24 @@ func getCurrentUser() (sid *windows.SID, err error) {
 	return
 }
 
-func isWriteable(folder string) bool {
+func (env *Shell) isWriteable(folder string) bool {
 	cu, err := getCurrentUser()
 	if err != nil {
 		// unable to get current user
+		env.Error("isWriteable", err)
 		return false
 	}
 
 	si, err := windows.GetNamedSecurityInfo(folder, windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION)
 	if err != nil {
+		env.Error("isWriteable", err)
 		return false
 	}
 
 	dacl, _, err := si.DACL()
 	if err != nil || dacl == nil {
 		// no dacl implies full access
+		env.Debug("isWriteable", "no dacl")
 		return true
 	}
 
@@ -248,21 +296,26 @@ func isWriteable(folder string) bool {
 
 		ret, _, _ := procGetAce.Call(uintptr(unsafe.Pointer(dacl)), uintptr(i), uintptr(unsafe.Pointer(&ace)))
 		if ret == 0 {
+			env.Debug("isWriteable", "no ace found")
 			return false
 		}
 
-		if ace.AceType == ACCESS_DENIED_ACE_TYPE {
-			continue
-		}
-
 		aceSid := (*windows.SID)(unsafe.Pointer(&ace.SidStart))
 
 		if !aceSid.Equals(cu) {
 			continue
 		}
 
-		allowMask := ^(windows.GENERIC_WRITE | windows.GENERIC_ALL)
-		if ace.AccessMask&uint32(allowMask) != 0 {
+		// this gets priority over the other access types
+		if ace.AceType == ACCESS_DENIED_ACE_TYPE {
+			env.Debug("isWriteable", "ACCESS_DENIED_ACE_TYPE")
+			return false
+		}
+
+		env.debugF("isWriteable", func() string {
+			return ace.AccessMask.permissions()
+		})
+		if ace.AccessMask.canWrite() {
 			return true
 		}
 	}
diff --git a/src/segments/gcp.go b/src/segments/gcp.go
index a153b026..d9a755e0 100644
--- a/src/segments/gcp.go
+++ b/src/segments/gcp.go
@@ -35,7 +35,7 @@ func (g *Gcp) Enabled() bool {
 	cfgDir := g.getConfigDirectory()
 	configFile, err := g.getActiveConfig(cfgDir)
 	if err != nil {
-		g.env.Log(platform.Error, "Gcp.Enabled()", err.Error())
+		g.env.Error("Gcp.Enabled()", err)
 		return false
 	}
 
@@ -43,13 +43,13 @@ func (g *Gcp) Enabled() bool {
 	cfg := g.env.FileContent(cfgpath)
 
 	if len(cfg) == 0 {
-		g.env.Log(platform.Error, "Gcp.Enabled()", "config file is empty")
+		g.env.Error("Gcp.Enabled()", errors.New("config file is empty"))
 		return false
 	}
 
 	data, err := ini.Load([]byte(cfg))
 	if err != nil {
-		g.env.Log(platform.Error, "Gcp.Enabled()", err.Error())
+		g.env.Error("Gcp.Enabled()", err)
 		return false
 	}
 
diff --git a/src/segments/path.go b/src/segments/path.go
index d8bf983e..3d83fa22 100644
--- a/src/segments/path.go
+++ b/src/segments/path.go
@@ -177,7 +177,7 @@ func (pt *Path) getFolderSeparator() string {
 	}
 	text, err := tmpl.Render()
 	if err != nil {
-		pt.env.Log(platform.Error, "getFolderSeparator", err.Error())
+		pt.env.Error("getFolderSeparator", err)
 	}
 	if len(text) == 0 {
 		return pt.env.PathSeparator()
@@ -404,7 +404,7 @@ func (pt *Path) replaceMappedLocations() (string, string) {
 		}
 		path, err := tmpl.Render()
 		if err != nil {
-			pt.env.Log(platform.Error, "replaceMappedLocations", err.Error())
+			pt.env.Error("replaceMappedLocations", err)
 		}
 		if len(path) == 0 {
 			continue
diff --git a/src/template/text.go b/src/template/text.go
index d3f26dd8..f88df0a3 100644
--- a/src/template/text.go
+++ b/src/template/text.go
@@ -48,7 +48,7 @@ func (t *Text) Render() (string, error) {
 	t.cleanTemplate()
 	tmpl, err := template.New(t.Template).Funcs(funcMap()).Parse(t.Template)
 	if err != nil {
-		t.Env.Log(platform.Error, "Render", err.Error())
+		t.Env.Error("Render", err)
 		return "", errors.New(InvalidTemplate)
 	}
 	context := &Context{}
@@ -57,7 +57,7 @@ func (t *Text) Render() (string, error) {
 	defer buffer.Reset()
 	err = tmpl.Execute(buffer, context)
 	if err != nil {
-		t.Env.Log(platform.Error, "Render", err.Error())
+		t.Env.Error("Render", err)
 		return "", errors.New(IncorrectTemplate)
 	}
 	text := buffer.String()
diff --git a/website/docs/configuration/colors.mdx b/website/docs/configuration/colors.mdx
index 5b1f89a8..db216a5d 100644
--- a/website/docs/configuration/colors.mdx
+++ b/website/docs/configuration/colors.mdx
@@ -63,7 +63,7 @@ The syntax is custom but should be rather straight-forward: `<foreground,backgro
 `<#ffffff,#000000>this is white with black background</> <#FF479C>but this is pink</>`.
 Anything between the color start `<#FF479C>` and end `</>` will be colored accordingly.
 
-If you want `prefix` to print a colored bracket that isn't the same as the segment's `foreground`, you can
+If you want to print a colored bracket that isn't the same as the segment's `foreground`, you can
 do so like this:
 
 ```json
diff --git a/website/docs/configuration/segment.mdx b/website/docs/configuration/segment.mdx
index 6b20316a..0e84133d 100644
--- a/website/docs/configuration/segment.mdx
+++ b/website/docs/configuration/segment.mdx
@@ -67,7 +67,6 @@ if we're at the last segment). Expects segments to have a colored background, el
 ### Plain
 
 Simple. Colored text on a transparent background. Make sure to set `foreground` for maximum enjoyment.
-Segments will be separated by empty spaces unless you specify `''` for the `prefix` and `postfix` settings for the segment.
 
 ### Diamond
 


Test patch:
diff --git a/src/engine/segment_test.go b/src/engine/segment_test.go
index 39bff5dc..f9efd14f 100644
--- a/src/engine/segment_test.go
+++ b/src/engine/segment_test.go
@@ -44,7 +44,6 @@ func TestParseTestConfig(t *testing.T) {
 			"foreground": "#ffffff",
 			"background": "#61AFEF",
 			"properties": {
-				"prefix": " \uE5FF ",
 				"style": "folder",
 				"exclude_folders": [
 					"/super/secret/project"
diff --git a/src/http/oauth_test.go b/src/http/oauth_test.go
index 49b470cb..4d2f2bdd 100644
--- a/src/http/oauth_test.go
+++ b/src/http/oauth_test.go
@@ -3,7 +3,6 @@ package http
 import (
 	"fmt"
 	"oh-my-posh/mock"
-	"oh-my-posh/platform"
 	"oh-my-posh/properties"
 	"testing"
 
@@ -161,7 +160,7 @@ func TestOauthResult(t *testing.T) {
 		env.On("Cache").Return(cache)
 		env.On("HTTPRequest", url).Return([]byte(tc.JSONResponse), tc.Error)
 		env.On("HTTPRequest", tokenURL).Return([]byte(tc.TokenResponse), tc.Error)
-		env.On("Log", platform.Error, "OAuth", mock2.Anything).Return()
+		env.On("Error", "OAuth", mock2.Anything).Return()
 
 		oauth := &OAuthRequest{
 			AccessTokenKey:  accessTokenKey,
diff --git a/src/http/request_test.go b/src/http/request_test.go
index cea677cc..1b799eba 100644
--- a/src/http/request_test.go
+++ b/src/http/request_test.go
@@ -3,7 +3,6 @@ package http
 import (
 	"net"
 	"oh-my-posh/mock"
-	"oh-my-posh/platform"
 	"oh-my-posh/properties"
 	"testing"
 
@@ -75,7 +74,7 @@ func TestRequestResult(t *testing.T) {
 
 		env.On("Cache").Return(cache)
 		env.On("HTTPRequest", url).Return([]byte(tc.JSONResponse), tc.Error)
-		env.On("Log", platform.Error, "OAuth", mock2.Anything).Return()
+		env.On("Error", "OAuth", mock2.Anything).Return()
 
 		request := &Request{}
 		request.Init(env, props)
diff --git a/src/segments/gcp_test.go b/src/segments/gcp_test.go
index 6c869835..4f82f828 100644
--- a/src/segments/gcp_test.go
+++ b/src/segments/gcp_test.go
@@ -57,7 +57,7 @@ func TestGcpSegment(t *testing.T) {
 		env.On("FileContent", fcPath).Return(tc.ActiveConfig)
 		cfgpath := path.Join("config", "configurations", "config_production")
 		env.On("FileContent", cfgpath).Return(tc.CfgData)
-		env.On("Log", platform.Error, "Gcp.Enabled()", mock2.Anything).Return()
+		env.On("Error", "Gcp.Enabled()", mock2.Anything).Return()
 		g := &Gcp{
 			env: env,
 		}
diff --git a/src/segments/path_test.go b/src/segments/path_test.go
index b206ff6d..9d998f41 100644
--- a/src/segments/path_test.go
+++ b/src/segments/path_test.go
@@ -26,7 +26,8 @@ func renderTemplate(env *mock.MockedEnvironment, segmentTemplate string, context
 			Env: make(map[string]string),
 		})
 	}
-	env.On("Log", mock2.Anything, mock2.Anything, mock2.Anything)
+	env.On("Error", mock2.Anything, mock2.Anything)
+	env.On("Debug", mock2.Anything, mock2.Anything)
 	tmpl := &template.Text{
 		Template: segmentTemplate,
 		Context:  context,
@@ -1292,7 +1293,8 @@ func TestGetFolderSeparator(t *testing.T) {
 	for _, tc := range cases {
 		env := new(mock.MockedEnvironment)
 		env.On("PathSeparator").Return("/")
-		env.On("Log", mock2.Anything, mock2.Anything, mock2.Anything)
+		env.On("Error", mock2.Anything, mock2.Anything)
+		env.On("Debug", mock2.Anything, mock2.Anything)
 		path := &Path{
 			env: env,
 		}
diff --git a/src/segments/wakatime_test.go b/src/segments/wakatime_test.go
index 7c212e2a..f0089e08 100644
--- a/src/segments/wakatime_test.go
+++ b/src/segments/wakatime_test.go
@@ -126,7 +126,7 @@ func TestWTGetUrl(t *testing.T) {
 	for _, tc := range cases {
 		env := &mock.MockedEnvironment{}
 
-		env.On("Log", mock2.Anything, mock2.Anything, mock2.Anything)
+		env.On("Error", mock2.Anything, mock2.Anything)
 		env.On("TemplateCache").Return(&platform.TemplateCache{
 			Env: map[string]string{"HELLO": "hello"},
 		})
diff --git a/src/template/link_test.go b/src/template/link_test.go
index e0c067cb..8d4f7481 100644
--- a/src/template/link_test.go
+++ b/src/template/link_test.go
@@ -24,7 +24,8 @@ func TestUrl(t *testing.T) {
 	env.On("TemplateCache").Return(&platform.TemplateCache{
 		Env: make(map[string]string),
 	})
-	env.On("Log", mock2.Anything, mock2.Anything, mock2.Anything)
+	env.On("Error", mock2.Anything, mock2.Anything)
+	env.On("Debug", mock2.Anything, mock2.Anything)
 	for _, tc := range cases {
 		tmpl := &Text{
 			Template: tc.Template,
diff --git a/src/template/round_test.go b/src/template/round_test.go
index a25c8b67..a174cab5 100644
--- a/src/template/round_test.go
+++ b/src/template/round_test.go
@@ -31,7 +31,8 @@ func TestRoundSeconds(t *testing.T) {
 	env.On("TemplateCache").Return(&platform.TemplateCache{
 		Env: make(map[string]string),
 	})
-	env.On("Log", mock2.Anything, mock2.Anything, mock2.Anything)
+	env.On("Error", mock2.Anything, mock2.Anything)
+	env.On("Debug", mock2.Anything, mock2.Anything)
 	for _, tc := range cases {
 		tmpl := &Text{
 			Template: tc.Template,
diff --git a/src/template/text_test.go b/src/template/text_test.go
index 470320dc..69b13ec7 100644
--- a/src/template/text_test.go
+++ b/src/template/text_test.go
@@ -152,7 +152,7 @@ func TestRenderTemplate(t *testing.T) {
 	env.On("TemplateCache").Return(&platform.TemplateCache{
 		Env: make(map[string]string),
 	})
-	env.On("Log", mock2.Anything, mock2.Anything, mock2.Anything)
+	env.On("Error", mock2.Anything, mock2.Anything)
 	for _, tc := range cases {
 		tmpl := &Text{
 			Template: tc.Template,
@@ -209,7 +209,7 @@ func TestRenderTemplateEnvVar(t *testing.T) {
 		env.On("TemplateCache").Return(&platform.TemplateCache{
 			Env: tc.Env,
 		})
-		env.On("Log", mock2.Anything, mock2.Anything, mock2.Anything)
+		env.On("Error", mock2.Anything, mock2.Anything)
 		tmpl := &Text{
 			Template: tc.Template,
 			Context:  tc.Context,