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"
// LineBreak creates a line break in the prompt
LineBreak BlockType = "newline"
// RPrompt a right aligned prompt in ZSH and Powershell
// RPrompt is a right aligned prompt
RPrompt BlockType = "rprompt"
// Left aligns 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) {
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
// 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.

View file

@ -337,11 +337,16 @@ func (env *ShellEnvironment) Pwd() string {
}
correctPath := func(pwd string) string {
// 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 env.GOOS() == WINDOWS {
driveLetter := regex.GetCompiledRegex(`^[a-z]:`)
return driveLetter.ReplaceAllStringFunc(pwd, strings.ToUpper)
}
return 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
}
dir, err := os.Getwd()
@ -735,6 +740,9 @@ func (env *ShellEnvironment) TemplateCache() *TemplateCache {
pwd := env.Pwd()
tmplCache.PWD = ReplaceHomeDirPrefixWithTilde(env, pwd)
tmplCache.Folder = Base(env, pwd)
if env.GOOS() == WINDOWS && strings.HasSuffix(tmplCache.Folder, ":") {
tmplCache.Folder += `\`
}
tmplCache.UserName = env.User()
if host, err := env.Host(); err == nil {
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 {
normalizedCwd := strings.ReplaceAll(dir, "\\", "/")
normalizedHomeDir := strings.ReplaceAll(home, "\\", "/")
if goos == WINDOWS {
dir = strings.ReplaceAll(dir, "\\", "/")
home = strings.ReplaceAll(home, "\\", "/")
}
for _, element := range regexes {
normalizedElement := strings.ReplaceAll(element, "\\\\", "/")
if strings.HasPrefix(normalizedElement, "~") {
normalizedElement = strings.Replace(normalizedElement, "~", normalizedHomeDir, 1)
normalizedElement = strings.Replace(normalizedElement, "~", home, 1)
}
pattern := fmt.Sprintf("^%s$", normalizedElement)
if goos == WINDOWS || goos == DARWIN {
pattern = "(?i)" + pattern
}
matched := regex.MatchString(pattern, normalizedCwd)
matched := regex.MatchString(pattern, dir)
if matched {
return true
}
@ -786,7 +796,7 @@ func dirMatchesOneOf(dir, home, goos string, regexes []string) bool {
return false
}
func isPathSeparator(env Environment, c uint8) bool {
func IsPathSeparator(env Environment, c uint8) bool {
if c == '/' {
return true
}
@ -800,14 +810,14 @@ func isPathSeparator(env Environment, c uint8) bool {
// Trailing path separators are removed before extracting the last element.
// If the path consists entirely of separators, Base returns a single separator.
func Base(env Environment, path string) string {
if path == "/" {
return path
}
volumeName := filepath.VolumeName(path)
// 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]
}
if len(path) == 0 {
return env.PathSeparator()
}
if volumeName == path {
return path
}
@ -815,22 +825,69 @@ func Base(env Environment, path string) string {
path = path[len(filepath.VolumeName(path)):]
// Find the last element
i := len(path) - 1
for i >= 0 && !isPathSeparator(env, path[i]) {
for i >= 0 && !IsPathSeparator(env, path[i]) {
i--
}
if i >= 0 {
path = path[i+1:]
}
// If empty now, it had only slashes.
if path == "" {
if len(path) == 0 {
return env.PathSeparator()
}
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 {
if strings.HasPrefix(path, env.Home()) {
return strings.Replace(path, env.Home(), "~", 1)
home := env.Home()
// 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
}

View file

@ -22,29 +22,6 @@ func TestHostNameWithLan(t *testing.T) {
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) {
cases := []struct {
GOOS string

View file

@ -5,8 +5,8 @@ import (
"oh-my-posh/environment"
"oh-my-posh/properties"
"oh-my-posh/regex"
"oh-my-posh/shell"
"oh-my-posh/template"
"path/filepath"
"sort"
"strings"
)
@ -45,7 +45,7 @@ const (
Full string = "full"
// Folder displays the current 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"
// Letter like agnoster, but with the first letter of each folder name
Letter string = "letter"
@ -70,64 +70,33 @@ func (pt *Path) Template() string {
}
func (pt *Path) Enabled() bool {
pt.pwd = pt.env.Pwd()
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)
pt.setPath()
if pt.env.IsWsl() {
pt.Location, _ = pt.env.RunCommand("wslpath", "-m", pt.pwd)
} else {
pt.Location = pt.pwd
}
pt.StackCount = pt.env.StackCount()
pt.Writable = pt.env.DirIsWritable(pt.pwd)
return true
}
func (pt *Path) Parent() string {
if pt.pwd == pt.env.Home() {
pwd := pt.getPwd()
if len(pwd) == 0 {
return ""
}
parent := filepath.Dir(pt.pwd)
if pt.pwd == parent {
root, path := environment.ParsePath(pt.env, pwd)
if len(path) == 0 {
// a root path has no parent
return ""
}
separator := pt.env.PathSeparator()
if parent == pt.rootLocation() || parent == separator {
separator = ""
base := environment.Base(pt.env, path)
path = pt.replaceFolderSeparators(path[:len(path)-len(base)])
if root != pt.env.PathSeparator() {
root = root[:len(root)-1] + pt.getFolderSeparator()
}
return pt.replaceMappedLocations(parent) + separator
}
func (pt *Path) formatWindowsDrive(pwd string) string {
if pt.env.GOOS() != environment.WINDOWS || !strings.HasSuffix(pwd, ":") {
return pwd
}
return pwd + "\\"
return root + path
}
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
}
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 {
separatorTemplate := pt.props.GetString(FolderSeparatorTemplate, "")
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{
Template: separatorTemplate,
@ -149,67 +160,66 @@ func (pt *Path) getFolderSeparator() string {
if err != nil {
pt.env.Log(environment.Error, "getFolderSeparator", err.Error())
}
if len(text) == 0 {
return pt.env.PathSeparator()
}
return text
}
func (pt *Path) getMixedPath() string {
func (pt *Path) getMixedPath(root, path string) string {
var buffer strings.Builder
pwd := pt.getPwd()
splitted := strings.Split(pwd, pt.env.PathSeparator())
threshold := int(pt.props.GetFloat64(MixedThreshold, 4))
for i, part := range splitted {
if part == "" {
continue
}
folder := part
if len(part) > threshold && i != 0 && i != len(splitted)-1 {
folder = pt.props.GetString(FolderIcon, "..")
}
separator := pt.getFolderSeparator()
if i == 0 {
separator = ""
folderIcon := pt.props.GetString(FolderIcon, "..")
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])
for i := 1; i < n; i++ {
folder := elements[i]
if len(folder) > threshold && i != n-1 {
folder = folderIcon
}
buffer.WriteString(fmt.Sprintf("%s%s", separator, folder))
}
return buffer.String()
}
func (pt *Path) getAgnosterPath() string {
func (pt *Path) getAgnosterPath(root, path string) string {
var buffer strings.Builder
pwd := pt.getPwd()
buffer.WriteString(pt.rootLocation())
pathDepth := pt.pathDepth(pwd)
folderIcon := pt.props.GetString(FolderIcon, "..")
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))
}
if pathDepth > 0 {
buffer.WriteString(fmt.Sprintf("%s%s", separator, environment.Base(pt.env, pwd)))
if n > 1 {
buffer.WriteString(fmt.Sprintf("%s%s", separator, elements[n-1]))
}
return buffer.String()
}
func (pt *Path) getAgnosterLeftPath() 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])
}
func (pt *Path) getAgnosterLeftPath(root, path string) string {
var buffer strings.Builder
buffer.WriteString(fmt.Sprintf("%s%s%s", splitted[0], separator, splitted[1]))
for i := 2; i < len(splitted); i++ {
folderIcon := pt.props.GetString(FolderIcon, "..")
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))
}
return buffer.String()
@ -218,7 +228,7 @@ func (pt *Path) getAgnosterLeftPath() string {
func (pt *Path) getRelevantLetter(folder string) string {
// check if there is at least a letter we can use
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
return folder
}
@ -228,36 +238,36 @@ func (pt *Path) getRelevantLetter(folder string) string {
return letter
}
func (pt *Path) getLetterPath() string {
func (pt *Path) getLetterPath(root, path string) string {
var buffer strings.Builder
pwd := pt.getPwd()
splitted := strings.Split(pwd, pt.env.PathSeparator())
separator := pt.getFolderSeparator()
for i := 0; i < len(splitted)-1; i++ {
folder := splitted[i]
if len(folder) == 0 {
continue
elements := strings.Split(path, pt.env.PathSeparator())
if root != pt.env.PathSeparator() {
elements = append([]string{root[:len(root)-1]}, elements...)
}
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(fmt.Sprintf("%s%s", letter, 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()
}
func (pt *Path) getUniqueLettersPath() string {
func (pt *Path) getUniqueLettersPath(root, path string) string {
var buffer strings.Builder
pwd := pt.getPwd()
splitted := strings.Split(pwd, pt.env.PathSeparator())
separator := pt.getFolderSeparator()
letters := make(map[string]bool, len(splitted))
for i := 0; i < len(splitted)-1; i++ {
folder := splitted[i]
if len(folder) == 0 {
continue
}
elements := strings.Split(path, pt.env.PathSeparator())
if root != pt.env.PathSeparator() {
elements = append([]string{root[:len(root)-1]}, elements...)
}
n := len(elements)
letters := make(map[string]bool)
for i := 0; i < n-1; i++ {
folder := elements[i]
letter := pt.getRelevantLetter(folder)
for letters[letter] {
if letter == folder {
@ -266,102 +276,126 @@ func (pt *Path) getUniqueLettersPath() string {
letter += folder[len(letter) : len(letter)+1]
}
letters[letter] = true
buffer.WriteString(fmt.Sprintf("%s%s", letter, separator))
}
if len(splitted) > 0 {
buffer.WriteString(splitted[len(splitted)-1])
if i != 0 {
buffer.WriteString(separator)
}
buffer.WriteString(letter)
}
buffer.WriteString(fmt.Sprintf("%s%s", separator, elements[n-1]))
return buffer.String()
}
func (pt *Path) getAgnosterFullPath() string {
pwd := pt.getPwd()
for len(pwd) > 1 && string(pwd[0]) == pt.env.PathSeparator() {
pwd = pwd[1:]
func (pt *Path) getAgnosterFullPath(root, path string) string {
path = pt.replaceFolderSeparators(path)
if root == pt.env.PathSeparator() {
return path
}
return pt.replaceFolderSeparators(pwd)
root = root[:len(root)-1] + pt.getFolderSeparator()
return root + path
}
func (pt *Path) getAgnosterShortPath() string {
pwd := pt.getPwd()
pathDepth := pt.pathDepth(pwd)
func (pt *Path) getAgnosterShortPath(root, path string) string {
elements := strings.Split(path, pt.env.PathSeparator())
if root != pt.env.PathSeparator() {
elements = append([]string{root[:len(root)-1]}, elements...)
}
depth := len(elements)
maxDepth := pt.props.GetInt(MaxDepth, 1)
if maxDepth < 1 {
maxDepth = 1
}
hideRootLocation := pt.props.GetBool(HideRootLocation, false)
if hideRootLocation {
// 1-indexing to avoid showing the root location when exceeding the max depth
pathDepth++
if !hideRootLocation {
maxDepth++
}
if pathDepth <= maxDepth {
return pt.getAgnosterFullPath()
if depth <= maxDepth {
return pt.getAgnosterFullPath(root, path)
}
folderSeparator := pt.getFolderSeparator()
pathSeparator := pt.env.PathSeparator()
splitted := strings.Split(pwd, pathSeparator)
fullPathDepth := len(splitted)
splitPos := fullPathDepth - maxDepth
separator := pt.getFolderSeparator()
folderIcon := pt.props.GetString(FolderIcon, "..")
var buffer strings.Builder
if hideRootLocation {
buffer.WriteString(splitted[splitPos])
splitPos++
} else {
folderIcon := pt.props.GetString(FolderIcon, "..")
root := pt.rootLocation()
buffer.WriteString(fmt.Sprintf("%s%s%s", root, folderSeparator, folderIcon))
if !hideRootLocation {
buffer.WriteString(fmt.Sprintf("%s%s", elements[0], separator))
maxDepth--
}
for i := splitPos; i < fullPathDepth; i++ {
buffer.WriteString(fmt.Sprintf("%s%s", folderSeparator, splitted[i]))
splitPos := depth - maxDepth
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()
}
func (pt *Path) getFullPath() string {
pwd := pt.getPwd()
return pt.replaceFolderSeparators(pwd)
func (pt *Path) getFullPath(root, path string) string {
if root != pt.env.PathSeparator() {
root = root[:len(root)-1] + pt.getFolderSeparator()
}
path = pt.replaceFolderSeparators(path)
return root + path
}
func (pt *Path) getFolderPath() string {
pwd := pt.getPwd()
pwd = environment.Base(pt.env, pwd)
return pt.replaceFolderSeparators(pwd)
func (pt *Path) getFolderPath(path string) string {
return environment.Base(pt.env, path)
}
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 {
pwd := pt.env.Flags().PSWD
if pwd == "" {
pwd = pt.env.Pwd()
pt.setPwd()
return pt.replaceMappedLocations(pt.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 pwd
return root
}
func (pt *Path) normalize(inputPath string) string {
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 = strings.ReplaceAll(normalized, "\\", "/")
goos := pt.env.GOOS()
if goos == environment.WINDOWS || goos == environment.DARWIN {
switch pt.env.GOOS() {
case environment.WINDOWS:
normalized = strings.ReplaceAll(normalized, "/", `\`)
fallthrough
case environment.DARWIN:
normalized = strings.ToLower(normalized)
}
if !strings.HasSuffix(normalized, "/") {
normalized += "/"
}
return normalized
}
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{}
// predefined mapped locations, can be disabled
if pt.props.GetBool(MappedLocationsEnabled, true) {
mappedLocations[pt.normalize("hkcu:")] = pt.props.GetString(WindowsRegistryIcon, "\uF013")
mappedLocations[pt.normalize("hklm:")] = pt.props.GetString(WindowsRegistryIcon, "\uF013")
mappedLocations["hkcu:"] = pt.props.GetString(WindowsRegistryIcon, "\uF013")
mappedLocations["hklm:"] = pt.props.GetString(WindowsRegistryIcon, "\uF013")
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
keyValues := pt.props.GetKeyValueMap(MappedLocations, make(map[string]string))
for key, val := range keyValues {
mappedLocations[pt.normalize(key)] = val
if key != "" {
mappedLocations[pt.normalize(key)] = val
}
}
// 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, len(mappedLocations))
i := 0
keys := make([]string, 0, len(mappedLocations))
for k := range mappedLocations {
keys[i] = k
i++
keys = append(keys, k)
}
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 {
if strings.HasPrefix(normalizedPwd, key) {
replacement := mappedLocations[key]
// -1 as we want to ignore the trailing slash
// set by the normalize function
return replacement + pwd[len(key)-1:]
keyRoot, keyPath := environment.ParsePath(pt.env, key)
if keyRoot != pwdRoot || !strings.HasPrefix(pwdPath, keyPath) {
continue
}
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 {
defaultSeparator := pt.env.PathSeparator()
if pwd == defaultSeparator {
return pwd
}
folderSeparator := pt.getFolderSeparator()
if folderSeparator == defaultSeparator {
return pwd
@ -408,22 +453,3 @@ func (pt *Path) replaceFolderSeparators(pwd string) string {
pwd = strings.ReplaceAll(pwd, defaultSeparator, folderSeparator)
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 Get-PoshContext {
$cleanPWD = $PWD.ProviderPath
$cleanPSWD = $PWD.ToString()
$cleanPWD = $cleanPWD.TrimEnd('\')
$cleanPSWD = $cleanPSWD.TrimEnd('\')
return $cleanPWD, $cleanPSWD
function Get-CleanPSWD {
$pswd = $PWD.ToString()
if ($pswd -ne '/') {
return $pswd.TrimEnd('\') -replace '^Microsoft\.PowerShell\.Core\\FileSystem::', ''
}
return $pswd
}
if (("::TOOLTIPS::" -eq "true") -and ($ExecutionContext.SessionState.LanguageMode -ne "ConstrainedLanguage")) {
Set-PSReadLineKeyHandler -Key Spacebar -BriefDescription 'OhMyPoshSpaceKeyHandler' -ScriptBlock {
[Microsoft.PowerShell.PSConsoleReadLine]::Insert(' ')
$position = $host.UI.RawUI.CursorPosition
$cleanPWD, $cleanPSWD = Get-PoshContext
$cleanPSWD = Get-CleanPSWD
$command = $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
$host.UI.RawUI.CursorPosition = $position
# 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) {
$themes | Select-Object @{ Name = 'hyperlink'; Expression = { Get-FileHyperlink -uri $_.FullName } } | Format-Table -HideTableHeaders
} else {
$cleanPSWD = Get-CleanPSWD
$themes | ForEach-Object -Process {
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"
}
}
@ -331,7 +332,7 @@ Example:
if ($script:PromptType -ne 'transient') {
Update-PoshErrorCode
}
$cleanPWD, $cleanPSWD = Get-PoshContext
$cleanPSWD = Get-CleanPSWD
$stackCount = global:Get-PoshStackCount
Set-PoshContext
$terminalWidth = $Host.UI.RawUI.WindowSize.Width
@ -339,7 +340,7 @@ Example:
if (-not $terminalWidth) {
$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
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

View file

@ -42,9 +42,9 @@ Display the current path.
## Mapped Locations
Allows you to override a location with an icon. It validates if the current path **starts with** the value and replaces
it with the icon if there's a match. To avoid issues with nested overrides, Oh My Posh will sort the list of mapped
locations before doing a replacement.
Allows you to override a location with an icon/string.
It validates if the current path **starts with the specific elements** and replaces it with the icon/string if there's a match.
To avoid issues with nested overrides, Oh My Posh will sort the list of mapped locations before doing a replacement.
| 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
- Oh My Posh will accept both `/` and `\` as path separators for a mapped location and will match regardless of which
is used by the current operating system.
- To make mapped Locations work cross-platform, you should use `/` as the path separator, Oh My Posh will
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 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
Renders each folder as the `folder_icon` separated by the `folder_separator_icon`.
Only the current folder name is displayed at the end.
Renders each intermediate folder as the `folder_icon` separated by the `folder_separator_icon`.
The first and the last (current) folder name are always displayed as-is.
### Agnoster Full
@ -96,17 +96,18 @@ Renders each folder name separated by the `folder_separator_icon`.
### Agnoster Short
When more than `max_depth` levels deep, it renders one `folder_icon` (if `hide_root_location` is `false`) followed by
the names of the last `max_depth` folders, separated by the `folder_separator_icon`.
When more than `max_depth` levels deep, it renders one `folder_icon` (if `hide_root_location` is `false`,
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
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
Display `$PWD` as a string.
Display the current working directory as a full string with each folder separated by the `folder_separator_icon`.
### Folder
@ -114,13 +115,13 @@ Display the name of the current folder.
### Mixed
Works like `Agnoster Full`, but for any middle folder short enough it will display its name instead. The maximum length
for the folders to display is governed by the `mixed_threshold` property.
Works like `Agnoster`, but for any intermediate folder name that is short enough, it will be displayed as-is.
The maximum length for the folders to display is governed by the `mixed_threshold` property.
### Letter
Works like `Full`, but will write every subfolder name using the first letter only, except when the folder name
starts with a symbol or icon.
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. Specially, the last (current) folder name is always displayed in full.
- `folder` will be shortened to `f`
- `.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.
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
`C:\p\dev`.
`C\p\dev`.
## Template ([info][templates])
@ -148,12 +149,12 @@ folders at the same level, so if `C:\projectA\dev` and `C:\projectB\dev` exist,
### Properties
| Name | Type | Description |
| Name | Type | Description |
| ------------- | --------- | ---------------------------------------------------------------------------- |
| `.Path` | `string` | the current directory (based on the `style` property) |
| `.Parent` | `string` | the current directory's parent folder (designed for use with style `folder`) |
| `.Location` | `string` | the current directory (raw value) |
| `.StackCount` | `int` | the stack count |
| `.Writable` | `boolean` | is the current directory writable by the user or not |
| `.Path` | `string` | the current directory (based on the `style` property) |
| `.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) |
| `.StackCount` | `int` | the stack count |
| `.Writable` | `boolean` | is the current directory writable by the user or not |
[templates]: /docs/configuration/templates