Repository: JanDeDobbeleer/oh-my-posh
Pull request #3731 (2023-04-19 10:40:02)

feat(path): add colored full path
feat(path): add colored full path

---
Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/JanDeDobbeleer/oh-my-posh/pull/3731).
* #3732
* __->__ #3731


Patch:
diff --git a/src/segments/path.go b/src/segments/path.go
index 04adc289..9dd69f90 100644
--- a/src/segments/path.go
+++ b/src/segments/path.go
@@ -73,6 +73,8 @@ const (
 	MaxWidth properties.Property = "max_width"
 	// Hides the root location if it doesn't fit in max_depth. Used in Agnoster Short
 	HideRootLocation properties.Property = "hide_root_location"
+	// A foreground color cycle
+	Cycle properties.Property = "cycle"
 )
 
 func (pt *Path) Template() string {
@@ -233,41 +235,39 @@ func (pt *Path) pathDepth(pwd string) int {
 }
 
 func (pt *Path) getAgnosterPath() string {
-	var buffer strings.Builder
 	folderIcon := pt.props.GetString(FolderIcon, "..")
-	separator := pt.getFolderSeparator()
-	elements := strings.Split(pt.relative, pt.env.PathSeparator())
-	if pt.root != pt.env.PathSeparator() {
-		elements = append([]string{pt.root}, elements...)
-	}
-	n := len(elements)
-	buffer.WriteString(elements[0])
-	for i := 2; i < n; i++ {
-		buffer.WriteString(fmt.Sprintf("%s%s", separator, folderIcon))
+	splitted := strings.Split(pt.relative, pt.env.PathSeparator())
+	if pt.root == pt.env.PathSeparator() {
+		pt.root = splitted[0]
+		splitted = splitted[1:]
 	}
-	if n > 1 {
-		buffer.WriteString(fmt.Sprintf("%s%s", separator, elements[n-1]))
+
+	var elements []string
+	n := len(splitted)
+	for i := 1; i < n; i++ {
+		elements = append(elements, folderIcon)
 	}
-	return buffer.String()
+	elements = append(elements, splitted[n-1])
+
+	return pt.colorizePath(pt.root, elements)
 }
 
 func (pt *Path) getAgnosterLeftPath() string {
-	var buffer strings.Builder
 	folderIcon := pt.props.GetString(FolderIcon, "..")
-	separator := pt.getFolderSeparator()
-	elements := strings.Split(pt.relative, pt.env.PathSeparator())
-	if pt.root != pt.env.PathSeparator() {
-		elements = append([]string{pt.root}, elements...)
-	}
-	n := len(elements)
-	buffer.WriteString(elements[0])
-	if n > 1 {
-		buffer.WriteString(fmt.Sprintf("%s%s", separator, elements[1]))
+	splitted := strings.Split(pt.relative, pt.env.PathSeparator())
+	if pt.root == pt.env.PathSeparator() {
+		pt.root = splitted[0]
+		splitted = splitted[1:]
 	}
-	for i := 2; i < n; i++ {
-		buffer.WriteString(fmt.Sprintf("%s%s", separator, folderIcon))
+
+	var elements []string
+	n := len(splitted)
+	elements = append(elements, splitted[0])
+	for i := 1; i < n; i++ {
+		elements = append(elements, folderIcon)
 	}
-	return buffer.String()
+
+	return pt.colorizePath(pt.root, elements)
 }
 
 func (pt *Path) getRelevantLetter(folder string) string {
@@ -284,43 +284,47 @@ func (pt *Path) getRelevantLetter(folder string) string {
 }
 
 func (pt *Path) getLetterPath() string {
-	var buffer strings.Builder
-	separator := pt.getFolderSeparator()
-	elements := strings.Split(pt.relative, pt.env.PathSeparator())
-	if pt.root != pt.env.PathSeparator() {
-		elements = append([]string{pt.root}, elements...)
+	splitted := strings.Split(pt.relative, pt.env.PathSeparator())
+	if pt.root == pt.env.PathSeparator() {
+		pt.root = splitted[0]
+		splitted = splitted[1:]
 	}
-	n := len(elements)
+	pt.root = pt.getRelevantLetter(pt.root)
+
+	var elements []string
+	n := len(splitted)
 	for i := 0; i < n-1; i++ {
-		letter := pt.getRelevantLetter(elements[i])
-		if i != 0 {
-			buffer.WriteString(separator)
-		}
-		buffer.WriteString(letter)
+		letter := pt.getRelevantLetter(splitted[i])
+		elements = append(elements, letter)
 	}
-	buffer.WriteString(fmt.Sprintf("%s%s", separator, elements[n-1]))
-	return buffer.String()
+	elements = append(elements, splitted[n-1])
+
+	return pt.colorizePath(pt.root, elements)
 }
 
 func (pt *Path) getUniqueLettersPath(maxWidth int) string {
-	var buffer strings.Builder
 	separator := pt.getFolderSeparator()
-	elements := strings.Split(pt.relative, pt.env.PathSeparator())
-	if pt.root != pt.env.PathSeparator() {
-		elements = append([]string{pt.root}, elements...)
+	splitted := strings.Split(pt.relative, pt.env.PathSeparator())
+	if pt.root == pt.env.PathSeparator() {
+		pt.root = splitted[0]
+		splitted = splitted[1:]
 	}
 
 	if maxWidth > 0 {
-		path := strings.Join(elements, separator)
+		path := strings.Join(splitted, separator)
 		if len(path) <= maxWidth {
-			return path
+			return pt.colorizePath(pt.root, splitted)
 		}
 	}
 
-	n := len(elements)
+	pt.root = pt.getRelevantLetter(pt.root)
+
+	var elements []string
+	n := len(splitted)
 	letters := make(map[string]bool)
+	letters[pt.root] = true
 	for i := 0; i < n-1; i++ {
-		folder := elements[i]
+		folder := splitted[i]
 		letter := pt.getRelevantLetter(folder)
 		for letters[letter] {
 			if letter == folder {
@@ -329,32 +333,33 @@ func (pt *Path) getUniqueLettersPath(maxWidth int) string {
 			letter += folder[len(letter) : len(letter)+1]
 		}
 		letters[letter] = true
-		if i != 0 {
-			buffer.WriteString(separator)
-		}
-		buffer.WriteString(letter)
+		elements = append(elements, letter)
 		// only return early on maxWidth > 0
 		// this enables the powerlevel10k behavior
 		if maxWidth > 0 {
-			trailing := strings.Join(elements[i+1:], separator)
-			leftover := maxWidth - buffer.Len() - len(trailing) - len(separator)
+			list := splitted[i+1:]
+			list = append(list, elements...)
+			current := strings.Join(list, separator)
+			leftover := maxWidth - len(current) - len(pt.root) - len(separator)
 			if leftover >= 0 {
-				buffer.WriteString(fmt.Sprintf("%s%s", separator, trailing))
-				return buffer.String()
+				elements = append(elements, strings.Join(splitted[i+1:], separator))
+				return pt.colorizePath(pt.root, elements)
 			}
 		}
 	}
-	buffer.WriteString(fmt.Sprintf("%s%s", separator, elements[n-1]))
-	return buffer.String()
+	elements = append(elements, splitted[n-1])
+
+	return pt.colorizePath(pt.root, elements)
 }
 
 func (pt *Path) getAgnosterFullPath() string {
-	path := strings.Trim(pt.relative, pt.env.PathSeparator())
-	path = pt.replaceFolderSeparators(path)
+	splitted := strings.Split(pt.relative, pt.env.PathSeparator())
 	if pt.root == pt.env.PathSeparator() {
-		return path
+		pt.root = splitted[0]
+		splitted = splitted[1:]
 	}
-	return pt.root + pt.getFolderSeparator() + path
+
+	return pt.colorizePath(pt.root, splitted)
 }
 
 func (pt *Path) getAgnosterShortPath() string {
@@ -372,11 +377,11 @@ func (pt *Path) getAgnosterShortPath() string {
 		return pt.getAgnosterFullPath()
 	}
 	pathSeparator := pt.env.PathSeparator()
-	folderSeparator := pt.getFolderSeparator()
 	rel := strings.TrimPrefix(pt.relative, pathSeparator)
 	splitted := strings.Split(rel, pathSeparator)
 	splitPos := pathDepth - maxDepth
-	var buffer strings.Builder
+	// var buffer strings.Builder
+	var elements []string
 	// unix root, needs to be replaced with the folder we're in at root level
 	root := pt.root
 	room := pathDepth - maxDepth
@@ -384,29 +389,24 @@ func (pt *Path) getAgnosterShortPath() string {
 		root = splitted[0]
 		room--
 	}
+
+	if hideRootLocation || room > 0 {
+		elements = append(elements, folderIcon)
+	}
+
 	if hideRootLocation {
-		buffer.WriteString(folderIcon)
-	} else {
-		buffer.WriteString(root)
-		if room > 0 {
-			buffer.WriteString(folderSeparator)
-			buffer.WriteString(folderIcon)
-		}
+		root = ""
 	}
+
 	for i := splitPos; i < pathDepth; i++ {
-		buffer.WriteString(fmt.Sprintf("%s%s", folderSeparator, splitted[i]))
+		elements = append(elements, splitted[i])
 	}
-	return buffer.String()
+	return pt.colorizePath(root, elements)
 }
 
 func (pt *Path) getFullPath() string {
-	rel := pt.relative
-	pathSeparator := pt.env.PathSeparator()
-	if len(pt.root) != 0 && pt.root != pathSeparator && !strings.HasSuffix(pt.root, pathSeparator) {
-		rel = pathSeparator + rel
-	}
-	path := pt.replaceFolderSeparators(rel)
-	return pt.root + path
+	elements := strings.Split(pt.relative, pt.env.PathSeparator())
+	return pt.colorizePath(pt.root, elements)
 }
 
 func (pt *Path) getFolderPath() string {
@@ -573,3 +573,31 @@ func (pt *Path) replaceFolderSeparators(pwd string) string {
 	pwd = strings.ReplaceAll(pwd, defaultSeparator, folderSeparator)
 	return pwd
 }
+
+func (pt *Path) colorizePath(root string, elements []string) string {
+	cycle := pt.props.GetStringArray(Cycle, []string{})
+	folderSeparator := pt.getFolderSeparator()
+
+	if len(cycle) != 0 {
+		colorize := "<%s>%s</>"
+
+		if len(root) != 0 {
+			root = fmt.Sprintf(colorize, cycle[0], root)
+			cycle = append(cycle[1:], cycle[0])
+		}
+
+		for i, element := range elements {
+			if len(element) == 0 {
+				continue
+			}
+			colored := fmt.Sprintf(colorize, cycle[0], element)
+			elements[i] = colored
+			cycle = append(cycle[1:], cycle[0])
+		}
+	}
+
+	if root == pt.env.PathSeparator() || len(root) == 0 {
+		return root + strings.Join(elements, folderSeparator)
+	}
+	return root + folderSeparator + strings.Join(elements, folderSeparator)
+}
diff --git a/website/docs/segments/path.mdx b/website/docs/segments/path.mdx
index cbf68695..83870cc6 100644
--- a/website/docs/segments/path.mdx
+++ b/website/docs/segments/path.mdx
@@ -10,36 +10,39 @@ Display the current path.
 
 ## Sample Configuration
 
-import Config from '@site/src/components/Config.js';
-
-<Config data={{
-  "type": "path",
-  "style": "powerline",
-  "powerline_symbol": "\uE0B0",
-  "foreground": "#ffffff",
-  "background": "#61AFEF",
-  "properties": {
-    "style": "folder",
-    "mapped_locations": {
-      "C:\\temp": "\ue799"
-    }
-  }
-}}/>
+import Config from "@site/src/components/Config.js";
+
+<Config
+  data={{
+    type: "path",
+    style: "powerline",
+    powerline_symbol: "\uE0B0",
+    foreground: "#ffffff",
+    background: "#61AFEF",
+    properties: {
+      style: "folder",
+      mapped_locations: {
+        "C:\\temp": "\ue799",
+      },
+    },
+  }}
+/>
 
 ## Properties
 
-| Name                        | Type      | Description                                                                                                                  |
-| --------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------- |
-| `folder_separator_icon`     | `string`  | the symbol to use as a separator between folders - defaults to platform path separator                                       |
-| `folder_separator_template` | `string`  | the [template][templates] to use as a separator between folders - defaults to ``                                             |
-| `home_icon`                 | `string`  | the icon to display when at `$HOME`, defaults to `~`                                                                         |
-| `folder_icon`               | `string`  | the icon to use as a folder indication - defaults to `..`                                                                    |
-| `windows_registry_icon`     | `string`  | the icon to display when in the Windows registry - defaults to `\uE0B1`                                                      |
-| `style`                     | `enum`    | how to display the current path                                                                                              |
-| `mixed_threshold`           | `number`  | the maximum length of a path segment that will be displayed when using `Mixed` - defaults to `4`                             |
-| `max_depth`                 | `number`  | maximum path depth to display before shortening when using `agnoster_short`, defaults to `1`                                 |
-| `max_width`                 | `number`  | maximum path length to display when using `powerlevel`, defaults to `0`                                                      |
-| `hide_root_location`        | `boolean` | hides the root location if it doesn't fit in the last `max_depth` folders, when using `agnoster_short` - defaults to `false` |
+| Name                        | Type       | Description                                                                                                                  |
+| --------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------- |
+| `folder_separator_icon`     | `string`   | the symbol to use as a separator between folders - defaults to platform path separator                                       |
+| `folder_separator_template` | `string`   | the [template][templates] to use as a separator between folders - defaults to ``                                             |
+| `home_icon`                 | `string`   | the icon to display when at `$HOME`, defaults to `~`                                                                         |
+| `folder_icon`               | `string`   | the icon to use as a folder indication - defaults to `..`                                                                    |
+| `windows_registry_icon`     | `string`   | the icon to display when in the Windows registry - defaults to `\uE0B1`                                                      |
+| `style`                     | `enum`     | how to display the current path                                                                                              |
+| `mixed_threshold`           | `number`   | the maximum length of a path segment that will be displayed when using `Mixed` - defaults to `4`                             |
+| `max_depth`                 | `number`   | maximum path depth to display before shortening when using `agnoster_short`, defaults to `1`                                 |
+| `max_width`                 | `number`   | maximum path length to display when using `powerlevel`, defaults to `0`                                                      |
+| `hide_root_location`        | `boolean`  | hides the root location if it doesn't fit in the last `max_depth` folders, when using `agnoster_short` - defaults to `false` |
+| `cycle`                     | `[]string` | a list of foreground colors to cycle through to colorize the individual path folders                                         |
 
 ## Mapped Locations
 


Test patch:
diff --git a/src/segments/path_test.go b/src/segments/path_test.go
index e665e9c9..f2ca3bce 100644
--- a/src/segments/path_test.go
+++ b/src/segments/path_test.go
@@ -956,6 +956,7 @@ func TestAgnosterPath(t *testing.T) {
 		PWD           string
 		GOOS          string
 		PathSeparator string
+		Cycle         []string
 	}{
 		{
 			Case:          "Windows registry drive case sensitive",
@@ -1073,9 +1074,17 @@ func TestAgnosterPath(t *testing.T) {
 			PWD:           "/mnt/folder/location",
 			PathSeparator: "/",
 		},
+		{
+			Case:          "Unix, colorize",
+			Expected:      "<blue>mnt</> > <yellow>f</> > <blue>location</>",
+			Home:          homeDir,
+			PWD:           "/mnt/folder/location",
+			PathSeparator: "/",
+			Cycle:         []string{"blue", "yellow"},
+		},
 	}
 
-	for _, tc := range cases { //nolint:dupl
+	for _, tc := range cases {
 		env := new(mock.MockedEnvironment)
 		env.On("Home").Return(tc.Home)
 		env.On("PathSeparator").Return(tc.PathSeparator)
@@ -1093,6 +1102,7 @@ func TestAgnosterPath(t *testing.T) {
 				FolderSeparatorIcon: " > ",
 				FolderIcon:          "f",
 				HomeIcon:            "~",
+				Cycle:               tc.Cycle,
 			},
 		}
 		path.setPaths()
@@ -1229,7 +1239,7 @@ func TestAgnosterLeftPath(t *testing.T) {
 		},
 	}
 
-	for _, tc := range cases { //nolint:dupl
+	for _, tc := range cases {
 		env := new(mock.MockedEnvironment)
 		env.On("Home").Return(tc.Home)
 		env.On("PathSeparator").Return(tc.PathSeparator)