diff --git a/src/segments/path.go b/src/segments/path.go index b6a67b78b05f..9a626254ee90 100644 --- a/src/segments/path.go +++ b/src/segments/path.go @@ -25,6 +25,7 @@ type Path struct { Location string Writable bool RootDir bool + Folders Folders } const ( @@ -48,8 +49,8 @@ const ( Short string = "short" // Full displays the full path Full string = "full" - // Folder displays the current folder - Folder string = "folder" + // FolderType displays the current folder + FolderType string = "folder" // Mixed like agnoster, but if a folder name is short enough, it is displayed as-is Mixed string = "mixed" // Letter like agnoster, but with the first letter of each folder name @@ -81,6 +82,8 @@ const ( FolderFormat properties.Property = "folder_format" // format to use on the first and last folder of the path EdgeFormat properties.Property = "edge_format" + // GitDirFormat format to use on the git directory + GitDirFormat properties.Property = "gitdir_format" ) func (pt *Path) Template() string { @@ -105,25 +108,34 @@ func (pt *Path) Enabled() bool { } func (pt *Path) setPaths() { + defer func() { + pt.Folders = pt.splitPath() + }() + 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 } + if len(pt.pwd) == 0 { return } + // ensure a clean path pt.root, pt.relative = pt.replaceMappedLocations() + // this is a full replacement of the parent if len(pt.root) == 0 { pt.pwd = pt.relative return } + pathSeparator := pt.env.PathSeparator() if !strings.HasSuffix(pt.root, pathSeparator) && len(pt.relative) > 0 { pt.pwd = pt.root + pathSeparator + pt.relative return } + pt.pwd = pt.root + pt.relative } @@ -173,7 +185,7 @@ func (pt *Path) setStyle() { fallthrough case Full: pt.Path = pt.getFullPath() - case Folder: + case FolderType: pt.Path = pt.getFolderPath() case Powerlevel: maxWidth := int(pt.props.GetFloat64(MaxWidth, 0)) @@ -213,15 +225,17 @@ func (pt *Path) getMixedPath() string { threshold := int(pt.props.GetFloat64(MixedThreshold, 4)) 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...) + pt.Folders = append(Folders{{Name: pt.root}}, pt.Folders...) } - n := len(elements) - buffer.WriteString(elements[0]) + + n := len(pt.Folders) + buffer.WriteString(pt.Folders[0].Name) + for i := 1; i < n; i++ { - folder := elements[i] - if len(folder) > threshold && i != n-1 { + folder := pt.Folders[i].Name + if len(folder) > threshold && i != n-1 && !pt.Folders[i].Display { folder = folderIcon } buffer.WriteString(fmt.Sprintf("%s%s", separator, folder)) @@ -229,33 +243,27 @@ func (pt *Path) getMixedPath() string { return buffer.String() } -func (pt *Path) pathDepth(pwd string) int { - splitted := strings.Split(pwd, pt.env.PathSeparator()) - depth := 0 - for _, part := range splitted { - if part != "" { - depth++ - } - } - return depth -} - func (pt *Path) getAgnosterPath() string { folderIcon := pt.props.GetString(FolderIcon, "..") - splitted := strings.Split(pt.relative, pt.env.PathSeparator()) + if pt.root == pt.env.PathSeparator() { - pt.root = splitted[0] - splitted = splitted[1:] + pt.root = pt.Folders[0].Name + pt.Folders = pt.Folders[1:] } var elements []string - n := len(splitted) + n := len(pt.Folders) for i := 1; i < n; i++ { + if pt.Folders[i].Display { + elements = append(elements, pt.Folders[i].Name) + continue + } + elements = append(elements, folderIcon) } - if len(splitted) > 0 { - elements = append(elements, splitted[n-1]) + if len(pt.Folders) > 0 { + elements = append(elements, pt.Folders[n-1].Name) } return pt.colorizePath(pt.root, elements) @@ -263,52 +271,66 @@ func (pt *Path) getAgnosterPath() string { func (pt *Path) getAgnosterLeftPath() string { folderIcon := pt.props.GetString(FolderIcon, "..") - splitted := strings.Split(pt.relative, pt.env.PathSeparator()) + if pt.root == pt.env.PathSeparator() { - pt.root = splitted[0] - splitted = splitted[1:] + pt.root = pt.Folders[0].Name + pt.Folders = pt.Folders[1:] } var elements []string - n := len(splitted) - elements = append(elements, splitted[0]) + n := len(pt.Folders) + elements = append(elements, pt.Folders[0].Name) for i := 1; i < n; i++ { + if pt.Folders[i].Display { + elements = append(elements, pt.Folders[i].Name) + continue + } + elements = append(elements, folderIcon) } return pt.colorizePath(pt.root, elements) } -func (pt *Path) getRelevantLetter(folder string) string { +func (pt *Path) getRelevantLetter(folder *Folder) string { + if folder.Display { + return folder.Name + } + // check if there is at least a letter we can use - matches := regex.FindNamedRegexMatch(`(?P[\p{L}0-9]).*`, folder) + matches := regex.FindNamedRegexMatch(`(?P[\p{L}0-9]).*`, folder.Name) if matches == nil || len(matches["letter"]) == 0 { // no letter found, keep the folder unchanged - return folder + return folder.Name } letter := matches["letter"] // handle non-letter characters before the first found letter - letter = folder[0:strings.Index(folder, letter)] + letter + letter = folder.Name[0:strings.Index(folder.Name, letter)] + letter return letter } func (pt *Path) getLetterPath() string { - splitted := strings.Split(pt.relative, pt.env.PathSeparator()) if pt.root == pt.env.PathSeparator() { - pt.root = splitted[0] - splitted = splitted[1:] + pt.root = pt.Folders[0].Name + pt.Folders = pt.Folders[1:] } - pt.root = pt.getRelevantLetter(pt.root) + + pt.root = pt.getRelevantLetter(&Folder{Name: pt.root}) var elements []string - n := len(splitted) + n := len(pt.Folders) for i := 0; i < n-1; i++ { - letter := pt.getRelevantLetter(splitted[i]) + if pt.Folders[i].Display { + elements = append(elements, pt.Folders[i].Name) + continue + } + + letter := pt.getRelevantLetter(pt.Folders[i]) elements = append(elements, letter) } - if len(splitted) > 0 { - elements = append(elements, splitted[n-1]) + if len(pt.Folders) > 0 { + elements = append(elements, pt.Folders[n-1].Name) } return pt.colorizePath(pt.root, elements) @@ -316,98 +338,101 @@ func (pt *Path) getLetterPath() string { func (pt *Path) getUniqueLettersPath(maxWidth int) string { separator := pt.getFolderSeparator() - splitted := strings.Split(pt.relative, pt.env.PathSeparator()) if pt.root == pt.env.PathSeparator() { - pt.root = splitted[0] - splitted = splitted[1:] + pt.root = pt.Folders[0].Name + pt.Folders = pt.Folders[1:] } if maxWidth > 0 { - path := strings.Join(splitted, separator) + path := strings.Join(pt.Folders.List(), separator) if len(path) <= maxWidth { - return pt.colorizePath(pt.root, splitted) + return pt.colorizePath(pt.root, pt.Folders.List()) } } - pt.root = pt.getRelevantLetter(pt.root) + pt.root = pt.getRelevantLetter(&Folder{Name: pt.root}) var elements []string - n := len(splitted) + n := len(pt.Folders) letters := make(map[string]bool) letters[pt.root] = true for i := 0; i < n-1; i++ { - folder := splitted[i] - letter := pt.getRelevantLetter(folder) + folder := pt.Folders[i].Name + letter := pt.getRelevantLetter(pt.Folders[i]) + for letters[letter] { if letter == folder { break } letter += folder[len(letter) : len(letter)+1] } + letters[letter] = true elements = append(elements, letter) + // only return early on maxWidth > 0 // this enables the powerlevel10k behavior if maxWidth > 0 { - list := splitted[i+1:] + list := pt.Folders[i+1:].List() list = append(list, elements...) current := strings.Join(list, separator) leftover := maxWidth - len(current) - len(pt.root) - len(separator) if leftover >= 0 { - elements = append(elements, strings.Join(splitted[i+1:], separator)) + elements = append(elements, strings.Join(pt.Folders[i+1:].List(), separator)) return pt.colorizePath(pt.root, elements) } } } - if len(splitted) > 0 { - elements = append(elements, splitted[n-1]) + if len(pt.Folders) > 0 { + elements = append(elements, pt.Folders[n-1].Name) } return pt.colorizePath(pt.root, elements) } func (pt *Path) getAgnosterFullPath() string { - splitted := strings.Split(pt.relative, pt.env.PathSeparator()) if pt.root == pt.env.PathSeparator() { - pt.root = splitted[0] - splitted = splitted[1:] + pt.root = pt.Folders[0].Name + pt.Folders = pt.Folders[1:] } - return pt.colorizePath(pt.root, splitted) + return pt.colorizePath(pt.root, pt.Folders.List()) } func (pt *Path) getAgnosterShortPath() string { - pathDepth := pt.pathDepth(pt.relative) + pathDepth := len(pt.Folders) + maxDepth := pt.props.GetInt(MaxDepth, 1) if maxDepth < 1 { maxDepth = 1 } + folderIcon := pt.props.GetString(FolderIcon, "..") hideRootLocation := pt.props.GetBool(HideRootLocation, false) + if pathDepth <= maxDepth { if hideRootLocation { pt.root = folderIcon } return pt.getAgnosterFullPath() } + pathSeparator := pt.env.PathSeparator() - rel := strings.TrimPrefix(pt.relative, pathSeparator) - splitted := strings.Split(rel, pathSeparator) splitPos := pathDepth - maxDepth - // var buffer strings.Builder - var elements []string + + 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 { - root = splitted[0] + root = pt.Folders[0].Name room-- } if hideRootLocation || room > 0 { - elements = append(elements, folderIcon) + folders = append(folders, folderIcon) } if hideRootLocation { @@ -415,14 +440,14 @@ func (pt *Path) getAgnosterShortPath() string { } for i := splitPos; i < pathDepth; i++ { - elements = append(elements, splitted[i]) + folders = append(folders, pt.Folders[i].Name) } - return pt.colorizePath(root, elements) + + return pt.colorizePath(root, folders) } func (pt *Path) getFullPath() string { - elements := strings.Split(pt.relative, pt.env.PathSeparator()) - return pt.colorizePath(pt.root, elements) + return pt.colorizePath(pt.root, pt.Folders.List()) } func (pt *Path) getFolderPath() string { @@ -649,3 +674,69 @@ func (pt *Path) colorizePath(root string, elements []string) string { return builder.String() } + +type Folder struct { + Name string + Display bool + Path string +} + +type Folders []*Folder + +func (f Folders) List() []string { + var list []string + + for _, folder := range f { + list = append(list, folder.Name) + } + + return list +} + +func (pt *Path) splitPath() Folders { + result := Folders{} + folders := []string{} + + if len(pt.relative) != 0 { + folders = strings.Split(pt.relative, pt.env.PathSeparator()) + } + + folderFormatMap := pt.makeFolderFormatMap() + + currentPath := pt.root + if currentPath == "~" { + currentPath = pt.env.Home() + pt.env.PathSeparator() + } + + var display bool + + for _, folder := range folders { + currentPath += folder + + if format := folderFormatMap[currentPath]; len(format) != 0 { + folder = fmt.Sprintf(format, folder) + display = true + } + + result = append(result, &Folder{Name: folder, Path: currentPath, Display: display}) + + currentPath += pt.env.PathSeparator() + + display = false + } + + return result +} + +func (pt *Path) makeFolderFormatMap() map[string]string { + folderFormatMap := make(map[string]string) + + if gitDirFormat := pt.props.GetString(GitDirFormat, ""); len(gitDirFormat) != 0 { + dir, err := pt.env.HasParentFilePath(".git") + if err == nil && dir.IsDir { + folderFormatMap[dir.ParentFolder] = gitDirFormat + } + } + + return folderFormatMap +} diff --git a/src/segments/path_test.go b/src/segments/path_test.go index 8decb2fdd32c..b4854293d87f 100644 --- a/src/segments/path_test.go +++ b/src/segments/path_test.go @@ -787,21 +787,21 @@ func TestFullAndFolderPath(t *testing.T) { {Style: Full, FolderSeparatorIcon: "|", Pwd: homeDir + abc, Expected: "~|abc"}, {Style: Full, FolderSeparatorIcon: "|", Pwd: abcd, Expected: "/a|b|c|d"}, - {Style: Folder, Pwd: "/", Expected: "/"}, - {Style: Folder, Pwd: homeDir, Expected: "~"}, - {Style: Folder, Pwd: homeDir, Expected: "someone", DisableMappedLocations: true}, - {Style: Folder, Pwd: homeDir + abc, Expected: "abc"}, - {Style: Folder, Pwd: abcd, Expected: "d"}, + {Style: FolderType, Pwd: "/", Expected: "/"}, + {Style: FolderType, Pwd: homeDir, Expected: "~"}, + {Style: FolderType, Pwd: homeDir, Expected: "someone", DisableMappedLocations: true}, + {Style: FolderType, Pwd: homeDir + abc, Expected: "abc"}, + {Style: FolderType, Pwd: abcd, Expected: "d"}, - {Style: Folder, FolderSeparatorIcon: "|", Pwd: "/", Expected: "/"}, - {Style: Folder, FolderSeparatorIcon: "|", Pwd: homeDir, Expected: "~"}, - {Style: Folder, FolderSeparatorIcon: "|", Pwd: homeDir, Expected: "someone", DisableMappedLocations: true}, - {Style: Folder, FolderSeparatorIcon: "|", Pwd: homeDir + abc, Expected: "abc"}, - {Style: Folder, FolderSeparatorIcon: "|", Pwd: abcd, Expected: "d"}, + {Style: FolderType, FolderSeparatorIcon: "|", Pwd: "/", Expected: "/"}, + {Style: FolderType, FolderSeparatorIcon: "|", Pwd: homeDir, Expected: "~"}, + {Style: FolderType, FolderSeparatorIcon: "|", Pwd: homeDir, Expected: "someone", DisableMappedLocations: true}, + {Style: FolderType, FolderSeparatorIcon: "|", Pwd: homeDir + abc, Expected: "abc"}, + {Style: FolderType, FolderSeparatorIcon: "|", Pwd: abcd, Expected: "d"}, // for Windows paths - {Style: Folder, FolderSeparatorIcon: "\\", Pwd: "C:\\", Expected: "C:\\", PathSeparator: "\\", GOOS: platform.WINDOWS}, - {Style: Folder, FolderSeparatorIcon: "\\", Pwd: homeDirWindows, Expected: "~", PathSeparator: "\\", GOOS: platform.WINDOWS}, + {Style: FolderType, FolderSeparatorIcon: "\\", Pwd: "C:\\", Expected: "C:\\", PathSeparator: "\\", GOOS: platform.WINDOWS}, + {Style: FolderType, FolderSeparatorIcon: "\\", Pwd: homeDirWindows, Expected: "~", PathSeparator: "\\", GOOS: platform.WINDOWS}, {Style: Full, FolderSeparatorIcon: "\\", Pwd: homeDirWindows, Expected: "~", PathSeparator: "\\", GOOS: platform.WINDOWS}, {Style: Full, FolderSeparatorIcon: "\\", Pwd: homeDirWindows + "\\abc", Expected: "~\\abc", PathSeparator: "\\", GOOS: platform.WINDOWS}, {Style: Full, FolderSeparatorIcon: "\\", Pwd: "C:\\Users\\posh", Expected: "C:\\Users\\posh", PathSeparator: "\\", GOOS: platform.WINDOWS}, @@ -994,7 +994,7 @@ func TestFolderPathCustomMappedLocations(t *testing.T) { path := &Path{ env: env, props: properties.Map{ - properties.Style: Folder, + properties.Style: FolderType, MappedLocations: map[string]string{ abcd: "#", }, @@ -1502,3 +1502,53 @@ func TestReplaceMappedLocations(t *testing.T) { assert.Equal(t, tc.Expected, path.pwd) } } + +func TestSplitPath(t *testing.T) { + cases := []struct { + Case string + Relative string + Root string + GitDir *platform.FileInfo + GitDirFormat string + Expected Folders + }{ + {Case: "Root directory", Root: "/", Expected: Folders{}}, + { + Case: "Regular directory", + Root: "/", + Relative: "c/d", + Expected: Folders{ + {Name: "c", Path: "/c"}, + {Name: "d", Path: "/c/d"}, + }, + }, + { + Case: "Home directory - git folder", + Root: "~", + Relative: "c/d", + GitDir: &platform.FileInfo{IsDir: true, ParentFolder: "/a/b/c"}, + GitDirFormat: "%s", + Expected: Folders{ + {Name: "c", Path: "/a/b/c", Display: true}, + {Name: "d", Path: "/a/b/c/d"}, + }, + }, + } + + 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) + path := &Path{ + env: env, + props: properties.Map{ + GitDirFormat: tc.GitDirFormat, + }, + root: tc.Root, + relative: tc.Relative, + } + got := path.splitPath() + assert.Equal(t, tc.Expected, got, tc.Case) + } +} diff --git a/website/docs/segments/path.mdx b/website/docs/segments/path.mdx index 84c2ad1ad911..648e7e37ec39 100644 --- a/website/docs/segments/path.mdx +++ b/website/docs/segments/path.mdx @@ -40,12 +40,13 @@ import Config from "@site/src/components/Config.js"; | `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` | +| `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 color overrides to cycle through to colorize the individual path folders, e.g. `[ "#ffffff,#111111" ]` | | `cycle_folder_separator` | `boolean` | colorize the `folder_separator_icon` as well when using a cycle - defaults to `false` | | `folder_format` | `string` | format to use on individual path folders - defaults to `%s` | | `edge_format` | `string` | format to use on the first and last folder of the path - defaults to `%s` | +| `gitdir_format` | `string` | format to use for a git root directory - defaults to `` | ## Mapped Locations