fix(path): parse path correctly

This commit is contained in:
L. Yeung 2022-10-03 22:46:16 +08:00 committed by Jan De Dobbeleer
parent 049f9d4f94
commit 5b6a3470d1
8 changed files with 1055 additions and 573 deletions

View file

@ -22,7 +22,7 @@ const (
Prompt BlockType = "prompt" Prompt BlockType = "prompt"
// LineBreak creates a line break in the prompt // LineBreak creates a line break in the prompt
LineBreak BlockType = "newline" LineBreak BlockType = "newline"
// RPrompt a right aligned prompt in ZSH and Powershell // RPrompt is a right aligned prompt
RPrompt BlockType = "rprompt" RPrompt BlockType = "rprompt"
// Left aligns left // Left aligns left
Left BlockAlignment = "left" Left BlockAlignment = "left"

View file

@ -130,7 +130,7 @@ func (e *Engine) shouldFill(block *Block, length int) (string, bool) {
func (e *Engine) renderBlock(block *Block) { func (e *Engine) renderBlock(block *Block) {
defer func() { defer func() {
// Due to a bug in Powershell, the end of the line needs to be cleared. // Due to a bug in PowerShell, the end of the line needs to be cleared.
// If this doesn't happen, the portion after the prompt gets colored in the background // If this doesn't happen, the portion after the prompt gets colored in the background
// color of the line above the new input line. Clearing the line fixes this, // color of the line above the new input line. Clearing the line fixes this,
// but can hopefully one day be removed when this is resolved natively. // but can hopefully one day be removed when this is resolved natively.

View file

@ -337,11 +337,16 @@ func (env *ShellEnvironment) Pwd() string {
} }
correctPath := func(pwd string) string { correctPath := func(pwd string) string {
// on Windows, and being case sensitive and not consistent and all, this gives silly issues // on Windows, and being case sensitive and not consistent and all, this gives silly issues
driveLetter := regex.GetCompiledRegex(`^[a-z]:`) if env.GOOS() == WINDOWS {
return driveLetter.ReplaceAllStringFunc(pwd, strings.ToUpper) driveLetter := regex.GetCompiledRegex(`^[a-z]:`)
return driveLetter.ReplaceAllStringFunc(pwd, strings.ToUpper)
}
return pwd
} }
if env.CmdFlags != nil && env.CmdFlags.PWD != "" { if env.CmdFlags != nil && env.CmdFlags.PWD != "" {
env.cwd = correctPath(env.CmdFlags.PWD) // ensure a clean path
root, path := ParsePath(env, correctPath(env.CmdFlags.PWD))
env.cwd = root + path
return env.cwd return env.cwd
} }
dir, err := os.Getwd() dir, err := os.Getwd()
@ -735,6 +740,9 @@ func (env *ShellEnvironment) TemplateCache() *TemplateCache {
pwd := env.Pwd() pwd := env.Pwd()
tmplCache.PWD = ReplaceHomeDirPrefixWithTilde(env, pwd) tmplCache.PWD = ReplaceHomeDirPrefixWithTilde(env, pwd)
tmplCache.Folder = Base(env, pwd) tmplCache.Folder = Base(env, pwd)
if env.GOOS() == WINDOWS && strings.HasSuffix(tmplCache.Folder, ":") {
tmplCache.Folder += `\`
}
tmplCache.UserName = env.User() tmplCache.UserName = env.User()
if host, err := env.Host(); err == nil { if host, err := env.Host(); err == nil {
tmplCache.HostName = host tmplCache.HostName = host
@ -766,19 +774,21 @@ func (env *ShellEnvironment) DirMatchesOneOf(dir string, regexes []string) (matc
} }
func dirMatchesOneOf(dir, home, goos string, regexes []string) bool { func dirMatchesOneOf(dir, home, goos string, regexes []string) bool {
normalizedCwd := strings.ReplaceAll(dir, "\\", "/") if goos == WINDOWS {
normalizedHomeDir := strings.ReplaceAll(home, "\\", "/") dir = strings.ReplaceAll(dir, "\\", "/")
home = strings.ReplaceAll(home, "\\", "/")
}
for _, element := range regexes { for _, element := range regexes {
normalizedElement := strings.ReplaceAll(element, "\\\\", "/") normalizedElement := strings.ReplaceAll(element, "\\\\", "/")
if strings.HasPrefix(normalizedElement, "~") { if strings.HasPrefix(normalizedElement, "~") {
normalizedElement = strings.Replace(normalizedElement, "~", normalizedHomeDir, 1) normalizedElement = strings.Replace(normalizedElement, "~", home, 1)
} }
pattern := fmt.Sprintf("^%s$", normalizedElement) pattern := fmt.Sprintf("^%s$", normalizedElement)
if goos == WINDOWS || goos == DARWIN { if goos == WINDOWS || goos == DARWIN {
pattern = "(?i)" + pattern pattern = "(?i)" + pattern
} }
matched := regex.MatchString(pattern, normalizedCwd) matched := regex.MatchString(pattern, dir)
if matched { if matched {
return true return true
} }
@ -786,7 +796,7 @@ func dirMatchesOneOf(dir, home, goos string, regexes []string) bool {
return false return false
} }
func isPathSeparator(env Environment, c uint8) bool { func IsPathSeparator(env Environment, c uint8) bool {
if c == '/' { if c == '/' {
return true return true
} }
@ -800,14 +810,14 @@ func isPathSeparator(env Environment, c uint8) bool {
// Trailing path separators are removed before extracting the last element. // Trailing path separators are removed before extracting the last element.
// If the path consists entirely of separators, Base returns a single separator. // If the path consists entirely of separators, Base returns a single separator.
func Base(env Environment, path string) string { func Base(env Environment, path string) string {
if path == "/" {
return path
}
volumeName := filepath.VolumeName(path) volumeName := filepath.VolumeName(path)
// Strip trailing slashes. // Strip trailing slashes.
for len(path) > 0 && isPathSeparator(env, path[len(path)-1]) { for len(path) > 0 && IsPathSeparator(env, path[len(path)-1]) {
path = path[0 : len(path)-1] path = path[0 : len(path)-1]
} }
if len(path) == 0 {
return env.PathSeparator()
}
if volumeName == path { if volumeName == path {
return path return path
} }
@ -815,22 +825,69 @@ func Base(env Environment, path string) string {
path = path[len(filepath.VolumeName(path)):] path = path[len(filepath.VolumeName(path)):]
// Find the last element // Find the last element
i := len(path) - 1 i := len(path) - 1
for i >= 0 && !isPathSeparator(env, path[i]) { for i >= 0 && !IsPathSeparator(env, path[i]) {
i-- i--
} }
if i >= 0 { if i >= 0 {
path = path[i+1:] path = path[i+1:]
} }
// If empty now, it had only slashes. // If empty now, it had only slashes.
if path == "" { if len(path) == 0 {
return env.PathSeparator() return env.PathSeparator()
} }
return path return path
} }
// ParsePath parses an input path and returns a clean root and a clean path.
func ParsePath(env Environment, inputPath string) (root, path string) {
if len(inputPath) == 0 {
return
}
separator := env.PathSeparator()
clean := func(path string) string {
matches := regex.FindAllNamedRegexMatch(fmt.Sprintf(`(?P<element>[^\%s]+)`, separator), path)
n := len(matches) - 1
s := new(strings.Builder)
for i, m := range matches {
s.WriteString(m["element"])
if i != n {
s.WriteString(separator)
}
}
return s.String()
}
if env.GOOS() == WINDOWS {
inputPath = strings.ReplaceAll(inputPath, "/", `\`)
// for a UNC path, extract \\hostname\sharename as the root
matches := regex.FindNamedRegexMatch(`^\\\\(?P<hostname>[^\\]+)\\+(?P<sharename>[^\\]+)\\*(?P<path>[\s\S]*)$`, inputPath)
if len(matches) > 0 {
root = `\\` + matches["hostname"] + `\` + matches["sharename"] + `\`
path = clean(matches["path"])
return
}
}
s := strings.SplitAfterN(inputPath, separator, 2)
root = s[0]
if !strings.HasSuffix(root, separator) {
// a root should end with a separator
root += separator
}
if len(s) == 2 {
path = clean(s[1])
}
return root, path
}
func ReplaceHomeDirPrefixWithTilde(env Environment, path string) string { func ReplaceHomeDirPrefixWithTilde(env Environment, path string) string {
if strings.HasPrefix(path, env.Home()) { home := env.Home()
return strings.Replace(path, env.Home(), "~", 1) // match Home directory exactly
if !strings.HasPrefix(path, home) {
return path
}
rem := path[len(home):]
if len(rem) == 0 || IsPathSeparator(env, rem[0]) {
return "~" + rem
} }
return path return path
} }

View file

@ -22,29 +22,6 @@ func TestHostNameWithLan(t *testing.T) {
assert.Equal(t, "hello", cleanHostName) assert.Equal(t, "hello", cleanHostName)
} }
func TestWindowsPathWithDriveLetter(t *testing.T) {
cases := []struct {
Case string
CWD string
Expected string
}{
{Case: "C drive", CWD: `C:\Windows\`, Expected: `C:\Windows\`},
{Case: "C drive lower case", CWD: `c:\Windows\`, Expected: `C:\Windows\`},
{Case: "P drive lower case", CWD: `p:\some\`, Expected: `P:\some\`},
{Case: "some drive lower case", CWD: `some:\some\`, Expected: `some:\some\`},
{Case: "drive ending in c:", CWD: `src:\source\`, Expected: `src:\source\`},
{Case: "registry drive", CWD: `HKLM:\SOFTWARE\magnetic:test\`, Expected: `HKLM:\SOFTWARE\magnetic:test\`},
}
for _, tc := range cases {
env := &ShellEnvironment{
CmdFlags: &Flags{
PWD: tc.CWD,
},
}
assert.Equal(t, env.Pwd(), tc.Expected)
}
}
func TestDirMatchesOneOf(t *testing.T) { func TestDirMatchesOneOf(t *testing.T) {
cases := []struct { cases := []struct {
GOOS string GOOS string

View file

@ -5,8 +5,8 @@ import (
"oh-my-posh/environment" "oh-my-posh/environment"
"oh-my-posh/properties" "oh-my-posh/properties"
"oh-my-posh/regex" "oh-my-posh/regex"
"oh-my-posh/shell"
"oh-my-posh/template" "oh-my-posh/template"
"path/filepath"
"sort" "sort"
"strings" "strings"
) )
@ -45,7 +45,7 @@ const (
Full string = "full" Full string = "full"
// Folder displays the current folder // Folder displays the current folder
Folder string = "folder" Folder string = "folder"
// Mixed like agnoster, but if the path is short it displays it // Mixed like agnoster, but if a folder name is short enough, it is displayed as-is
Mixed string = "mixed" Mixed string = "mixed"
// Letter like agnoster, but with the first letter of each folder name // Letter like agnoster, but with the first letter of each folder name
Letter string = "letter" Letter string = "letter"
@ -70,64 +70,33 @@ func (pt *Path) Template() string {
} }
func (pt *Path) Enabled() bool { func (pt *Path) Enabled() bool {
pt.pwd = pt.env.Pwd() pt.setPath()
switch style := pt.props.GetString(properties.Style, Agnoster); style {
case Agnoster:
pt.Path = pt.getAgnosterPath()
case AgnosterFull:
pt.Path = pt.getAgnosterFullPath()
case AgnosterShort:
pt.Path = pt.getAgnosterShortPath()
case Mixed:
pt.Path = pt.getMixedPath()
case Letter:
pt.Path = pt.getLetterPath()
case Unique:
pt.Path = pt.getUniqueLettersPath()
case AgnosterLeft:
pt.Path = pt.getAgnosterLeftPath()
case Short:
// "short" is a duplicate of "full", just here for backwards compatibility
fallthrough
case Full:
pt.Path = pt.getFullPath()
case Folder:
pt.Path = pt.getFolderPath()
default:
pt.Path = fmt.Sprintf("Path style: %s is not available", style)
}
pt.Path = pt.formatWindowsDrive(pt.Path)
if pt.env.IsWsl() { if pt.env.IsWsl() {
pt.Location, _ = pt.env.RunCommand("wslpath", "-m", pt.pwd) pt.Location, _ = pt.env.RunCommand("wslpath", "-m", pt.pwd)
} else { } else {
pt.Location = pt.pwd pt.Location = pt.pwd
} }
pt.StackCount = pt.env.StackCount() pt.StackCount = pt.env.StackCount()
pt.Writable = pt.env.DirIsWritable(pt.pwd) pt.Writable = pt.env.DirIsWritable(pt.pwd)
return true return true
} }
func (pt *Path) Parent() string { func (pt *Path) Parent() string {
if pt.pwd == pt.env.Home() { pwd := pt.getPwd()
if len(pwd) == 0 {
return "" return ""
} }
parent := filepath.Dir(pt.pwd) root, path := environment.ParsePath(pt.env, pwd)
if pt.pwd == parent { if len(path) == 0 {
// a root path has no parent
return "" return ""
} }
separator := pt.env.PathSeparator() base := environment.Base(pt.env, path)
if parent == pt.rootLocation() || parent == separator { path = pt.replaceFolderSeparators(path[:len(path)-len(base)])
separator = "" if root != pt.env.PathSeparator() {
root = root[:len(root)-1] + pt.getFolderSeparator()
} }
return pt.replaceMappedLocations(parent) + separator return root + path
}
func (pt *Path) formatWindowsDrive(pwd string) string {
if pt.env.GOOS() != environment.WINDOWS || !strings.HasSuffix(pwd, ":") {
return pwd
}
return pwd + "\\"
} }
func (pt *Path) Init(props properties.Properties, env environment.Environment) { func (pt *Path) Init(props properties.Properties, env environment.Environment) {
@ -135,10 +104,52 @@ func (pt *Path) Init(props properties.Properties, env environment.Environment) {
pt.env = env pt.env = env
} }
func (pt *Path) setPath() {
pwd := pt.getPwd()
if len(pwd) == 0 {
return
}
root, path := environment.ParsePath(pt.env, pwd)
if len(path) == 0 {
pt.Path = pt.formatRoot(root)
return
}
switch style := pt.props.GetString(properties.Style, Agnoster); style {
case Agnoster:
pt.Path = pt.getAgnosterPath(root, path)
case AgnosterFull:
pt.Path = pt.getAgnosterFullPath(root, path)
case AgnosterShort:
pt.Path = pt.getAgnosterShortPath(root, path)
case Mixed:
pt.Path = pt.getMixedPath(root, path)
case Letter:
pt.Path = pt.getLetterPath(root, path)
case Unique:
pt.Path = pt.getUniqueLettersPath(root, path)
case AgnosterLeft:
pt.Path = pt.getAgnosterLeftPath(root, path)
case Short:
// "short" is a duplicate of "full", just here for backwards compatibility
fallthrough
case Full:
pt.Path = pt.getFullPath(root, path)
case Folder:
pt.Path = pt.getFolderPath(path)
default:
pt.Path = fmt.Sprintf("Path style: %s is not available", style)
}
}
func (pt *Path) getFolderSeparator() string { func (pt *Path) getFolderSeparator() string {
separatorTemplate := pt.props.GetString(FolderSeparatorTemplate, "") separatorTemplate := pt.props.GetString(FolderSeparatorTemplate, "")
if len(separatorTemplate) == 0 { if len(separatorTemplate) == 0 {
return pt.props.GetString(FolderSeparatorIcon, pt.env.PathSeparator()) separator := pt.props.GetString(FolderSeparatorIcon, pt.env.PathSeparator())
// if empty, use the default separator
if len(separator) == 0 {
return pt.env.PathSeparator()
}
return separator
} }
tmpl := &template.Text{ tmpl := &template.Text{
Template: separatorTemplate, Template: separatorTemplate,
@ -149,67 +160,66 @@ func (pt *Path) getFolderSeparator() string {
if err != nil { if err != nil {
pt.env.Log(environment.Error, "getFolderSeparator", err.Error()) pt.env.Log(environment.Error, "getFolderSeparator", err.Error())
} }
if len(text) == 0 {
return pt.env.PathSeparator()
}
return text return text
} }
func (pt *Path) getMixedPath() string { func (pt *Path) getMixedPath(root, path string) string {
var buffer strings.Builder var buffer strings.Builder
pwd := pt.getPwd()
splitted := strings.Split(pwd, pt.env.PathSeparator())
threshold := int(pt.props.GetFloat64(MixedThreshold, 4)) threshold := int(pt.props.GetFloat64(MixedThreshold, 4))
for i, part := range splitted { folderIcon := pt.props.GetString(FolderIcon, "..")
if part == "" { separator := pt.getFolderSeparator()
continue elements := strings.Split(path, pt.env.PathSeparator())
} if root != pt.env.PathSeparator() {
elements = append([]string{root[:len(root)-1]}, elements...)
folder := part }
if len(part) > threshold && i != 0 && i != len(splitted)-1 { n := len(elements)
folder = pt.props.GetString(FolderIcon, "..") buffer.WriteString(elements[0])
} for i := 1; i < n; i++ {
separator := pt.getFolderSeparator() folder := elements[i]
if i == 0 { if len(folder) > threshold && i != n-1 {
separator = "" folder = folderIcon
} }
buffer.WriteString(fmt.Sprintf("%s%s", separator, folder)) buffer.WriteString(fmt.Sprintf("%s%s", separator, folder))
} }
return buffer.String() return buffer.String()
} }
func (pt *Path) getAgnosterPath() string { func (pt *Path) getAgnosterPath(root, path string) string {
var buffer strings.Builder var buffer strings.Builder
pwd := pt.getPwd()
buffer.WriteString(pt.rootLocation())
pathDepth := pt.pathDepth(pwd)
folderIcon := pt.props.GetString(FolderIcon, "..") folderIcon := pt.props.GetString(FolderIcon, "..")
separator := pt.getFolderSeparator() separator := pt.getFolderSeparator()
for i := 1; i < pathDepth; i++ { elements := strings.Split(path, pt.env.PathSeparator())
if root != pt.env.PathSeparator() {
elements = append([]string{root[:len(root)-1]}, elements...)
}
n := len(elements)
buffer.WriteString(elements[0])
for i := 2; i < n; i++ {
buffer.WriteString(fmt.Sprintf("%s%s", separator, folderIcon)) buffer.WriteString(fmt.Sprintf("%s%s", separator, folderIcon))
} }
if pathDepth > 0 { if n > 1 {
buffer.WriteString(fmt.Sprintf("%s%s", separator, environment.Base(pt.env, pwd))) buffer.WriteString(fmt.Sprintf("%s%s", separator, elements[n-1]))
} }
return buffer.String() return buffer.String()
} }
func (pt *Path) getAgnosterLeftPath() string { func (pt *Path) getAgnosterLeftPath(root, path string) string {
pwd := pt.getPwd()
separator := pt.env.PathSeparator()
pwd = strings.Trim(pwd, separator)
splitted := strings.Split(pwd, separator)
folderIcon := pt.props.GetString(FolderIcon, "..")
separator = pt.getFolderSeparator()
switch len(splitted) {
case 0:
return ""
case 1:
return splitted[0]
case 2:
return fmt.Sprintf("%s%s%s", splitted[0], separator, splitted[1])
}
var buffer strings.Builder var buffer strings.Builder
buffer.WriteString(fmt.Sprintf("%s%s%s", splitted[0], separator, splitted[1])) folderIcon := pt.props.GetString(FolderIcon, "..")
for i := 2; i < len(splitted); i++ { separator := pt.getFolderSeparator()
elements := strings.Split(path, pt.env.PathSeparator())
if root != pt.env.PathSeparator() {
elements = append([]string{root[:len(root)-1]}, elements...)
}
n := len(elements)
buffer.WriteString(elements[0])
if n > 1 {
buffer.WriteString(fmt.Sprintf("%s%s", separator, elements[1]))
}
for i := 2; i < n; i++ {
buffer.WriteString(fmt.Sprintf("%s%s", separator, folderIcon)) buffer.WriteString(fmt.Sprintf("%s%s", separator, folderIcon))
} }
return buffer.String() return buffer.String()
@ -218,7 +228,7 @@ func (pt *Path) getAgnosterLeftPath() string {
func (pt *Path) getRelevantLetter(folder string) string { func (pt *Path) getRelevantLetter(folder string) string {
// check if there is at least a letter we can use // check if there is at least a letter we can use
matches := regex.FindNamedRegexMatch(`(?P<letter>[\p{L}0-9]).*`, folder) matches := regex.FindNamedRegexMatch(`(?P<letter>[\p{L}0-9]).*`, folder)
if matches == nil || matches["letter"] == "" { if matches == nil || len(matches["letter"]) == 0 {
// no letter found, keep the folder unchanged // no letter found, keep the folder unchanged
return folder return folder
} }
@ -228,36 +238,36 @@ func (pt *Path) getRelevantLetter(folder string) string {
return letter return letter
} }
func (pt *Path) getLetterPath() string { func (pt *Path) getLetterPath(root, path string) string {
var buffer strings.Builder var buffer strings.Builder
pwd := pt.getPwd()
splitted := strings.Split(pwd, pt.env.PathSeparator())
separator := pt.getFolderSeparator() separator := pt.getFolderSeparator()
for i := 0; i < len(splitted)-1; i++ { elements := strings.Split(path, pt.env.PathSeparator())
folder := splitted[i] if root != pt.env.PathSeparator() {
if len(folder) == 0 { elements = append([]string{root[:len(root)-1]}, elements...)
continue }
n := len(elements)
for i := 0; i < n-1; i++ {
letter := pt.getRelevantLetter(elements[i])
if i != 0 {
buffer.WriteString(separator)
} }
letter := pt.getRelevantLetter(folder) buffer.WriteString(letter)
buffer.WriteString(fmt.Sprintf("%s%s", letter, separator))
}
if len(splitted) > 0 {
buffer.WriteString(splitted[len(splitted)-1])
} }
buffer.WriteString(fmt.Sprintf("%s%s", separator, elements[n-1]))
return buffer.String() return buffer.String()
} }
func (pt *Path) getUniqueLettersPath() string { func (pt *Path) getUniqueLettersPath(root, path string) string {
var buffer strings.Builder var buffer strings.Builder
pwd := pt.getPwd()
splitted := strings.Split(pwd, pt.env.PathSeparator())
separator := pt.getFolderSeparator() separator := pt.getFolderSeparator()
letters := make(map[string]bool, len(splitted)) elements := strings.Split(path, pt.env.PathSeparator())
for i := 0; i < len(splitted)-1; i++ { if root != pt.env.PathSeparator() {
folder := splitted[i] elements = append([]string{root[:len(root)-1]}, elements...)
if len(folder) == 0 { }
continue n := len(elements)
} letters := make(map[string]bool)
for i := 0; i < n-1; i++ {
folder := elements[i]
letter := pt.getRelevantLetter(folder) letter := pt.getRelevantLetter(folder)
for letters[letter] { for letters[letter] {
if letter == folder { if letter == folder {
@ -266,102 +276,126 @@ func (pt *Path) getUniqueLettersPath() string {
letter += folder[len(letter) : len(letter)+1] letter += folder[len(letter) : len(letter)+1]
} }
letters[letter] = true letters[letter] = true
buffer.WriteString(fmt.Sprintf("%s%s", letter, separator)) if i != 0 {
} buffer.WriteString(separator)
if len(splitted) > 0 { }
buffer.WriteString(splitted[len(splitted)-1]) buffer.WriteString(letter)
} }
buffer.WriteString(fmt.Sprintf("%s%s", separator, elements[n-1]))
return buffer.String() return buffer.String()
} }
func (pt *Path) getAgnosterFullPath() string { func (pt *Path) getAgnosterFullPath(root, path string) string {
pwd := pt.getPwd() path = pt.replaceFolderSeparators(path)
for len(pwd) > 1 && string(pwd[0]) == pt.env.PathSeparator() { if root == pt.env.PathSeparator() {
pwd = pwd[1:] return path
} }
return pt.replaceFolderSeparators(pwd) root = root[:len(root)-1] + pt.getFolderSeparator()
return root + path
} }
func (pt *Path) getAgnosterShortPath() string { func (pt *Path) getAgnosterShortPath(root, path string) string {
pwd := pt.getPwd() elements := strings.Split(path, pt.env.PathSeparator())
pathDepth := pt.pathDepth(pwd) if root != pt.env.PathSeparator() {
elements = append([]string{root[:len(root)-1]}, elements...)
}
depth := len(elements)
maxDepth := pt.props.GetInt(MaxDepth, 1) maxDepth := pt.props.GetInt(MaxDepth, 1)
if maxDepth < 1 { if maxDepth < 1 {
maxDepth = 1 maxDepth = 1
} }
hideRootLocation := pt.props.GetBool(HideRootLocation, false) hideRootLocation := pt.props.GetBool(HideRootLocation, false)
if hideRootLocation { if !hideRootLocation {
// 1-indexing to avoid showing the root location when exceeding the max depth maxDepth++
pathDepth++
} }
if pathDepth <= maxDepth { if depth <= maxDepth {
return pt.getAgnosterFullPath() return pt.getAgnosterFullPath(root, path)
} }
folderSeparator := pt.getFolderSeparator() separator := pt.getFolderSeparator()
pathSeparator := pt.env.PathSeparator() folderIcon := pt.props.GetString(FolderIcon, "..")
splitted := strings.Split(pwd, pathSeparator)
fullPathDepth := len(splitted)
splitPos := fullPathDepth - maxDepth
var buffer strings.Builder var buffer strings.Builder
if hideRootLocation { if !hideRootLocation {
buffer.WriteString(splitted[splitPos]) buffer.WriteString(fmt.Sprintf("%s%s", elements[0], separator))
splitPos++ maxDepth--
} else {
folderIcon := pt.props.GetString(FolderIcon, "..")
root := pt.rootLocation()
buffer.WriteString(fmt.Sprintf("%s%s%s", root, folderSeparator, folderIcon))
} }
for i := splitPos; i < fullPathDepth; i++ { splitPos := depth - maxDepth
buffer.WriteString(fmt.Sprintf("%s%s", folderSeparator, splitted[i])) if splitPos != 1 {
buffer.WriteString(fmt.Sprintf("%s%s", folderIcon, separator))
}
for i := splitPos; i < depth; i++ {
buffer.WriteString(elements[i])
if i != depth-1 {
buffer.WriteString(separator)
}
} }
return buffer.String() return buffer.String()
} }
func (pt *Path) getFullPath() string { func (pt *Path) getFullPath(root, path string) string {
pwd := pt.getPwd() if root != pt.env.PathSeparator() {
return pt.replaceFolderSeparators(pwd) root = root[:len(root)-1] + pt.getFolderSeparator()
}
path = pt.replaceFolderSeparators(path)
return root + path
} }
func (pt *Path) getFolderPath() string { func (pt *Path) getFolderPath(path string) string {
pwd := pt.getPwd() return environment.Base(pt.env, path)
pwd = environment.Base(pt.env, pwd) }
return pt.replaceFolderSeparators(pwd)
func (pt *Path) setPwd() {
if len(pt.pwd) > 0 {
return
}
if pt.env.Shell() == shell.PWSH || pt.env.Shell() == shell.PWSH5 {
pt.pwd = pt.env.Flags().PSWD
}
if len(pt.pwd) == 0 {
pt.pwd = pt.env.Pwd()
return
}
// ensure a clean path
root, path := environment.ParsePath(pt.env, pt.pwd)
pt.pwd = root + path
} }
func (pt *Path) getPwd() string { func (pt *Path) getPwd() string {
pwd := pt.env.Flags().PSWD pt.setPwd()
if pwd == "" { return pt.replaceMappedLocations(pt.pwd)
pwd = pt.env.Pwd() }
func (pt *Path) formatRoot(root string) string {
n := len(root)
// trim the trailing separator first
root = root[:n-1]
// only preserve the trailing separator for a Unix/Windows/PSDrive root
if len(root) == 0 || (strings.HasPrefix(pt.pwd, root) && strings.HasSuffix(root, ":")) {
return root + pt.env.PathSeparator()
} }
pwd = pt.replaceMappedLocations(pwd) return root
return pwd
} }
func (pt *Path) normalize(inputPath string) string { func (pt *Path) normalize(inputPath string) string {
normalized := inputPath normalized := inputPath
if strings.HasPrefix(inputPath, "~") { if strings.HasPrefix(normalized, "~") && (len(normalized) == 1 || environment.IsPathSeparator(pt.env, normalized[1])) {
normalized = pt.env.Home() + normalized[1:] normalized = pt.env.Home() + normalized[1:]
} }
normalized = strings.ReplaceAll(normalized, "\\", "/") switch pt.env.GOOS() {
goos := pt.env.GOOS() case environment.WINDOWS:
if goos == environment.WINDOWS || goos == environment.DARWIN { normalized = strings.ReplaceAll(normalized, "/", `\`)
fallthrough
case environment.DARWIN:
normalized = strings.ToLower(normalized) normalized = strings.ToLower(normalized)
} }
if !strings.HasSuffix(normalized, "/") {
normalized += "/"
}
return normalized return normalized
} }
func (pt *Path) replaceMappedLocations(pwd string) string { func (pt *Path) replaceMappedLocations(pwd string) string {
if strings.HasPrefix(pwd, "Microsoft.PowerShell.Core\\FileSystem::") {
pwd = strings.Replace(pwd, "Microsoft.PowerShell.Core\\FileSystem::", "", 1)
}
mappedLocations := map[string]string{} mappedLocations := map[string]string{}
// predefined mapped locations, can be disabled
if pt.props.GetBool(MappedLocationsEnabled, true) { if pt.props.GetBool(MappedLocationsEnabled, true) {
mappedLocations[pt.normalize("hkcu:")] = pt.props.GetString(WindowsRegistryIcon, "\uF013") mappedLocations["hkcu:"] = pt.props.GetString(WindowsRegistryIcon, "\uF013")
mappedLocations[pt.normalize("hklm:")] = pt.props.GetString(WindowsRegistryIcon, "\uF013") mappedLocations["hklm:"] = pt.props.GetString(WindowsRegistryIcon, "\uF013")
mappedLocations[pt.normalize(pt.env.Home())] = pt.props.GetString(HomeIcon, "~") mappedLocations[pt.normalize(pt.env.Home())] = pt.props.GetString(HomeIcon, "~")
} }
@ -369,37 +403,48 @@ func (pt *Path) replaceMappedLocations(pwd string) string {
// mapped locations can override predefined locations // mapped locations can override predefined locations
keyValues := pt.props.GetKeyValueMap(MappedLocations, make(map[string]string)) keyValues := pt.props.GetKeyValueMap(MappedLocations, make(map[string]string))
for key, val := range keyValues { for key, val := range keyValues {
mappedLocations[pt.normalize(key)] = val if key != "" {
mappedLocations[pt.normalize(key)] = val
}
} }
// sort map keys in reverse order // sort map keys in reverse order
// fixes case when a subfoder and its parent are mapped // fixes case when a subfoder and its parent are mapped
// ex /users/test and /users/test/dev // ex /users/test and /users/test/dev
keys := make([]string, len(mappedLocations)) keys := make([]string, 0, len(mappedLocations))
i := 0
for k := range mappedLocations { for k := range mappedLocations {
keys[i] = k keys = append(keys, k)
i++
} }
sort.Sort(sort.Reverse(sort.StringSlice(keys))) sort.Sort(sort.Reverse(sort.StringSlice(keys)))
normalizedPwd := pt.normalize(pwd) cleanPwdRoot, cleanPwdPath := environment.ParsePath(pt.env, pwd)
pwdRoot := pt.normalize(cleanPwdRoot)
pwdPath := pt.normalize(cleanPwdPath)
for _, key := range keys { for _, key := range keys {
if strings.HasPrefix(normalizedPwd, key) { keyRoot, keyPath := environment.ParsePath(pt.env, key)
replacement := mappedLocations[key] if keyRoot != pwdRoot || !strings.HasPrefix(pwdPath, keyPath) {
// -1 as we want to ignore the trailing slash continue
// set by the normalize function }
return replacement + pwd[len(key)-1:] value := mappedLocations[key]
rem := cleanPwdPath[len(keyPath):]
if len(rem) == 0 {
// exactly match the full path
return value
}
if len(keyPath) == 0 {
// only match the root
return value + pt.env.PathSeparator() + cleanPwdPath
}
// match several prefix elements
if rem[0:1] == pt.env.PathSeparator() {
return value + rem
} }
} }
return pwd return cleanPwdRoot + cleanPwdPath
} }
func (pt *Path) replaceFolderSeparators(pwd string) string { func (pt *Path) replaceFolderSeparators(pwd string) string {
defaultSeparator := pt.env.PathSeparator() defaultSeparator := pt.env.PathSeparator()
if pwd == defaultSeparator {
return pwd
}
folderSeparator := pt.getFolderSeparator() folderSeparator := pt.getFolderSeparator()
if folderSeparator == defaultSeparator { if folderSeparator == defaultSeparator {
return pwd return pwd
@ -408,22 +453,3 @@ func (pt *Path) replaceFolderSeparators(pwd string) string {
pwd = strings.ReplaceAll(pwd, defaultSeparator, folderSeparator) pwd = strings.ReplaceAll(pwd, defaultSeparator, folderSeparator)
return pwd return pwd
} }
func (pt *Path) rootLocation() string {
pwd := pt.getPwd()
pwd = strings.TrimPrefix(pwd, pt.env.PathSeparator())
splitted := strings.Split(pwd, pt.env.PathSeparator())
rootLocation := splitted[0]
return rootLocation
}
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 - 1
}

File diff suppressed because it is too large Load diff

View file

@ -100,22 +100,22 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
function Set-PoshContext {} function Set-PoshContext {}
function Get-PoshContext { function Get-CleanPSWD {
$cleanPWD = $PWD.ProviderPath $pswd = $PWD.ToString()
$cleanPSWD = $PWD.ToString() if ($pswd -ne '/') {
$cleanPWD = $cleanPWD.TrimEnd('\') return $pswd.TrimEnd('\') -replace '^Microsoft\.PowerShell\.Core\\FileSystem::', ''
$cleanPSWD = $cleanPSWD.TrimEnd('\') }
return $cleanPWD, $cleanPSWD return $pswd
} }
if (("::TOOLTIPS::" -eq "true") -and ($ExecutionContext.SessionState.LanguageMode -ne "ConstrainedLanguage")) { if (("::TOOLTIPS::" -eq "true") -and ($ExecutionContext.SessionState.LanguageMode -ne "ConstrainedLanguage")) {
Set-PSReadLineKeyHandler -Key Spacebar -BriefDescription 'OhMyPoshSpaceKeyHandler' -ScriptBlock { Set-PSReadLineKeyHandler -Key Spacebar -BriefDescription 'OhMyPoshSpaceKeyHandler' -ScriptBlock {
[Microsoft.PowerShell.PSConsoleReadLine]::Insert(' ') [Microsoft.PowerShell.PSConsoleReadLine]::Insert(' ')
$position = $host.UI.RawUI.CursorPosition $position = $host.UI.RawUI.CursorPosition
$cleanPWD, $cleanPSWD = Get-PoshContext $cleanPSWD = Get-CleanPSWD
$command = $null $command = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$command, [ref]$null) [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$command, [ref]$null)
$standardOut = @(Start-Utf8Process $script:OMPExecutable @("print", "tooltip", "--error=$script:ErrorCode", "--pwd=$cleanPWD", "--shell=$script:ShellName", "--pswd=$cleanPSWD", "--config=$env:POSH_THEME", "--command=$command", "--shell-version=$script:PSVersion")) $standardOut = @(Start-Utf8Process $script:OMPExecutable @("print", "tooltip", "--error=$script:ErrorCode", "--shell=$script:ShellName", "--pswd=$cleanPSWD", "--config=$env:POSH_THEME", "--command=$command", "--shell-version=$script:PSVersion"))
Write-Host $standardOut -NoNewline Write-Host $standardOut -NoNewline
$host.UI.RawUI.CursorPosition = $position $host.UI.RawUI.CursorPosition = $position
# we need this workaround to prevent the text after cursor from disappearing when the tooltip is rendered # we need this workaround to prevent the text after cursor from disappearing when the tooltip is rendered
@ -259,9 +259,10 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
if ($List -eq $true) { if ($List -eq $true) {
$themes | Select-Object @{ Name = 'hyperlink'; Expression = { Get-FileHyperlink -uri $_.FullName } } | Format-Table -HideTableHeaders $themes | Select-Object @{ Name = 'hyperlink'; Expression = { Get-FileHyperlink -uri $_.FullName } } | Format-Table -HideTableHeaders
} else { } else {
$cleanPSWD = Get-CleanPSWD
$themes | ForEach-Object -Process { $themes | ForEach-Object -Process {
Write-Host "Theme: $(Get-FileHyperlink -uri $_.FullName -Name ($_.BaseName -replace '\.omp$', ''))`n" Write-Host "Theme: $(Get-FileHyperlink -uri $_.FullName -Name ($_.BaseName -replace '\.omp$', ''))`n"
@(Start-Utf8Process $script:OMPExecutable @("print", "primary", "--config=$($_.FullName)", "--pwd=$PWD", "--shell=$script:ShellName")) @(Start-Utf8Process $script:OMPExecutable @("print", "primary", "--config=$($_.FullName)", "--pswd=$cleanPSWD", "--shell=$script:ShellName"))
Write-Host "`n" Write-Host "`n"
} }
} }
@ -331,7 +332,7 @@ Example:
if ($script:PromptType -ne 'transient') { if ($script:PromptType -ne 'transient') {
Update-PoshErrorCode Update-PoshErrorCode
} }
$cleanPWD, $cleanPSWD = Get-PoshContext $cleanPSWD = Get-CleanPSWD
$stackCount = global:Get-PoshStackCount $stackCount = global:Get-PoshStackCount
Set-PoshContext Set-PoshContext
$terminalWidth = $Host.UI.RawUI.WindowSize.Width $terminalWidth = $Host.UI.RawUI.WindowSize.Width
@ -339,7 +340,7 @@ Example:
if (-not $terminalWidth) { if (-not $terminalWidth) {
$terminalWidth = 0 $terminalWidth = 0
} }
$standardOut = @(Start-Utf8Process $script:OMPExecutable @("print", $script:PromptType, "--error=$script:ErrorCode", "--pwd=$cleanPWD", "--pswd=$cleanPSWD", "--execution-time=$script:ExecutionTime", "--stack-count=$stackCount", "--config=$env:POSH_THEME", "--shell-version=$script:PSVersion", "--terminal-width=$terminalWidth", "--shell=$script:ShellName")) $standardOut = @(Start-Utf8Process $script:OMPExecutable @("print", $script:PromptType, "--error=$script:ErrorCode", "--pswd=$cleanPSWD", "--execution-time=$script:ExecutionTime", "--stack-count=$stackCount", "--config=$env:POSH_THEME", "--shell-version=$script:PSVersion", "--terminal-width=$terminalWidth", "--shell=$script:ShellName"))
# make sure PSReadLine knows if we have a multiline prompt # make sure PSReadLine knows if we have a multiline prompt
Set-PSReadLineOption -ExtraPromptLineCount (($standardOut | Measure-Object -Line).Lines - 1) Set-PSReadLineOption -ExtraPromptLineCount (($standardOut | Measure-Object -Line).Lines - 1)
# the output can be multiline, joining these ensures proper rendering by adding line breaks with `n # the output can be multiline, joining these ensures proper rendering by adding line breaks with `n

View file

@ -42,9 +42,9 @@ Display the current path.
## Mapped Locations ## Mapped Locations
Allows you to override a location with an icon. It validates if the current path **starts with** the value and replaces Allows you to override a location with an icon/string.
it with the icon if there's a match. To avoid issues with nested overrides, Oh My Posh will sort the list of mapped It validates if the current path **starts with the specific elements** and replaces it with the icon/string if there's a match.
locations before doing a replacement. To avoid issues with nested overrides, Oh My Posh will sort the list of mapped locations before doing a replacement.
| Name | Type | Description | | Name | Type | Description |
| -------------------------- | --------- | -------------------------------------------------------------------------------------------------------- | | -------------------------- | --------- | -------------------------------------------------------------------------------------------------------- |
@ -63,8 +63,8 @@ For example, to swap out `C:\Users\Leet\GitHub` with a GitHub icon, you can do t
### Notes ### Notes
- Oh My Posh will accept both `/` and `\` as path separators for a mapped location and will match regardless of which - To make mapped Locations work cross-platform, you should use `/` as the path separator, Oh My Posh will
is used by the current operating system. automatically match effective separators based on the running operating system.
- The character `~` at the start of a mapped location will match the user's home directory. - 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. - The match is case-insensitive on Windows and macOS, but case-sensitive on other operating systems.
@ -87,8 +87,8 @@ Style sets the way the path is displayed. Based on previous experience and popul
### Agnoster ### Agnoster
Renders each folder as the `folder_icon` separated by the `folder_separator_icon`. Renders each intermediate folder as the `folder_icon` separated by the `folder_separator_icon`.
Only the current folder name is displayed at the end. The first and the last (current) folder name are always displayed as-is.
### Agnoster Full ### Agnoster Full
@ -96,17 +96,18 @@ Renders each folder name separated by the `folder_separator_icon`.
### Agnoster Short ### Agnoster Short
When more than `max_depth` levels deep, it renders one `folder_icon` (if `hide_root_location` is `false`) followed by When more than `max_depth` levels deep, it renders one `folder_icon` (if `hide_root_location` is `false`,
the names of the last `max_depth` folders, separated by the `folder_separator_icon`. which means the root folder does not count for depth) followed by the names of the last `max_depth` folders,
separated by the `folder_separator_icon`.
### Agnoster Left ### Agnoster Left
Renders each folder as the `folder_icon` separated by the `folder_separator_icon`. Renders each folder as the `folder_icon` separated by the `folder_separator_icon`.
Only the root folder name and it's child are displayed in full. Only the first folder name and its child are displayed in full.
### Full ### Full
Display `$PWD` as a string. Display the current working directory as a full string with each folder separated by the `folder_separator_icon`.
### Folder ### Folder
@ -114,13 +115,13 @@ Display the name of the current folder.
### Mixed ### Mixed
Works like `Agnoster Full`, but for any middle folder short enough it will display its name instead. The maximum length Works like `Agnoster`, but for any intermediate folder name that is short enough, it will be displayed as-is.
for the folders to display is governed by the `mixed_threshold` property. The maximum length for the folders to display is governed by the `mixed_threshold` property.
### Letter ### Letter
Works like `Full`, but will write every subfolder name using the first letter only, except when the folder name Works like `Agnoster Full`, but will write every folder name using the first letter only, except when the folder name
starts with a symbol or icon. starts with a symbol or icon. Specially, the last (current) folder name is always displayed in full.
- `folder` will be shortened to `f` - `folder` will be shortened to `f`
- `.config` will be shortened to `.c` - `.config` will be shortened to `.c`
@ -132,9 +133,9 @@ starts with a symbol or icon.
Works like `Letter`, but will make sure every folder name is the shortest unique value. Works like `Letter`, but will make sure every folder name is the shortest unique value.
The uniqueness refers to the displayed path, so `C:\dev\dev\dev\development` will be displayed as The uniqueness refers to the displayed path, so `C:\dev\dev\dev\development` will be displayed as
`C:\d\de\dev\development` (instead of `C:\d\d\d\development` for `Letter`). Uniqueness does _not_ refer to other `C\d\de\dev\development` (instead of `C\d\d\d\development` for `Letter`). Uniqueness does **not** refer to other
folders at the same level, so if `C:\projectA\dev` and `C:\projectB\dev` exist, then both will be displayed as folders at the same level, so if `C:\projectA\dev` and `C:\projectB\dev` exist, then both will be displayed as
`C:\p\dev`. `C\p\dev`.
## Template ([info][templates]) ## Template ([info][templates])
@ -148,12 +149,12 @@ folders at the same level, so if `C:\projectA\dev` and `C:\projectB\dev` exist,
### Properties ### Properties
| Name | Type | Description | | Name | Type | Description |
| ------------- | --------- | ---------------------------------------------------------------------------- | | ------------- | --------- | ---------------------------------------------------------------------------- |
| `.Path` | `string` | the current directory (based on the `style` property) | | `.Path` | `string` | the current directory (based on the `style` property) |
| `.Parent` | `string` | the current directory's parent folder (designed for use with style `folder`) | | `.Parent` | `string` | the current directory's parent folder which ends with a path separator (designed for use with style `folder`, it is empty if `.Path` contains only one single element) |
| `.Location` | `string` | the current directory (raw value) | | `.Location` | `string` | the current directory (raw value) |
| `.StackCount` | `int` | the stack count | | `.StackCount` | `int` | the stack count |
| `.Writable` | `boolean` | is the current directory writable by the user or not | | `.Writable` | `boolean` | is the current directory writable by the user or not |
[templates]: /docs/configuration/templates [templates]: /docs/configuration/templates