fix(path): improve path cleaning, normalization and parsing
Some checks failed
Code QL / code-ql (push) Waiting to run
Release / changelog (push) Waiting to run
Release / artifacts (push) Blocked by required conditions
Azure Static Web Apps CI/CD / Build and Deploy (push) Has been cancelled

This commit is contained in:
L. Yeung 2024-08-29 22:00:16 +08:00 committed by Jan De Dobbeleer
parent 476bfd1fff
commit abd6676c5b
6 changed files with 511 additions and 378 deletions

View file

@ -171,7 +171,7 @@ func (e *Engine) getTitleTemplateText() string {
}
func (e *Engine) renderBlock(block *config.Block, cancelNewline bool) bool {
defer e.patchPowerShellBleed()
defer e.applyPowerShellBleedPatch()
// This is deprecated but we leave it in to not break configs
// It is encouraged to use "newline": true on block level
@ -267,7 +267,7 @@ func (e *Engine) renderBlock(block *config.Block, cancelNewline bool) bool {
return true
}
func (e *Engine) patchPowerShellBleed() {
func (e *Engine) applyPowerShellBleedPatch() {
// when in PowerShell, we need to clear the line after the prompt
// to avoid the background being printed on the next line
// when at the end of the buffer.
@ -514,10 +514,6 @@ func New(flags *runtime.Flags) *Engine {
env.Init()
cfg := config.Load(env)
if cfg.PatchPwshBleed {
patchPowerShellBleed(env.Shell(), flags)
}
env.Var = cfg.Var
flags.HasTransient = cfg.TransientPrompt != nil
@ -532,10 +528,16 @@ func New(flags *runtime.Flags) *Engine {
Plain: flags.Plain,
}
if cfg.PatchPwshBleed {
eng.patchPowerShellBleed()
}
return eng
}
func patchPowerShellBleed(sh string, flags *runtime.Flags) {
func (e *Engine) patchPowerShellBleed() {
sh := e.Env.Shell()
// when in PowerShell, and force patching the bleed bug
// we need to reduce the terminal width by 1 so the last
// character isn't cut off by the ANSI escape sequences
@ -544,10 +546,12 @@ func patchPowerShellBleed(sh string, flags *runtime.Flags) {
return
}
// only do this when relevant
if flags.TerminalWidth <= 0 {
// Since the terminal width may not be given by the CLI flag, we should always call this here.
_, err := e.Env.TerminalWidth()
if err != nil {
// Skip when we're unable to determine the terminal width.
return
}
flags.TerminalWidth--
e.Env.Flags().TerminalWidth--
}

View file

@ -173,25 +173,20 @@ func (term *Terminal) Pwd() string {
if term.cwd != "" {
return term.cwd
}
correctPath := func(pwd string) string {
if term.GOOS() != WINDOWS {
return pwd
}
// on Windows, and being case sensitive and not consistent and all, this gives silly issues
driveLetter := regex.GetCompiledRegex(`^[a-z]:`)
return driveLetter.ReplaceAllStringFunc(pwd, strings.ToUpper)
}
if term.CmdFlags != nil && term.CmdFlags.PWD != "" {
term.cwd = correctPath(term.CmdFlags.PWD)
term.cwd = CleanPath(term, term.CmdFlags.PWD)
term.Debug(term.cwd)
return term.cwd
}
dir, err := os.Getwd()
if err != nil {
term.Error(err)
return ""
}
term.cwd = correctPath(dir)
term.cwd = CleanPath(term, dir)
term.Debug(term.cwd)
return term.cwd
}
@ -321,7 +316,10 @@ func (term *Terminal) LsDir(path string) []fs.DirEntry {
func (term *Terminal) PathSeparator() string {
defer term.Trace(time.Now())
return string(os.PathSeparator)
if term.GOOS() == WINDOWS {
return `\`
}
return "/"
}
func (term *Terminal) User() string {
@ -882,6 +880,54 @@ func Base(env Environment, path string) string {
return path
}
func CleanPath(env Environment, path string) string {
if len(path) == 0 {
return path
}
cleaned := path
separator := env.PathSeparator()
// The prefix can be empty for a relative path.
var prefix string
if IsPathSeparator(env, cleaned[0]) {
prefix = separator
}
if env.GOOS() == WINDOWS {
// Normalize (forward) slashes to backslashes on Windows.
cleaned = strings.ReplaceAll(cleaned, "/", `\`)
// Clean the prefix for a UNC path, if any.
if regex.MatchString(`^\\{2}[^\\]+`, cleaned) {
cleaned = strings.TrimPrefix(cleaned, `\\.\UNC\`)
if len(cleaned) == 0 {
return cleaned
}
prefix = `\\`
}
// Always use an uppercase drive letter on Windows.
driveLetter := regex.GetCompiledRegex(`^[a-z]:`)
cleaned = driveLetter.ReplaceAllStringFunc(cleaned, strings.ToUpper)
}
sb := new(strings.Builder)
sb.WriteString(prefix)
// Clean slashes.
matches := regex.FindAllNamedRegexMatch(fmt.Sprintf(`(?P<element>[^\%s]+)`, separator), cleaned)
n := len(matches) - 1
for i, m := range matches {
sb.WriteString(m["element"])
if i != n {
sb.WriteString(separator)
}
}
return sb.String()
}
func ReplaceTildePrefixWithHomeDir(env Environment, path string) string {
if !strings.HasPrefix(path, "~") {
return path

View file

@ -13,23 +13,44 @@ import (
"github.com/jandedobbeleer/oh-my-posh/src/template"
)
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
}
type Path struct {
props properties.Properties
env runtime.Environment
root string
relative string
pwd string
cygPath bool
windowsPath bool
pathSeparator string
pwd string
root string
relative string
folders Folders
// After `setPaths` is called, the above 4 fields should remain unchanged to preserve the original path info.
cygPath bool
windowsPath bool
pathSeparator string
mappedLocations map[string]string
Path string
StackCount int
Location string
Writable bool
RootDir bool
Folders Folders
}
const (
@ -121,7 +142,7 @@ func (pt *Path) Enabled() bool {
func (pt *Path) setPaths() {
defer func() {
pt.Folders = pt.splitPath()
pt.folders = pt.splitPath()
}()
displayCygpath := func() bool {
@ -147,33 +168,33 @@ func (pt *Path) setPaths() {
}
// 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
}
if !strings.HasSuffix(pt.root, pt.pathSeparator) && len(pt.relative) > 0 {
pt.pwd = pt.root + pt.pathSeparator + pt.relative
return
}
pt.pwd = pt.root + pt.relative
pt.root, pt.relative = pt.replaceMappedLocations(pt.pwd)
pt.pwd = pt.join(pt.root, pt.relative)
}
func (pt *Path) Parent() string {
if len(pt.pwd) == 0 {
return ""
}
if len(pt.relative) == 0 {
// a root path has no parent
folders := pt.folders.List()
if len(folders) == 0 {
// No parent.
return ""
}
base := runtime.Base(pt.env, pt.pwd)
path := pt.replaceFolderSeparators(pt.pwd[:len(pt.pwd)-len(base)])
return path
sb := new(strings.Builder)
folderSeparator := pt.getFolderSeparator()
sb.WriteString(pt.root)
if !pt.endWithSeparator(pt.root) {
sb.WriteString(folderSeparator)
}
for _, folder := range folders[:len(folders)-1] {
sb.WriteString(folder)
sb.WriteString(folderSeparator)
}
return sb.String()
}
func (pt *Path) Init(props properties.Properties, env runtime.Environment) {
@ -183,12 +204,14 @@ func (pt *Path) Init(props properties.Properties, env runtime.Environment) {
func (pt *Path) setStyle() {
if len(pt.relative) == 0 {
root := pt.root
// Only append a separator to a non-filesystem PSDrive root or a Windows drive root.
if (len(pt.env.Flags().PSWD) != 0 || pt.windowsPath) && strings.HasSuffix(pt.root, ":") {
pt.root += pt.getFolderSeparator()
if (len(pt.env.Flags().PSWD) != 0 || pt.windowsPath) && strings.HasSuffix(root, ":") {
root += pt.getFolderSeparator()
}
pt.Path = pt.colorizePath(pt.root, nil)
pt.Path = pt.colorizePath(root, nil)
return
}
@ -276,75 +299,77 @@ func (pt *Path) getFolderSeparator() string {
}
func (pt *Path) getMixedPath() string {
root := pt.root
folders := pt.folders
threshold := int(pt.props.GetFloat64(MixedThreshold, 4))
folderIcon := pt.props.GetString(FolderIcon, "..")
if pt.root == pt.pathSeparator {
pt.root = pt.Folders[0].Name
pt.Folders = pt.Folders[1:]
if pt.isRootFS(root) {
root = folders[0].Name
folders = folders[1:]
}
var folders []string
var elements []string
for i, n := 0, len(pt.Folders); i < n; i++ {
folder := pt.Folders[i].Name
if len(folder) > threshold && i != n-1 && !pt.Folders[i].Display {
folder = folderIcon
for i, n := 0, len(folders); i < n; i++ {
folderName := folders[i].Name
if len(folderName) > threshold && i != n-1 && !folders[i].Display {
elements = append(elements, folderIcon)
continue
}
folders = append(folders, folder)
elements = append(elements, folderName)
}
return pt.colorizePath(pt.root, folders)
return pt.colorizePath(root, elements)
}
func (pt *Path) getAgnosterPath() string {
root := pt.root
folders := pt.folders
folderIcon := pt.props.GetString(FolderIcon, "..")
if pt.root == pt.pathSeparator {
pt.root = pt.Folders[0].Name
pt.Folders = pt.Folders[1:]
if pt.isRootFS(root) {
root = folders[0].Name
folders = folders[1:]
}
var elements []string
n := len(pt.Folders)
for i := 0; i < n-1; i++ {
name := folderIcon
if pt.Folders[i].Display {
name = pt.Folders[i].Name
}
elements = append(elements, name)
}
if len(pt.Folders) > 0 {
elements = append(elements, pt.Folders[n-1].Name)
}
return pt.colorizePath(pt.root, elements)
}
func (pt *Path) getAgnosterLeftPath() string {
folderIcon := pt.props.GetString(FolderIcon, "..")
if pt.root == pt.pathSeparator {
pt.root = pt.Folders[0].Name
pt.Folders = pt.Folders[1:]
}
var elements []string
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)
for i, n := 0, len(folders); i < n; i++ {
if folders[i].Display || i == n-1 {
elements = append(elements, folders[i].Name)
continue
}
elements = append(elements, folderIcon)
}
return pt.colorizePath(pt.root, elements)
return pt.colorizePath(root, elements)
}
func (pt *Path) getAgnosterLeftPath() string {
root := pt.root
folders := pt.folders
folderIcon := pt.props.GetString(FolderIcon, "..")
if pt.isRootFS(root) {
root = folders[0].Name
folders = folders[1:]
}
var elements []string
elements = append(elements, folders[0].Name)
for i, n := 1, len(folders); i < n; i++ {
if folders[i].Display {
elements = append(elements, folders[i].Name)
continue
}
elements = append(elements, folderIcon)
}
return pt.colorizePath(root, elements)
}
func (pt *Path) getRelevantLetter(folder *Folder) string {
@ -365,62 +390,78 @@ func (pt *Path) getRelevantLetter(folder *Folder) string {
}
func (pt *Path) getLetterPath() string {
if pt.root == pt.pathSeparator {
pt.root = pt.Folders[0].Name
pt.Folders = pt.Folders[1:]
root := pt.root
folders := pt.folders
if pt.isRootFS(root) {
root = folders[0].Name
folders = folders[1:]
}
pt.root = pt.getRelevantLetter(&Folder{Name: pt.root})
root = pt.getRelevantLetter(&Folder{Name: root})
var elements []string
n := len(pt.Folders)
for i := 0; i < n-1; i++ {
if pt.Folders[i].Display {
elements = append(elements, pt.Folders[i].Name)
for i, n := 0, len(folders); i < n; i++ {
if folders[i].Display || i == n-1 {
elements = append(elements, folders[i].Name)
continue
}
letter := pt.getRelevantLetter(pt.Folders[i])
letter := pt.getRelevantLetter(folders[i])
elements = append(elements, letter)
}
if len(pt.Folders) > 0 {
elements = append(elements, pt.Folders[n-1].Name)
}
return pt.colorizePath(pt.root, elements)
return pt.colorizePath(root, elements)
}
func (pt *Path) getUniqueLettersPath(maxWidth int) string {
root := pt.root
folders := pt.folders
separator := pt.getFolderSeparator()
if pt.root == pt.pathSeparator {
pt.root = pt.Folders[0].Name
pt.Folders = pt.Folders[1:]
if pt.isRootFS(root) {
root = folders[0].Name
folders = folders[1:]
}
folderNames := folders.List()
usePowerlevelStyle := func(root, relative string) bool {
length := len(root) + len(relative)
if !pt.endWithSeparator(root) {
length += len(separator)
}
return length <= maxWidth
}
if maxWidth > 0 {
path := strings.Join(pt.Folders.List(), separator)
if len(path) <= maxWidth {
return pt.colorizePath(pt.root, pt.Folders.List())
relative := strings.Join(folderNames, separator)
if usePowerlevelStyle(root, relative) {
return pt.colorizePath(root, folderNames)
}
}
pt.root = pt.getRelevantLetter(&Folder{Name: pt.root})
root = pt.getRelevantLetter(&Folder{Name: root})
var elements []string
n := len(pt.Folders)
letters := make(map[string]bool)
letters[pt.root] = true
for i := 0; i < n-1; i++ {
folder := pt.Folders[i].Name
letter := pt.getRelevantLetter(pt.Folders[i])
letters[root] = true
for i, n := 0, len(folders); i < n; i++ {
folderName := folderNames[i]
if i == n-1 {
elements = append(elements, folderName)
break
}
letter := pt.getRelevantLetter(folders[i])
for letters[letter] {
if letter == folder {
if letter == folderName {
break
}
letter += folder[len(letter) : len(letter)+1]
letter += folderName[len(letter) : len(letter)+1]
}
letters[letter] = true
@ -429,87 +470,95 @@ func (pt *Path) getUniqueLettersPath(maxWidth int) string {
// only return early on maxWidth > 0
// this enables the powerlevel10k behavior
if maxWidth > 0 {
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(pt.Folders[i+1:].List(), separator))
return pt.colorizePath(pt.root, elements)
list := elements
list = append(list, folderNames[i+1:]...)
relative := strings.Join(list, separator)
if usePowerlevelStyle(root, relative) {
return pt.colorizePath(root, list)
}
}
}
if len(pt.Folders) > 0 {
elements = append(elements, pt.Folders[n-1].Name)
}
return pt.colorizePath(pt.root, elements)
return pt.colorizePath(root, elements)
}
func (pt *Path) getAgnosterFullPath() string {
if pt.root == pt.pathSeparator {
pt.root = pt.Folders[0].Name
pt.Folders = pt.Folders[1:]
root := pt.root
folders := pt.folders
if pt.isRootFS(root) {
root = folders[0].Name
folders = folders[1:]
}
return pt.colorizePath(pt.root, pt.Folders.List())
return pt.colorizePath(root, folders.List())
}
func (pt *Path) getAgnosterShortPath() string {
pathDepth := len(pt.Folders)
root := pt.root
folders := pt.folders
if pt.isRootFS(root) {
root = folders[0].Name
folders = folders[1:]
}
maxDepth := pt.props.GetInt(MaxDepth, 1)
if maxDepth < 1 {
maxDepth = 1
}
folderIcon := pt.props.GetString(FolderIcon, "..")
pathDepth := len(folders)
hideRootLocation := pt.props.GetBool(HideRootLocation, false)
folderIcon := pt.props.GetString(FolderIcon, "..")
if pathDepth <= maxDepth {
if hideRootLocation {
pt.root = folderIcon
}
// No need to shorten.
if pathDepth < maxDepth || (pathDepth == maxDepth && !hideRootLocation) {
return pt.getAgnosterFullPath()
}
splitPos := pathDepth - maxDepth
elements := []string{folderIcon}
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 == pt.pathSeparator {
root = pt.Folders[0].Name
room--
}
if hideRootLocation || room > 0 {
folders = append(folders, folderIcon)
for i := pathDepth - maxDepth; i < pathDepth; i++ {
elements = append(elements, folders[i].Name)
}
if hideRootLocation {
root = ""
return pt.colorizePath(elements[0], elements[1:])
}
for i := splitPos; i < pathDepth; i++ {
folders = append(folders, pt.Folders[i].Name)
}
return pt.colorizePath(root, folders)
return pt.colorizePath(root, elements)
}
func (pt *Path) getFullPath() string {
return pt.colorizePath(pt.root, pt.Folders.List())
return pt.colorizePath(pt.root, pt.folders.List())
}
func (pt *Path) getFolderPath() string {
return pt.colorizePath(runtime.Base(pt.env, pt.pwd), nil)
folderName := pt.folders[len(pt.folders)-1].Name
return pt.colorizePath(folderName, nil)
}
func (pt *Path) replaceMappedLocations() (string, string) {
mappedLocations := map[string]string{}
func (pt *Path) join(root, relative string) string {
// this is a full replacement of the parent
if len(root) == 0 {
return relative
}
if !pt.endWithSeparator(root) && len(relative) > 0 {
return root + pt.pathSeparator + relative
}
return root + relative
}
func (pt *Path) setMappedLocations() {
if pt.mappedLocations != nil {
return
}
mappedLocations := make(map[string]string)
// predefined mapped locations, can be disabled
if pt.props.GetBool(MappedLocationsEnabled, true) {
mappedLocations["hkcu:"] = pt.props.GetString(WindowsRegistryIcon, "\uF013")
@ -520,7 +569,7 @@ func (pt *Path) replaceMappedLocations() (string, string) {
// merge custom locations with mapped locations
// mapped locations can override predefined locations
keyValues := pt.props.GetKeyValueMap(MappedLocations, make(map[string]string))
for key, val := range keyValues {
for key, value := range keyValues {
if len(key) == 0 {
continue
}
@ -540,89 +589,86 @@ func (pt *Path) replaceMappedLocations() (string, string) {
continue
}
mappedLocations[pt.normalize(path)] = val
// When two templates resolve to the same key, the values are compared in ascending order and the latter is taken.
if v, exist := mappedLocations[pt.normalize(path)]; exist && value <= v {
continue
}
mappedLocations[pt.normalize(path)] = value
}
pt.mappedLocations = mappedLocations
}
func (pt *Path) replaceMappedLocations(inputPath string) (string, string) {
root, relative := pt.parsePath(inputPath)
if len(relative) == 0 {
pt.RootDir = true
}
pt.setMappedLocations()
if len(pt.mappedLocations) == 0 {
return root, relative
}
// sort map keys in reverse order
// fixes case when a subfoder and its parent are mapped
// ex /users/test and /users/test/dev
keys := make([]string, 0, len(mappedLocations))
for k := range mappedLocations {
keys := make([]string, 0, len(pt.mappedLocations))
for k := range pt.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)
escape := func(path string) string {
// Escape chevron characters to avoid applying unexpected text styles.
return strings.NewReplacer("<", "<<>", ">", "<>>").Replace(path)
}
for _, key := range keys {
keyRoot, keyRelative := pt.parsePath(key)
matchSubFolders := strings.HasSuffix(keyRelative, "*")
matchSubFolders := strings.HasSuffix(keyRelative, pt.pathSeparator+"*")
if matchSubFolders && len(keyRelative) > 1 {
keyRelative = keyRelative[0 : len(keyRelative)-1] // remove trailing /* or \*
if matchSubFolders {
// Remove the trailing wildcard (*).
keyRelative = keyRelative[:len(keyRelative)-1]
}
if keyRoot != rootN || !strings.HasPrefix(relativeN, keyRelative) {
continue
}
value := mappedLocations[key]
value := pt.mappedLocations[key]
overflow := relative[len(keyRelative):]
// exactly match the full path
if len(overflow) == 0 {
// exactly match the full path
return value, ""
}
// only match the root
if len(keyRelative) == 0 {
// only match the root
return value, strings.Trim(relative, pt.pathSeparator)
return value, strings.Trim(escape(relative), pt.pathSeparator)
}
// match several prefix elements
if matchSubFolders || overflow[0:1] == pt.pathSeparator {
return value, strings.Trim(overflow, pt.pathSeparator)
if matchSubFolders || overflow[:1] == pt.pathSeparator {
return value, strings.Trim(escape(overflow), pt.pathSeparator)
}
}
return root, strings.Trim(relative, pt.pathSeparator)
return escape(root), strings.Trim(escape(relative), pt.pathSeparator)
}
func (pt *Path) normalizePath(path string) string {
if pt.env.GOOS() != runtime.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 avoid replacing color codes
clean = append(clean, 92) // 92 == \
continue
}
clean = append(clean, char)
}
return string(clean)
}
// ParsePath parses an input path and returns a clean root and a clean path.
// parsePath parses a clean input path into a root and a relative.
func (pt *Path) parsePath(inputPath string) (string, string) {
var root, path string
var root, relative string
if len(inputPath) == 0 {
return root, path
return root, relative
}
if pt.cygPath {
@ -638,76 +684,58 @@ func (pt *Path) parsePath(inputPath string) (string, string) {
}
}
clean := func(path string) string {
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(pt.pathSeparator)
}
}
return s.String()
}
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)
if pt.env.GOOS() == runtime.WINDOWS {
// Handle a UNC path, if any.
pattern := fmt.Sprintf(`^\%[1]s{2}(?P<hostname>[^\%[1]s]+)\%[1]s(?P<sharename>[^\%[1]s]+)(\%[1]s(?P<path>[\s\S]*))?$`, pt.pathSeparator)
matches := regex.FindNamedRegexMatch(pattern, inputPath)
if len(matches) > 0 {
root = `\\` + matches["hostname"] + `\` + matches["sharename"]
path = clean(matches["path"])
return root, path
root = fmt.Sprintf(`%[1]s%[1]s%[2]s%[1]s%[3]s`, pt.pathSeparator, matches["hostname"], matches["sharename"])
relative = matches["path"]
return root, relative
}
}
s := strings.SplitAfterN(inputPath, pt.pathSeparator, 2)
root = s[0]
if pt.windowsPath {
root = strings.TrimSuffix(root, pt.pathSeparator)
}
if len(s) == 2 {
path = clean(s[1])
if len(root) > 1 {
root = root[:len(root)-1]
}
relative = s[1]
}
return root, path
return root, relative
}
func (pt *Path) isRootFS(path string) bool {
return len(path) == 1 && runtime.IsPathSeparator(pt.env, path[0])
}
func (pt *Path) endWithSeparator(path string) bool {
if len(path) == 0 {
return false
}
return runtime.IsPathSeparator(pt.env, path[len(path)-1])
}
func (pt *Path) normalize(inputPath string) string {
normalized := inputPath
if strings.HasPrefix(normalized, "~") && (len(normalized) == 1 || runtime.IsPathSeparator(pt.env, normalized[1])) {
normalized = pt.env.Home() + normalized[1:]
}
if pt.cygPath {
return normalized
}
normalized = runtime.CleanPath(pt.env, normalized)
switch pt.env.GOOS() {
case runtime.WINDOWS:
normalized = pt.normalizePath(normalized)
fallthrough
case runtime.DARWIN:
if pt.env.GOOS() == runtime.WINDOWS || pt.env.GOOS() == runtime.DARWIN {
normalized = strings.ToLower(normalized)
}
return normalized
}
func (pt *Path) replaceFolderSeparators(pwd string) string {
defaultSeparator := pt.pathSeparator
folderSeparator := pt.getFolderSeparator()
if folderSeparator == defaultSeparator {
return pwd
}
pwd = strings.ReplaceAll(pwd, defaultSeparator, folderSeparator)
return pwd
}
func (pt *Path) colorizePath(root string, elements []string) string {
cycle := pt.props.GetStringArray(Cycle, []string{})
skipColorize := len(cycle) == 0
@ -730,8 +758,8 @@ func (pt *Path) colorizePath(root string, elements []string) string {
}
if len(elements) == 0 {
root = fmt.Sprintf(leftFormat, root)
return colorizeElement(root)
formattedRoot := fmt.Sprintf(leftFormat, root)
return colorizeElement(formattedRoot)
}
colorizeSeparator := func() string {
@ -741,13 +769,13 @@ func (pt *Path) colorizePath(root string, elements []string) string {
return fmt.Sprintf("<%s>%s</>", cycle[0], folderSeparator)
}
var builder strings.Builder
sb := new(strings.Builder)
formattedRoot := fmt.Sprintf(leftFormat, root)
builder.WriteString(colorizeElement(formattedRoot))
sb.WriteString(colorizeElement(formattedRoot))
if root != pt.pathSeparator && len(root) != 0 {
builder.WriteString(colorizeSeparator())
if !pt.endWithSeparator(root) {
sb.WriteString(colorizeSeparator())
}
for i, element := range elements {
@ -760,76 +788,49 @@ func (pt *Path) colorizePath(root string, elements []string) string {
format = rightFormat
}
element = fmt.Sprintf(format, element)
builder.WriteString(colorizeElement(element))
formattedElement := fmt.Sprintf(format, element)
sb.WriteString(colorizeElement(formattedElement))
if i != len(elements)-1 {
builder.WriteString(colorizeSeparator())
sb.WriteString(colorizeSeparator())
}
}
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
return sb.String()
}
func (pt *Path) splitPath() Folders {
result := Folders{}
folders := []string{}
folders := Folders{}
if len(pt.relative) != 0 {
folders = strings.Split(pt.relative, pt.pathSeparator)
if len(pt.relative) == 0 {
return folders
}
elements := strings.Split(pt.relative, pt.pathSeparator)
folderFormatMap := pt.makeFolderFormatMap()
currentPath := pt.root
getCurrentPath := func() string {
if pt.root == "~" {
return pt.env.Home() + pt.pathSeparator
}
if pt.windowsPath {
return pt.root + pt.pathSeparator
}
return pt.root
if !pt.endWithSeparator(pt.root) {
currentPath += pt.pathSeparator
}
currentPath := getCurrentPath()
var display bool
for _, folder := range folders {
currentPath += folder
for _, element := range elements {
currentPath += element
if format := folderFormatMap[currentPath]; len(format) != 0 {
folder = fmt.Sprintf(format, folder)
element = fmt.Sprintf(format, element)
display = true
}
result = append(result, &Folder{Name: folder, Path: currentPath, Display: display})
folders = append(folders, &Folder{Name: element, Path: currentPath, Display: display})
currentPath += pt.pathSeparator
display = false
}
return result
return folders
}
func (pt *Path) makeFolderFormatMap() map[string]string {
@ -838,7 +839,9 @@ func (pt *Path) makeFolderFormatMap() map[string]string {
if gitDirFormat := pt.props.GetString(GitDirFormat, ""); len(gitDirFormat) != 0 {
dir, err := pt.env.HasParentFilePath(".git", false)
if err == nil && dir.IsDir {
folderFormatMap[dir.ParentFolder] = gitDirFormat
// Make it consistent with the modified path.
path := pt.join(pt.replaceMappedLocations(dir.ParentFolder))
folderFormatMap[path] = gitDirFormat
}
}

View file

@ -459,7 +459,7 @@ func TestAgnosterPathStyles(t *testing.T) {
},
{
Style: AgnosterFull,
Expected: "PSDRIVE:/ | src",
Expected: "PSDRIVE: | src",
HomePath: homeDir,
Pwd: "/foo",
Pswd: "PSDRIVE:/src",
@ -489,7 +489,7 @@ func TestAgnosterPathStyles(t *testing.T) {
},
{
Style: AgnosterShort,
Expected: ".. | src",
Expected: "PSDRIVE: | src",
HomePath: homeDir,
Pwd: "/foo",
Pswd: "PSDRIVE:/src",
@ -591,7 +591,7 @@ func TestAgnosterPathStyles(t *testing.T) {
},
{
Style: AgnosterShort,
Expected: "PSDRIVE:/ | .. | init",
Expected: "PSDRIVE: | .. | init",
HomePath: homeDir,
Pwd: "/foo",
Pswd: "PSDRIVE:/src/init",
@ -630,7 +630,7 @@ func TestAgnosterPathStyles(t *testing.T) {
},
{
Style: AgnosterShort,
Expected: ".. > foo",
Expected: "~ > foo",
HomePath: homeDir,
Pwd: homeDir + "/foo",
PathSeparator: "/",
@ -640,7 +640,7 @@ func TestAgnosterPathStyles(t *testing.T) {
},
{
Style: AgnosterShort,
Expected: ".. > foo > bar",
Expected: "~ > foo > bar",
HomePath: homeDir,
Pwd: homeDir + "/foo/bar",
PathSeparator: "/",
@ -661,7 +661,7 @@ func TestAgnosterPathStyles(t *testing.T) {
},
{
Style: AgnosterShort,
Expected: ".. | space foo",
Expected: "~ | space foo",
HomePath: homeDir,
Pwd: homeDir + "/space foo",
PathSeparator: "/",
@ -752,7 +752,7 @@ func TestAgnosterPathStyles(t *testing.T) {
},
{
Style: AgnosterShort,
Expected: ".. > foo",
Expected: "~ > foo",
HomePath: homeDirWindows,
Pwd: homeDirWindows + "\\foo",
GOOS: runtime.WINDOWS,
@ -848,6 +848,7 @@ func TestFullAndFolderPath(t *testing.T) {
// for Windows paths
{Style: FolderType, FolderSeparatorIcon: "\\", Pwd: "C:\\", Expected: "C:\\", PathSeparator: "\\", GOOS: runtime.WINDOWS},
{Style: FolderType, FolderSeparatorIcon: "\\", Pwd: "\\\\localhost\\d$", Expected: "\\\\localhost\\d$", PathSeparator: "\\", GOOS: runtime.WINDOWS},
{Style: FolderType, FolderSeparatorIcon: "\\", Pwd: homeDirWindows, Expected: "~", PathSeparator: "\\", GOOS: runtime.WINDOWS},
{Style: Full, FolderSeparatorIcon: "\\", Pwd: homeDirWindows, Expected: "~", PathSeparator: "\\", GOOS: runtime.WINDOWS},
{Style: Full, FolderSeparatorIcon: "\\", Pwd: homeDirWindows + "\\abc", Expected: "~\\abc", PathSeparator: "\\", GOOS: runtime.WINDOWS},
@ -1001,41 +1002,6 @@ func TestFullPathCustomMappedLocations(t *testing.T) {
}
}
func TestPowerlevelMappedLocations(t *testing.T) {
cases := []struct {
Pwd string
MappedLocations map[string]string
Expected string
}{
{Pwd: "/Users/michal/Applications", MappedLocations: map[string]string{"~": "#"}, Expected: "#/Applications"},
}
for _, tc := range cases {
env := new(mock.Environment)
env.On("Home").Return("/Users/michal")
env.On("Pwd").Return(tc.Pwd)
env.On("GOOS").Return(runtime.DARWIN)
env.On("PathSeparator").Return("/")
env.On("Shell").Return(shell.GENERIC)
env.On("DebugF", testify_.Anything, testify_.Anything).Return(nil)
env.On("Flags").Return(&runtime.Flags{})
path := &Path{
env: env,
props: properties.Map{
properties.Style: Powerlevel,
MappedLocations: tc.MappedLocations,
},
}
path.setPaths()
path.setStyle()
got := renderTemplateNoTrimSpace(env, "{{ .Path }}", path)
assert.Equal(t, tc.Expected, got)
}
}
func TestFolderPathCustomMappedLocations(t *testing.T) {
pwd := abcd
env := new(mock.Environment)
@ -1501,46 +1467,155 @@ func TestGetFolderSeparator(t *testing.T) {
func TestNormalizePath(t *testing.T) {
cases := []struct {
Input string
HomeDir string
GOOS string
Expected string
Case string
Input string
HomeDir string
GOOS string
PathSeparator string
Expected string
}{
{Input: "/foo/~/bar", HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, Expected: "\\foo\\~\\bar"},
{Input: homeDirWindows + "\\Foo", HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, Expected: "c:\\users\\someone\\foo"},
{Input: "~/Bob\\Foo", HomeDir: homeDir, GOOS: runtime.LINUX, Expected: homeDir + "/Bob\\Foo"},
{Input: "~/Bob\\Foo", HomeDir: homeDir, GOOS: runtime.DARWIN, Expected: homeDir + "/bob\\foo"},
{Input: "~\\Bob\\Foo", HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, Expected: "c:\\users\\someone\\bob\\foo"},
{Input: "/foo/~/bar", HomeDir: homeDir, GOOS: runtime.LINUX, Expected: "/foo/~/bar"},
{Input: "~/baz", HomeDir: homeDir, GOOS: runtime.LINUX, Expected: homeDir + "/baz"},
{Input: "~/baz", HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, Expected: "c:\\users\\someone\\baz"},
{
Case: "Windows: absolute w/o drive letter, forward slash included",
Input: "/foo/~/bar",
HomeDir: homeDirWindows,
GOOS: runtime.WINDOWS,
PathSeparator: `\`,
Expected: "\\foo\\~\\bar",
},
{
Case: "Windows: absolute",
Input: homeDirWindows + "\\Foo",
HomeDir: homeDirWindows,
GOOS: runtime.WINDOWS,
PathSeparator: `\`,
Expected: "c:\\users\\someone\\foo",
},
{
Case: "Linux: home prefix, backslash included",
Input: "~/Bob\\Foo",
HomeDir: homeDir,
GOOS: runtime.LINUX,
Expected: homeDir + "/Bob\\Foo",
},
{
Case: "macOS: home prefix, backslash included",
Input: "~/Bob\\Foo",
HomeDir: homeDir,
GOOS: runtime.DARWIN,
Expected: homeDir + "/bob\\foo",
},
{
Case: "Windows: home prefix",
Input: "~\\Bob\\Foo",
HomeDir: homeDirWindows,
GOOS: runtime.WINDOWS,
PathSeparator: `\`,
Expected: "c:\\users\\someone\\bob\\foo",
},
{
Case: "Linux: absolute",
Input: "/foo/~/bar",
HomeDir: homeDir,
GOOS: runtime.LINUX,
Expected: "/foo/~/bar",
},
{
Case: "Linux: home prefix",
Input: "~/baz",
HomeDir: homeDir,
GOOS: runtime.LINUX,
Expected: homeDir + "/baz",
},
{
Case: "Windows: home prefix",
Input: "~/baz",
HomeDir: homeDirWindows,
GOOS: runtime.WINDOWS,
PathSeparator: `\`,
Expected: "c:\\users\\someone\\baz",
},
{
Case: "Windows: UNC root w/ prefix",
Input: `\\.\UNC\localhost\c$`,
HomeDir: homeDirWindows,
GOOS: runtime.WINDOWS,
PathSeparator: `\`,
Expected: "\\\\localhost\\c$",
},
{
Case: "Windows: UNC root w/ prefix, forward slash included",
Input: "//./UNC/localhost/c$",
HomeDir: homeDirWindows,
GOOS: runtime.WINDOWS,
PathSeparator: `\`,
Expected: "\\\\localhost\\c$",
},
{
Case: "Windows: UNC root",
Input: `\\localhost\c$\`,
HomeDir: homeDirWindows,
GOOS: runtime.WINDOWS,
PathSeparator: `\`,
Expected: "\\\\localhost\\c$",
},
{
Case: "Windows: UNC root, forward slash included",
Input: "//localhost/c$",
HomeDir: homeDirWindows,
GOOS: runtime.WINDOWS,
PathSeparator: `\`,
Expected: "\\\\localhost\\c$",
},
{
Case: "Windows: UNC",
Input: `\\localhost\c$\some`,
HomeDir: homeDirWindows,
GOOS: runtime.WINDOWS,
PathSeparator: `\`,
Expected: "\\\\localhost\\c$\\some",
},
{
Case: "Windows: UNC, forward slash included",
Input: "//localhost/c$/some",
HomeDir: homeDirWindows,
GOOS: runtime.WINDOWS,
PathSeparator: `\`,
Expected: "\\\\localhost\\c$\\some",
},
}
for _, tc := range cases {
env := new(mock.Environment)
env.On("Home").Return(tc.HomeDir)
env.On("GOOS").Return(tc.GOOS)
pt := &Path{
env: env,
if len(tc.PathSeparator) == 0 {
tc.PathSeparator = "/"
}
env.On("PathSeparator").Return(tc.PathSeparator)
pt := &Path{env: env}
got := pt.normalize(tc.Input)
assert.Equal(t, tc.Expected, got)
assert.Equal(t, tc.Expected, got, tc.Case)
}
}
func TestReplaceMappedLocations(t *testing.T) {
cases := []struct {
Case string
Pwd string
Expected string
Case string
Pwd string
MappedLocationsEnabled bool
Expected string
}{
{Pwd: "/c/l/k/f", Expected: "f"},
{Pwd: "/f/g/h", Expected: "/f/g/h"},
{Pwd: "/f/g/h/e", Expected: "^/e"},
{Pwd: abcd, Expected: "#"},
{Pwd: "/a/b/c/d/e", Expected: "#/e"},
{Pwd: "/a/b/c/d/e", Expected: "#/e"},
{Pwd: "/a/b/c/D/e", Expected: "#/e"},
{Pwd: "/a/b/k/j/e", Expected: "e"},
{Pwd: "/a/b/k/l", Expected: "@/l"},
{Pwd: "/a/b/k/l", MappedLocationsEnabled: true, Expected: "~/l"},
}
for _, tc := range cases {
@ -1556,11 +1631,12 @@ func TestReplaceMappedLocations(t *testing.T) {
path := &Path{
env: env,
props: properties.Map{
MappedLocationsEnabled: false,
MappedLocationsEnabled: tc.MappedLocationsEnabled,
MappedLocations: map[string]string{
abcd: "#",
"/f/g/h/*": "^",
"/c/l/k/*": "",
"~": "@",
"~/j/*": "",
},
},
@ -1600,8 +1676,8 @@ func TestSplitPath(t *testing.T) {
GitDir: &runtime.FileInfo{IsDir: true, ParentFolder: "/a/b/c"},
GitDirFormat: "<b>%s</b>",
Expected: Folders{
{Name: "<b>c</b>", Path: "/a/b/c", Display: true},
{Name: "d", Path: "/a/b/c/d"},
{Name: "<b>c</b>", Path: "~/c", Display: true},
{Name: "d", Path: "~/c/d"},
},
},
{
@ -1622,6 +1698,7 @@ func TestSplitPath(t *testing.T) {
for _, tc := range cases {
env := new(mock.Environment)
env.On("PathSeparator").Return("/")
env.On("Home").Return("/a/b")
env.On("HasParentFilePath", ".git", false).Return(tc.GitDir, nil)
env.On("GOOS").Return(tc.GOOS)

View file

@ -86,6 +86,8 @@ const (
hyperLinkText = "<TEXT>"
hyperLinkTextEnd = "</TEXT>"
empty = "<>"
startProgress = "\x1b]9;4;3;0\x07"
endProgress = "\x1b]9;4;0;0\x07"
@ -160,10 +162,6 @@ func Pwd(pwdType, userName, hostName, pwd string) string {
return ""
}
if strings.HasSuffix(pwd, ":") {
pwd += `/`
}
switch pwdType {
case OSC7:
return fmt.Sprintf(formats.Osc7, hostName, pwd)
@ -346,6 +344,9 @@ func Write(background, foreground color.Ansi, text string) {
i += len([]rune(match[ANCHOR])) - 1
builder.WriteString(formats.HyperlinkEnd)
continue
case empty:
i += len([]rune(match[ANCHOR])) - 1
continue
}
i = writeArchorOverride(match, background, i)

View file

@ -77,7 +77,9 @@ For example, to swap out `C:\Users\Leet\GitHub` with a GitHub icon, you can do t
- To make mapped locations work cross-platform, use `/` as the path separator, Oh My Posh will
automatically match effective separators based on the running operating system.
- If you want to match all child directories, you can use `*` as a wildcard, for example:
`"C:/Users/Bill/*": "$"` will turn `C:/Users/Bill/Downloads` into `$/Downloads`.
`"C:/Users/Bill/*": "$"` will turn `C:/Users/Bill/Downloads` into `$/Downloads` but leave `C:/Users/Bill` unchanged.
- To prevent mangling path elements, if you use any text style tags (e.g., `<lightGreen>...</>`) in replacement values,
you should avoid using a chevron character (`<`/`>`) in the `folder_separator_icon` property, and vice versa.
- The character `~` at the start of a mapped location will match the user's home directory.
- The match is case-insensitive on Windows and macOS, but case-sensitive on other operating systems. This means that for
user Bill, who has a user account `Bill` on Windows and `bill` on Linux, `~/Foo` might match