Repository: JanDeDobbeleer/oh-my-posh
Pull request #3083 (2022-11-15 12:13:47)

Only solutions
### 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)
<!---

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/color/ansi.go b/src/color/ansi.go
index 057cbad1..0b3e45d3 100644
--- a/src/color/ansi.go
+++ b/src/color/ansi.go
@@ -140,20 +140,59 @@ func (a *Ansi) InitPlain() {
 }
 
 func (a *Ansi) GenerateHyperlink(text string) string {
-	parts := strings.SplitAfter(text, ")")
-	var buffer strings.Builder
-	var part string
-	for i := range parts {
-		part += parts[i]
-		if strings.Contains(parts[i], "[") && !strings.Contains(parts[i], "]") {
+	const (
+		LINK  = "link"
+		TEXT  = "text"
+		OTHER = "plain"
+	)
+
+	var result, hyperlink strings.Builder
+	var squareIndex, roundCount int
+	state := OTHER
+
+	for i, s := range text {
+		if s == '[' && state == OTHER {
+			state = TEXT
+			hyperlink.WriteRune(s)
 			continue
 		}
-		buffer.WriteString(a.replaceHyperlink(part))
-		part = ""
+
+		if state == OTHER {
+			result.WriteRune(s)
+			continue
+		}
+
+		hyperlink.WriteRune(s)
+
+		switch s {
+		case ']':
+			// potential end of text part of hyperlink
+			squareIndex = i
+		case '(':
+			// split into link part
+			if squareIndex == i-1 {
+				state = LINK
+			}
+			if state == LINK {
+				roundCount++
+			}
+		case ')':
+			if state != LINK {
+				continue
+			}
+			roundCount--
+			if roundCount != 0 {
+				continue
+			}
+			// end of link part
+			result.WriteString(a.replaceHyperlink(hyperlink.String()))
+			hyperlink.Reset()
+			state = OTHER
+		}
 	}
-	// when we did not process any parts, we return the original text
-	buffer.WriteString(part)
-	return buffer.String()
+
+	result.WriteString(hyperlink.String())
+	return result.String()
 }
 
 func (a *Ansi) replaceHyperlink(text string) string {
diff --git a/src/platform/networks_windows.go b/src/platform/networks_windows.go
index 92228290..cc07ea87 100644
--- a/src/platform/networks_windows.go
+++ b/src/platform/networks_windows.go
@@ -229,7 +229,9 @@ func (env *Shell) wifiNetwork() (*Connection, error) {
 }
 
 func (env *Shell) parseNetworkInterface(network *WLAN_INTERFACE_INFO, clientHandle uint32) (*Connection, error) {
-	info := Connection{}
+	info := Connection{
+		Type: WIFI,
+	}
 
 	// Query wifi connection state
 	var dataSize uint16
diff --git a/src/platform/shell_unix.go b/src/platform/shell_unix.go
index 594ca6cc..b8fe655c 100644
--- a/src/platform/shell_unix.go
+++ b/src/platform/shell_unix.go
@@ -6,11 +6,11 @@ import (
 	"errors"
 	"os"
 	"strings"
-	"syscall"
 	"time"
 
 	"github.com/shirou/gopsutil/v3/host"
 	terminal "github.com/wayneashleyberry/terminal-dimensions"
+	"golang.org/x/sys/unix"
 )
 
 func (env *Shell) Root() bool {
@@ -126,35 +126,7 @@ func (env *Shell) LookWinAppPath(file string) (string, error) {
 
 func (env *Shell) DirIsWritable(path string) bool {
 	defer env.Trace(time.Now(), "DirIsWritable", path)
-	info, err := os.Stat(path)
-	if err != nil {
-		env.Log(Error, "DirIsWritable", err.Error())
-		return false
-	}
-
-	if !info.IsDir() {
-		env.Log(Error, "DirIsWritable", "Path isn't a directory")
-		return false
-	}
-
-	// Check if the user bit is enabled in file permission
-	if info.Mode().Perm()&(1<<(uint(7))) == 0 {
-		env.Log(Error, "DirIsWritable", "Write permission bit is not set on this file for user")
-		return false
-	}
-
-	var stat syscall.Stat_t
-	if err = syscall.Stat(path, &stat); err != nil {
-		env.Log(Error, "DirIsWritable", err.Error())
-		return false
-	}
-
-	if uint32(os.Geteuid()) != stat.Uid {
-		env.Log(Error, "DirIsWritable", "User doesn't have permission to write to this directory")
-		return false
-	}
-
-	return true
+	return unix.Access(path, unix.W_OK) == nil
 }
 
 func (env *Shell) Connection(connectionType ConnectionType) (*Connection, error) {
diff --git a/src/platform/shell_windows.go b/src/platform/shell_windows.go
index e230a040..fc1f2fb7 100644
--- a/src/platform/shell_windows.go
+++ b/src/platform/shell_windows.go
@@ -235,24 +235,7 @@ func (env *Shell) ConvertToLinuxPath(path string) string {
 
 func (env *Shell) DirIsWritable(path string) bool {
 	defer env.Trace(time.Now(), "DirIsWritable")
-	info, err := os.Stat(path)
-	if err != nil {
-		env.Log(Error, "DirIsWritable", err.Error())
-		return false
-	}
-
-	if !info.IsDir() {
-		env.Log(Error, "DirIsWritable", "Path isn't a directory")
-		return false
-	}
-
-	// Check if the user bit is enabled in file permission
-	if info.Mode().Perm()&(1<<(uint(7))) == 0 {
-		env.Log(Error, "DirIsWritable", "Write permission bit is not set on this file for user")
-		return false
-	}
-
-	return true
+	return isWriteable(path)
 }
 
 func (env *Shell) Connection(connectionType ConnectionType) (*Connection, error) {
diff --git a/src/platform/win32_windows.go b/src/platform/win32_windows.go
index 8db44be4..6534f6dc 100644
--- a/src/platform/win32_windows.go
+++ b/src/platform/win32_windows.go
@@ -3,6 +3,7 @@ package platform
 import (
 	"errors"
 	"oh-my-posh/regex"
+	"reflect"
 	"strings"
 	"syscall"
 	"unicode/utf16"
@@ -194,3 +195,76 @@ func readWinAppLink(path string) (string, error) {
 	}
 	return appExecLink.Path()
 }
+
+var (
+	advapi     = syscall.NewLazyDLL("advapi32.dll")
+	procGetAce = advapi.NewProc("GetAce")
+)
+
+const (
+	ACCESS_DENIED_ACE_TYPE = 1 //nolint: revive
+)
+
+type AccessAllowedAce struct {
+	AceType    uint8
+	AceFlags   uint8
+	AceSize    uint16
+	AccessMask uint32
+	SidStart   uint32
+}
+
+func getCurrentUser() (sid *windows.SID, err error) {
+	token := windows.GetCurrentProcessToken()
+	defer token.Close()
+
+	tokenuser, err := token.GetTokenUser()
+	sid = tokenuser.User.Sid
+	return
+}
+
+func isWriteable(folder string) bool {
+	cu, err := getCurrentUser()
+	if err != nil {
+		// unable to get current user
+		return false
+	}
+
+	si, err := windows.GetNamedSecurityInfo(folder, windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION)
+	if err != nil {
+		return false
+	}
+
+	dacl, _, err := si.DACL()
+	if err != nil || dacl == nil {
+		// no dacl implies full access
+		return true
+	}
+
+	rs := reflect.ValueOf(dacl).Elem()
+	aceCount := rs.Field(3).Uint()
+
+	for i := uint64(0); i < aceCount; i++ {
+		ace := &AccessAllowedAce{}
+
+		ret, _, _ := procGetAce.Call(uintptr(unsafe.Pointer(dacl)), uintptr(i), uintptr(unsafe.Pointer(&ace)))
+		if ret == 0 {
+			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 {
+			return true
+		}
+	}
+	return false
+}
diff --git a/src/segments/path.go b/src/segments/path.go
index 04cbdb11..d8bf983e 100644
--- a/src/segments/path.go
+++ b/src/segments/path.go
@@ -394,9 +394,22 @@ func (pt *Path) replaceMappedLocations() (string, string) {
 	// mapped locations can override predefined locations
 	keyValues := pt.props.GetKeyValueMap(MappedLocations, make(map[string]string))
 	for key, val := range keyValues {
-		if key != "" {
-			mappedLocations[pt.normalize(key)] = val
+		if len(key) == 0 {
+			continue
+		}
+		tmpl := &template.Text{
+			Template: key,
+			Context:  pt,
+			Env:      pt.env,
+		}
+		path, err := tmpl.Render()
+		if err != nil {
+			pt.env.Log(platform.Error, "replaceMappedLocations", err.Error())
+		}
+		if len(path) == 0 {
+			continue
 		}
+		mappedLocations[pt.normalize(path)] = val
 	}
 
 	// sort map keys in reverse order
diff --git a/src/template/text.go b/src/template/text.go
index 749a56e6..d3f26dd8 100644
--- a/src/template/text.go
+++ b/src/template/text.go
@@ -42,6 +42,9 @@ func (c *Context) init(t *Text) {
 }
 
 func (t *Text) Render() (string, error) {
+	if !strings.Contains(t.Template, "{{") || !strings.Contains(t.Template, "}}") {
+		return t.Template, nil
+	}
 	t.cleanTemplate()
 	tmpl, err := template.New(t.Template).Funcs(funcMap()).Parse(t.Template)
 	if err != nil {
@@ -143,5 +146,6 @@ func (t *Text) cleanTemplate() {
 		}
 	}
 
-	t.Template = result
+	// return the result and remaining unresolved property
+	t.Template = result + property
 }


Test patch:
diff --git a/src/color/ansi_test.go b/src/color/ansi_test.go
index e060f927..50a31d9e 100644
--- a/src/color/ansi_test.go
+++ b/src/color/ansi_test.go
@@ -31,16 +31,16 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) {
 		ShellName string
 		Expected  string
 	}{
-		{
-			Text:      "in <accent>\x1b[1mpwsh \x1b[22m</> ",
-			ShellName: shell.PWSH,
-			Expected:  "in <accent>\x1b[1mpwsh \x1b[22m</> ",
-		},
 		{
 			Text:      "[google](http://www.google.be) [maps (2/2)](http://maps.google.be)",
 			ShellName: shell.FISH,
 			Expected:  "\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\ \x1b]8;;http://maps.google.be\x1b\\maps (2/2)\x1b]8;;\x1b\\",
 		},
+		{
+			Text:      "in <accent>\x1b[1mpwsh \x1b[22m</> ",
+			ShellName: shell.PWSH,
+			Expected:  "in <accent>\x1b[1mpwsh \x1b[22m</> ",
+		},
 		{Text: "[google](http://www.google.be)", ShellName: shell.ZSH, Expected: "%{\x1b]8;;http://www.google.be\x1b\\%}google%{\x1b]8;;\x1b\\%}"},
 		{Text: "[google](http://www.google.be)", ShellName: shell.PWSH, Expected: "\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\"},
 		{Text: "[google](http://www.google.be)", ShellName: shell.BASH, Expected: "\\[\x1b]8;;http://www.google.be\x1b\\\\\\]google\\[\x1b]8;;\x1b\\\\\\]"},
@@ -100,3 +100,22 @@ func TestFormatText(t *testing.T) {
 		assert.Equal(t, tc.Expected, formattedText, tc.Case)
 	}
 }
+
+func TestGenerateFileLink(t *testing.T) {
+	cases := []struct {
+		Text     string
+		Expected string
+	}{
+		{
+			Text:     `[Posh](file:C:/Program Files (x86)/Common Files/Microsoft Shared/Posh)`,
+			Expected: "\x1b]8;;file:C:/Program Files (x86)/Common Files/Microsoft Shared/Posh\x1b\\Posh\x1b]8;;\x1b\\",
+		},
+		{Text: `[Windows](file:C:/Windows)`, Expected: "\x1b]8;;file:C:/Windows\x1b\\Windows\x1b]8;;\x1b\\"},
+	}
+	for _, tc := range cases {
+		a := Ansi{}
+		a.Init(shell.PWSH)
+		hyperlinkText := a.GenerateHyperlink(tc.Text)
+		assert.Equal(t, tc.Expected, hyperlinkText)
+	}
+}
diff --git a/src/segments/aws_test.go b/src/segments/aws_test.go
index 43623b03..6bbc510a 100644
--- a/src/segments/aws_test.go
+++ b/src/segments/aws_test.go
@@ -3,7 +3,6 @@ package segments
 import (
 	"oh-my-posh/mock"
 	"oh-my-posh/properties"
-	"oh-my-posh/template"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -45,7 +44,7 @@ func TestAWSSegment(t *testing.T) {
 			Region:          "eu-west",
 			Template:        "profile: {{.Profile}}{{if .Region}} in {{.Region}}{{end}}",
 		},
-		{Case: "template: invalid", ExpectedString: template.InvalidTemplate, ExpectedEnabled: true, Profile: "c", Template: "{{ .Burp"},
+		{Case: "template: invalid", ExpectedString: "{{ .Burp", ExpectedEnabled: true, Profile: "c", Template: "{{ .Burp"},
 	}
 
 	for _, tc := range cases {
diff --git a/src/segments/path_test.go b/src/segments/path_test.go
index 9d70150f..b206ff6d 100644
--- a/src/segments/path_test.go
+++ b/src/segments/path_test.go
@@ -842,6 +842,7 @@ func TestFullPathCustomMappedLocations(t *testing.T) {
 		PathSeparator   string
 		Expected        string
 	}{
+		{Pwd: "/a/b/c/d", MappedLocations: map[string]string{"{{ .Env.HOME }}/d": "#"}, Expected: "#"},
 		{Pwd: "/a/b/c/d", MappedLocations: map[string]string{"/a/b/c/d": "#"}, Expected: "#"},
 		{Pwd: "\\a\\b\\c\\d", MappedLocations: map[string]string{"\\a\\b": "#"}, GOOS: platform.WINDOWS, PathSeparator: "\\", Expected: "#\\c\\d"},
 		{Pwd: "/a/b/c/d", MappedLocations: map[string]string{"/a/b": "#"}, Expected: "#/c/d"},
@@ -868,6 +869,11 @@ func TestFullPathCustomMappedLocations(t *testing.T) {
 		}
 		env.On("Flags").Return(args)
 		env.On("Shell").Return(shell.PLAIN)
+		env.On("TemplateCache").Return(&platform.TemplateCache{
+			Env: map[string]string{
+				"HOME": "/a/b/c",
+			},
+		})
 		path := &Path{
 			env: env,
 			props: properties.Map{
diff --git a/src/template/text_test.go b/src/template/text_test.go
index 7114c2b0..233d3d62 100644
--- a/src/template/text_test.go
+++ b/src/template/text_test.go
@@ -20,6 +20,11 @@ func TestRenderTemplate(t *testing.T) {
 		ShouldError bool
 		Context     interface{}
 	}{
+		{
+			Case:     "color override with dots",
+			Expected: "😺💬<#FF8000> Meow! What should I do next? ...</>",
+			Template: "😺💬<#FF8000> Meow! What should I do next? ...</>",
+		},
 		{
 			Case:     "tillig's regex",
 			Expected: " ⎈ hello :: world ",