Repository: JanDeDobbeleer/oh-my-posh
Pull request #5133 (2024-06-20 10:22:11)

features and stuff
### 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

- **feat(path): allow displaying the cygwin style path**
- **fix(upgrade): disable upgrade feedback when notice is disabled**
- **fix(path): force linux style path for .Location**

<!---

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/cli/upgrade.go b/src/cli/upgrade.go
index b4820653..8ca0fbe1 100644
--- a/src/cli/upgrade.go
+++ b/src/cli/upgrade.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"runtime"
 
+	"github.com/jandedobbeleer/oh-my-posh/src/engine"
 	"github.com/jandedobbeleer/oh-my-posh/src/platform"
 	"github.com/jandedobbeleer/oh-my-posh/src/upgrade"
 	"github.com/spf13/cobra"
@@ -34,8 +35,12 @@ var upgradeCmd = &cobra.Command{
 			return
 		}
 
+		cfg := engine.LoadConfig(env)
+
 		if _, hasNotice := upgrade.Notice(env, true); !hasNotice {
-			fmt.Print("\n✅  no new version available\n\n")
+			if !cfg.DisableNotice {
+				fmt.Print("\n✅  no new version available\n\n")
+			}
 			return
 		}
 
diff --git a/src/segments/path.go b/src/segments/path.go
index bfb161a5..f7a4bf3b 100644
--- a/src/segments/path.go
+++ b/src/segments/path.go
@@ -17,9 +17,12 @@ type Path struct {
 	props properties.Properties
 	env   platform.Environment
 
-	root     string
-	relative string
-	pwd      string
+	root          string
+	relative      string
+	pwd           string
+	cygPath       bool
+	windowsPath   bool
+	pathSeparator string
 
 	Path       string
 	StackCount int
@@ -89,6 +92,8 @@ const (
 	RightFormat properties.Property = "right_format"
 	// GitDirFormat format to use on the git directory
 	GitDirFormat properties.Property = "gitdir_format"
+	// DisplayCygpath transforms the path to a cygpath format
+	DisplayCygpath properties.Property = "display_cygpath"
 )
 
 func (pt *Path) Template() string {
@@ -104,7 +109,7 @@ func (pt *Path) Enabled() bool {
 	pt.setStyle()
 	pwd := pt.env.Pwd()
 
-	pt.Location = pt.env.TemplateCache().AbsolutePWD
+	pt.Location = strings.ReplaceAll(pt.env.TemplateCache().AbsolutePWD, `\`, `/`)
 	pt.StackCount = pt.env.StackCount()
 	pt.Writable = pt.env.DirIsWritable(pwd)
 	return true
@@ -115,6 +120,20 @@ func (pt *Path) setPaths() {
 		pt.Folders = pt.splitPath()
 	}()
 
+	displayCygpath := func() bool {
+		enableCygpath := pt.props.GetBool(DisplayCygpath, false)
+		if !enableCygpath {
+			return false
+		}
+
+		isCygwin := pt.env.GOOS() == platform.WINDOWS && pt.env.Shell() == shell.BASH
+		return isCygwin
+	}
+
+	pt.cygPath = displayCygpath()
+	pt.windowsPath = pt.env.GOOS() == platform.WINDOWS && !pt.cygPath
+	pt.pathSeparator = pt.env.PathSeparator()
+
 	pt.pwd = pt.env.Pwd()
 	if (pt.env.Shell() == shell.PWSH || pt.env.Shell() == shell.PWSH5) && len(pt.env.Flags().PSWD) != 0 {
 		pt.pwd = pt.env.Flags().PSWD
@@ -133,9 +152,8 @@ func (pt *Path) setPaths() {
 		return
 	}
 
-	pathSeparator := pt.env.PathSeparator()
-	if !strings.HasSuffix(pt.root, pathSeparator) && len(pt.relative) > 0 {
-		pt.pwd = pt.root + pathSeparator + pt.relative
+	if !strings.HasSuffix(pt.root, pt.pathSeparator) && len(pt.relative) > 0 {
+		pt.pwd = pt.root + pt.pathSeparator + pt.relative
 		return
 	}
 
@@ -163,11 +181,14 @@ func (pt *Path) Init(props properties.Properties, env platform.Environment) {
 func (pt *Path) setStyle() {
 	if len(pt.relative) == 0 {
 		pt.Path = pt.root
+
 		if strings.HasSuffix(pt.Path, ":") {
 			pt.Path += pt.getFolderSeparator()
 		}
+
 		return
 	}
+
 	switch style := pt.props.GetString(properties.Style, Agnoster); style {
 	case Agnoster:
 		pt.Path = pt.getAgnosterPath()
@@ -228,35 +249,40 @@ func (pt *Path) getMaxWidth() int {
 func (pt *Path) getFolderSeparator() string {
 	separatorTemplate := pt.props.GetString(FolderSeparatorTemplate, "")
 	if len(separatorTemplate) == 0 {
-		separator := pt.props.GetString(FolderSeparatorIcon, pt.env.PathSeparator())
+		separator := pt.props.GetString(FolderSeparatorIcon, pt.pathSeparator)
 		// if empty, use the default separator
 		if len(separator) == 0 {
-			return pt.env.PathSeparator()
+			return pt.pathSeparator
 		}
 		return separator
 	}
+
 	tmpl := &template.Text{
 		Template: separatorTemplate,
 		Context:  pt,
 		Env:      pt.env,
 	}
+
 	text, err := tmpl.Render()
 	if err != nil {
 		pt.env.Error(err)
 	}
+
 	if len(text) == 0 {
-		return pt.env.PathSeparator()
+		return pt.pathSeparator
 	}
+
 	return text
 }
 
 func (pt *Path) getMixedPath() string {
 	var buffer strings.Builder
+
 	threshold := int(pt.props.GetFloat64(MixedThreshold, 4))
 	folderIcon := pt.props.GetString(FolderIcon, "..")
 	separator := pt.getFolderSeparator()
 
-	if pt.root != pt.env.PathSeparator() {
+	if pt.root != pt.pathSeparator {
 		pt.Folders = append(Folders{{Name: pt.root}}, pt.Folders...)
 	}
 
@@ -276,7 +302,7 @@ func (pt *Path) getMixedPath() string {
 func (pt *Path) getAgnosterPath() string {
 	folderIcon := pt.props.GetString(FolderIcon, "..")
 
-	if pt.root == pt.env.PathSeparator() {
+	if pt.root == pt.pathSeparator {
 		pt.root = pt.Folders[0].Name
 		pt.Folders = pt.Folders[1:]
 	}
@@ -303,7 +329,7 @@ func (pt *Path) getAgnosterPath() string {
 func (pt *Path) getAgnosterLeftPath() string {
 	folderIcon := pt.props.GetString(FolderIcon, "..")
 
-	if pt.root == pt.env.PathSeparator() {
+	if pt.root == pt.pathSeparator {
 		pt.root = pt.Folders[0].Name
 		pt.Folders = pt.Folders[1:]
 	}
@@ -341,7 +367,7 @@ func (pt *Path) getRelevantLetter(folder *Folder) string {
 }
 
 func (pt *Path) getLetterPath() string {
-	if pt.root == pt.env.PathSeparator() {
+	if pt.root == pt.pathSeparator {
 		pt.root = pt.Folders[0].Name
 		pt.Folders = pt.Folders[1:]
 	}
@@ -370,7 +396,7 @@ func (pt *Path) getLetterPath() string {
 func (pt *Path) getUniqueLettersPath(maxWidth int) string {
 	separator := pt.getFolderSeparator()
 
-	if pt.root == pt.env.PathSeparator() {
+	if pt.root == pt.pathSeparator {
 		pt.root = pt.Folders[0].Name
 		pt.Folders = pt.Folders[1:]
 	}
@@ -424,7 +450,7 @@ func (pt *Path) getUniqueLettersPath(maxWidth int) string {
 }
 
 func (pt *Path) getAgnosterFullPath() string {
-	if pt.root == pt.env.PathSeparator() {
+	if pt.root == pt.pathSeparator {
 		pt.root = pt.Folders[0].Name
 		pt.Folders = pt.Folders[1:]
 	}
@@ -450,14 +476,13 @@ func (pt *Path) getAgnosterShortPath() string {
 		return pt.getAgnosterFullPath()
 	}
 
-	pathSeparator := pt.env.PathSeparator()
 	splitPos := pathDepth - maxDepth
 
 	var folders []string
 	// unix root, needs to be replaced with the folder we're in at root level
 	root := pt.root
 	room := pathDepth - maxDepth
-	if root == pathSeparator {
+	if root == pt.pathSeparator {
 		root = pt.Folders[0].Name
 		room--
 	}
@@ -502,18 +527,22 @@ func (pt *Path) replaceMappedLocations() (string, string) {
 		if len(key) == 0 {
 			continue
 		}
+
 		tmpl := &template.Text{
 			Template: key,
 			Context:  pt,
 			Env:      pt.env,
 		}
+
 		path, err := tmpl.Render()
 		if err != nil {
 			pt.env.Error(err)
 		}
+
 		if len(path) == 0 {
 			continue
 		}
+
 		mappedLocations[pt.normalize(path)] = val
 	}
 
@@ -524,59 +553,71 @@ func (pt *Path) replaceMappedLocations() (string, string) {
 	for k := range mappedLocations {
 		keys = append(keys, k)
 	}
+
 	sort.Sort(sort.Reverse(sort.StringSlice(keys)))
 
 	root, relative := pt.parsePath(pt.pwd)
 	if len(relative) == 0 {
 		pt.RootDir = true
 	}
+
 	rootN := pt.normalize(root)
 	relativeN := pt.normalize(relative)
-	pathSeparator := pt.env.PathSeparator()
 
 	for _, key := range keys {
 		keyRoot, keyRelative := pt.parsePath(key)
 		matchSubFolders := strings.HasSuffix(keyRelative, "*")
+
 		if matchSubFolders && len(keyRelative) > 1 {
 			keyRelative = keyRelative[0 : len(keyRelative)-1] // remove trailing /* or \*
 		}
+
 		if keyRoot != rootN || !strings.HasPrefix(relativeN, keyRelative) {
 			continue
 		}
+
 		value := mappedLocations[key]
 		overflow := relative[len(keyRelative):]
+
 		if len(overflow) == 0 {
 			// exactly match the full path
 			return value, ""
 		}
+
 		if len(keyRelative) == 0 {
 			// only match the root
-			return value, strings.Trim(relative, pathSeparator)
+			return value, strings.Trim(relative, pt.pathSeparator)
 		}
+
 		// match several prefix elements
-		if matchSubFolders || overflow[0:1] == pt.env.PathSeparator() {
-			return value, strings.Trim(overflow, pathSeparator)
+		if matchSubFolders || overflow[0:1] == pt.pathSeparator {
+			return value, strings.Trim(overflow, pt.pathSeparator)
 		}
 	}
-	return root, strings.Trim(relative, pathSeparator)
+
+	return root, strings.Trim(relative, pt.pathSeparator)
 }
 
 func (pt *Path) normalizePath(path string) string {
-	if pt.env.GOOS() != platform.WINDOWS {
+	if pt.env.GOOS() != platform.WINDOWS || pt.cygPath {
 		return path
 	}
+
 	var clean []rune
 	for _, char := range path {
 		var lastChar rune
 		if len(clean) > 0 {
 			lastChar = clean[len(clean)-1:][0]
 		}
+
 		if char == '/' && lastChar != 60 { // 60 == <, this is done to ovoid replacing color codes
 			clean = append(clean, 92) // 92 == \
 			continue
 		}
+
 		clean = append(clean, char)
 	}
+
 	return string(clean)
 }
 
@@ -586,21 +627,34 @@ func (pt *Path) parsePath(inputPath string) (string, string) {
 	if len(inputPath) == 0 {
 		return root, path
 	}
-	separator := pt.env.PathSeparator()
+
+	if pt.cygPath {
+		path, err := pt.env.RunCommand("cygpath", "-u", inputPath)
+		if len(path) != 0 {
+			inputPath = path
+			pt.pathSeparator = "/"
+		}
+
+		if err != nil {
+			pt.cygPath = false
+			pt.windowsPath = true
+		}
+	}
+
 	clean := func(path string) string {
-		matches := regex.FindAllNamedRegexMatch(fmt.Sprintf(`(?P<element>[^\%s]+)`, separator), path)
+		matches := regex.FindAllNamedRegexMatch(fmt.Sprintf(`(?P<element>[^\%s]+)`, pt.pathSeparator), path)
 		n := len(matches) - 1
 		s := new(strings.Builder)
 		for i, m := range matches {
 			s.WriteString(m["element"])
 			if i != n {
-				s.WriteString(separator)
+				s.WriteString(pt.pathSeparator)
 			}
 		}
 		return s.String()
 	}
 
-	if pt.env.GOOS() == platform.WINDOWS {
+	if pt.windowsPath {
 		inputPath = pt.normalizePath(inputPath)
 		// for a UNC path, extract \\hostname\sharename as the root
 		matches := regex.FindNamedRegexMatch(`^\\\\(?P<hostname>[^\\]+)\\+(?P<sharename>[^\\]+)\\*(?P<path>[\s\S]*)$`, inputPath)
@@ -610,14 +664,18 @@ func (pt *Path) parsePath(inputPath string) (string, string) {
 			return root, path
 		}
 	}
-	s := strings.SplitAfterN(inputPath, separator, 2)
+
+	s := strings.SplitAfterN(inputPath, pt.pathSeparator, 2)
 	root = s[0]
-	if pt.env.GOOS() == platform.WINDOWS {
-		root = strings.TrimSuffix(root, separator)
+
+	if pt.windowsPath {
+		root = strings.TrimSuffix(root, pt.pathSeparator)
 	}
+
 	if len(s) == 2 {
 		path = clean(s[1])
 	}
+
 	return root, path
 }
 
@@ -626,6 +684,11 @@ func (pt *Path) normalize(inputPath string) string {
 	if strings.HasPrefix(normalized, "~") && (len(normalized) == 1 || platform.IsPathSeparator(pt.env, normalized[1])) {
 		normalized = pt.env.Home() + normalized[1:]
 	}
+
+	if pt.cygPath {
+		return normalized
+	}
+
 	switch pt.env.GOOS() {
 	case platform.WINDOWS:
 		normalized = pt.normalizePath(normalized)
@@ -633,11 +696,12 @@ func (pt *Path) normalize(inputPath string) string {
 	case platform.DARWIN:
 		normalized = strings.ToLower(normalized)
 	}
+
 	return normalized
 }
 
 func (pt *Path) replaceFolderSeparators(pwd string) string {
-	defaultSeparator := pt.env.PathSeparator()
+	defaultSeparator := pt.pathSeparator
 	folderSeparator := pt.getFolderSeparator()
 	if folderSeparator == defaultSeparator {
 		return pwd
@@ -685,7 +749,7 @@ func (pt *Path) colorizePath(root string, elements []string) string {
 	root = fmt.Sprintf(leftFormat, root)
 	builder.WriteString(colorizeElement(root))
 
-	if root != pt.env.PathSeparator() && len(root) != 0 {
+	if root != pt.pathSeparator && len(root) != 0 {
 		builder.WriteString(colorizeSeparator())
 	}
 
@@ -732,18 +796,18 @@ func (pt *Path) splitPath() Folders {
 	folders := []string{}
 
 	if len(pt.relative) != 0 {
-		folders = strings.Split(pt.relative, pt.env.PathSeparator())
+		folders = strings.Split(pt.relative, pt.pathSeparator)
 	}
 
 	folderFormatMap := pt.makeFolderFormatMap()
 
 	getCurrentPath := func() string {
 		if pt.root == "~" {
-			return pt.env.Home() + pt.env.PathSeparator()
+			return pt.env.Home() + pt.pathSeparator
 		}
 
-		if pt.env.GOOS() == platform.WINDOWS {
-			return pt.root + pt.env.PathSeparator()
+		if pt.windowsPath {
+			return pt.root + pt.pathSeparator
 		}
 
 		return pt.root
@@ -763,7 +827,7 @@ func (pt *Path) splitPath() Folders {
 
 		result = append(result, &Folder{Name: folder, Path: currentPath, Display: display})
 
-		currentPath += pt.env.PathSeparator()
+		currentPath += pt.pathSeparator
 
 		display = false
 	}
diff --git a/themes/schema.json b/themes/schema.json
index 6da564e1..af3caee5 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -2545,6 +2545,12 @@
                     "title": "The format to use on a git root directory",
                     "description": "Golang string format to apply to the .git folder",
                     "default": ""
+                  },
+                  "display_cygpath": {
+                    "type": "boolean",
+                    "title": "Display the Cygwin (Linux) style path",
+                    "description": "Display the Cygwin (Linux) style path using cygpath -u $PWD.",
+                    "default": false
                   }
                 }
               }
diff --git a/website/docs/segments/path.mdx b/website/docs/segments/path.mdx
index 2f29b2ba..3d9a63d2 100644
--- a/website/docs/segments/path.mdx
+++ b/website/docs/segments/path.mdx
@@ -49,6 +49,7 @@ import Config from "@site/src/components/Config.js";
 | `left_format`               |  `string`  |    `%s`    | format to use on the firstfolder of the path - defaults to `edge_format`                                         |
 | `right_format`              |  `string`  |    `%s`    | format to use on the last folder of the path - defaults to `edge_format`                                         |
 | `gitdir_format`             |  `string`  |            | format to use for a git root directory                                                                           |
+| `display_cygpath`           | `boolean`  |  `false`   | display the Cygwin style path using `cygpath -u $PWD`                                                            |
 
 ## Mapped Locations
 


Test patch:
diff --git a/src/segments/path_test.go b/src/segments/path_test.go
index 55971038..9c70f6ef 100644
--- a/src/segments/path_test.go
+++ b/src/segments/path_test.go
@@ -1,6 +1,7 @@
 package segments
 
 import (
+	"errors"
 	"strings"
 	"testing"
 
@@ -170,6 +171,9 @@ func TestAgnosterPathStyles(t *testing.T) {
 		HomeIcon            string
 		FolderSeparatorIcon string
 		GOOS                string
+		Shell               string
+		Cygpath             string
+		CygpathError        error
 		MaxDepth            int
 		MaxWidth            int
 		HideRootLocation    bool
@@ -417,6 +421,28 @@ func TestAgnosterPathStyles(t *testing.T) {
 			PathSeparator:       "\\",
 			FolderSeparatorIcon: " > ",
 		},
+		{
+			Style:               Mixed,
+			Expected:            "c > .. > foo > .. > man",
+			HomePath:            homeDirWindows,
+			Pwd:                 "C:\\Users\\foo\\foobar\\man",
+			GOOS:                platform.WINDOWS,
+			Shell:               shell.BASH,
+			Cygpath:             "/c/Users/foo/foobar/man",
+			PathSeparator:       "\\",
+			FolderSeparatorIcon: " > ",
+		},
+		{
+			Style:               Mixed,
+			Expected:            "C: > .. > foo > .. > man",
+			HomePath:            homeDirWindows,
+			Pwd:                 "C:\\Users\\foo\\foobar\\man",
+			GOOS:                platform.WINDOWS,
+			Shell:               shell.BASH,
+			CygpathError:        errors.New("oh no"),
+			PathSeparator:       "\\",
+			FolderSeparatorIcon: " > ",
+		},
 
 		{
 			Style:               AgnosterFull,
@@ -743,10 +769,20 @@ func TestAgnosterPathStyles(t *testing.T) {
 			PSWD: tc.Pswd,
 		}
 		env.On("Flags").Return(args)
-		env.On("Shell").Return(shell.PWSH)
+
+		if len(tc.Shell) == 0 {
+			tc.Shell = shell.PWSH
+		}
+		env.On("Shell").Return(tc.Shell)
 
 		env.On("DebugF", mock2.Anything, mock2.Anything).Return(nil)
 
+		displayCygpath := tc.GOOS == platform.WINDOWS && tc.Shell == shell.BASH
+		if displayCygpath {
+			env.On("RunCommand", "cygpath", []string{"-u", tc.Pwd}).Return(tc.Cygpath, tc.CygpathError)
+			env.On("RunCommand", "cygpath", mock2.Anything).Return("brrrr", nil)
+		}
+
 		path := &Path{
 			env: env,
 			props: properties.Map{
@@ -755,6 +791,7 @@ func TestAgnosterPathStyles(t *testing.T) {
 				MaxDepth:            tc.MaxDepth,
 				MaxWidth:            tc.MaxWidth,
 				HideRootLocation:    tc.HideRootLocation,
+				DisplayCygpath:      displayCygpath,
 			},
 		}
 
@@ -917,17 +954,22 @@ func TestFullPathCustomMappedLocations(t *testing.T) {
 		env := new(mock.MockedEnvironment)
 		env.On("Home").Return(homeDir)
 		env.On("Pwd").Return(tc.Pwd)
+
 		if len(tc.GOOS) == 0 {
 			tc.GOOS = platform.DARWIN
 		}
+
 		env.On("GOOS").Return(tc.GOOS)
+
 		if len(tc.PathSeparator) == 0 {
 			tc.PathSeparator = "/"
 		}
+
 		env.On("PathSeparator").Return(tc.PathSeparator)
 		args := &platform.Flags{
 			PSWD: tc.Pwd,
 		}
+
 		env.On("Flags").Return(args)
 		env.On("Shell").Return(shell.GENERIC)
 		env.On("DebugF", mock2.Anything, mock2.Anything).Return(nil)
@@ -936,6 +978,7 @@ func TestFullPathCustomMappedLocations(t *testing.T) {
 				"HOME": "/a/b/c",
 			},
 		})
+
 		path := &Path{
 			env: env,
 			props: properties.Map{
@@ -944,6 +987,7 @@ func TestFullPathCustomMappedLocations(t *testing.T) {
 				MappedLocations:        tc.MappedLocations,
 			},
 		}
+
 		path.setPaths()
 		path.setStyle()
 		got := renderTemplateNoTrimSpace(env, "{{ .Path }}", path)
@@ -1414,24 +1458,29 @@ func TestGetFolderSeparator(t *testing.T) {
 
 	for _, tc := range cases {
 		env := new(mock.MockedEnvironment)
-		env.On("PathSeparator").Return("/")
 		env.On("Error", mock2.Anything)
 		env.On("Debug", mock2.Anything)
 		env.On("DebugF", mock2.Anything, mock2.Anything).Return(nil)
 		path := &Path{
-			env: env,
+			env:           env,
+			pathSeparator: "/",
 		}
+
 		props := properties.Map{}
+
 		if len(tc.FolderSeparatorTemplate) > 0 {
 			props[FolderSeparatorTemplate] = tc.FolderSeparatorTemplate
 		}
+
 		if len(tc.FolderSeparatorIcon) > 0 {
 			props[FolderSeparatorIcon] = tc.FolderSeparatorIcon
 		}
+
 		env.On("TemplateCache").Return(&platform.TemplateCache{
 			Env:   make(map[string]string),
 			Shell: "bash",
 		})
+
 		path.props = props
 		got := path.getFolderSeparator()
 		assert.Equal(t, tc.Expected, got)
@@ -1558,18 +1607,21 @@ func TestSplitPath(t *testing.T) {
 
 	for _, tc := range cases {
 		env := new(mock.MockedEnvironment)
-		env.On("PathSeparator").Return("/")
 		env.On("Home").Return("/a/b")
 		env.On("HasParentFilePath", ".git").Return(tc.GitDir, nil)
 		env.On("GOOS").Return(tc.GOOS)
+
 		path := &Path{
 			env: env,
 			props: properties.Map{
 				GitDirFormat: tc.GitDirFormat,
 			},
-			root:     tc.Root,
-			relative: tc.Relative,
+			root:          tc.Root,
+			relative:      tc.Relative,
+			pathSeparator: "/",
+			windowsPath:   tc.GOOS == platform.WINDOWS,
 		}
+
 		got := path.splitPath()
 		assert.Equal(t, tc.Expected, got, tc.Case)
 	}