From 5db251ce53f19173cfa5642b57f30a4644b3457b Mon Sep 17 00:00:00 2001 From: Aaron Sherber Date: Sat, 18 Sep 2021 16:03:00 -0400 Subject: [PATCH] feat(path): flexible matching for mapped-locations --- docs/docs/segment-path.md | 16 +++++++-- src/segment_path.go | 25 ++++++++++--- src/segment_path_test.go | 74 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 8 deletions(-) diff --git a/docs/docs/segment-path.md b/docs/docs/segment-path.md index 97d2f81c..3c96b31f 100644 --- a/docs/docs/segment-path.md +++ b/docs/docs/segment-path.md @@ -43,12 +43,12 @@ 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 +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. - mapped_locations_enabled: `boolean` - replace known locations in the path with the replacements before applying the -style. defaults to `true` -- mapped_locations: `map[string]string` - custom glyph/text for specific paths. Works regardless of the `mapped_locations_enabled` +style - defaults to `true` +- mapped_locations: `object` - custom glyph/text for specific paths. Works regardless of the `mapped_locations_enabled` setting. For example, to swap out `C:\Users\Leet\GitHub` with a GitHub icon, you can do the following: @@ -59,6 +59,16 @@ 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. +- 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 +`C:\Users\Bill\Foo` or `C:\Users\Bill\foo` on Windows but only `/home/bill/Foo` on Linux. + ## Style Style sets the way the path is displayed. Based on previous experience and popular themes, there are 5 flavors. diff --git a/src/segment_path.go b/src/segment_path.go index ffafeb77..7c1aa480 100644 --- a/src/segment_path.go +++ b/src/segment_path.go @@ -220,6 +220,19 @@ func (pt *path) getPwd() string { return pwd } +func (pt *path) normalize(inputPath string) string { + normalized := inputPath + if strings.HasPrefix(inputPath, "~") { + normalized = pt.env.homeDir() + normalized[1:] + } + normalized = strings.ReplaceAll(normalized, "\\", "/") + goos := pt.env.getRuntimeGOOS() + if goos == windowsPlatform || goos == darwinPlatform { + normalized = strings.ToLower(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) @@ -229,14 +242,14 @@ func (pt *path) replaceMappedLocations(pwd string) string { if pt.props.getBool(MappedLocationsEnabled, true) { mappedLocations["HKCU:"] = pt.props.getString(WindowsRegistryIcon, "\uF013") mappedLocations["HKLM:"] = pt.props.getString(WindowsRegistryIcon, "\uF013") - mappedLocations[pt.env.homeDir()] = pt.props.getString(HomeIcon, "~") + mappedLocations[pt.normalize(pt.env.homeDir())] = pt.props.getString(HomeIcon, "~") } // 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 { - mappedLocations[key] = val + mappedLocations[pt.normalize(key)] = val } // sort map keys in reverse order @@ -250,9 +263,11 @@ func (pt *path) replaceMappedLocations(pwd string) string { } sort.Sort(sort.Reverse(sort.StringSlice(keys))) - for _, value := range keys { - if strings.HasPrefix(pwd, value) { - return strings.Replace(pwd, value, mappedLocations[value], 1) + normalizedPwd := pt.normalize(pwd) + for _, key := range keys { + if strings.HasPrefix(normalizedPwd, key) { + value := mappedLocations[key] + return value + pwd[len(key):] } } return pwd diff --git a/src/segment_path_test.go b/src/segment_path_test.go index 085688ee..a4016c93 100644 --- a/src/segment_path_test.go +++ b/src/segment_path_test.go @@ -220,6 +220,7 @@ func TestRootLocationHome(t *testing.T) { } env.On("getArgs", nil).Return(args) env.On("getPathSeperator", nil).Return(tc.PathSeperator) + env.On("getRuntimeGOOS", nil).Return("") path := &path{ env: env, props: props, @@ -247,6 +248,7 @@ func TestPathDepthMultipleLevelsDeep(t *testing.T) { } env := new(MockedEnvironment) env.On("getPathSeperator", nil).Return("/") + env.On("getRunteGOOS", nil).Return("") path := &path{ env: env, } @@ -500,12 +502,82 @@ func TestGetFullPath(t *testing.T) { } } +func TestGetFullPathCustomMappedLocations(t *testing.T) { + cases := []struct { + Pwd string + MappedLocations map[string]string + Expected string + }{ + {Pwd: "/a/b/c/d", MappedLocations: map[string]string{"/a/b/c/d": "#"}, Expected: "#"}, + {Pwd: "/a/b/c/d", MappedLocations: map[string]string{"\\a\\b": "#"}, Expected: "#/c/d"}, + {Pwd: "\\a\\b\\c\\d", MappedLocations: map[string]string{"\\a\\b": "#"}, Expected: "#\\c\\d"}, + {Pwd: "/a/b/c/d", MappedLocations: map[string]string{"/a/b": "#"}, Expected: "#/c/d"}, + {Pwd: "/a/b/c/d", MappedLocations: map[string]string{"/a/b": "/e/f"}, Expected: "/e/f/c/d"}, + {Pwd: "/usr/home/a/b/c/d", MappedLocations: map[string]string{"~\\a\\b": "#"}, Expected: "#/c/d"}, + {Pwd: "/usr/home/a/b/c/d", MappedLocations: map[string]string{"~/a/b": "#"}, Expected: "#/c/d"}, + {Pwd: "/a/usr/home/b/c/d", MappedLocations: map[string]string{"/a~": "#"}, Expected: "/a/usr/home/b/c/d"}, + {Pwd: "/usr/home/a/b/c/d", MappedLocations: map[string]string{"/a/b": "#"}, Expected: "/usr/home/a/b/c/d"}, + } + + for _, tc := range cases { + env := new(MockedEnvironment) + env.On("getPathSeperator", nil).Return("/") + env.On("homeDir", nil).Return("/usr/home") + env.On("getcwd", nil).Return(tc.Pwd) + env.On("getRuntimeGOOS", nil).Return("") + args := &args{ + PSWD: &tc.Pwd, + } + env.On("getArgs", nil).Return(args) + path := &path{ + env: env, + props: &properties{ + values: map[Property]interface{}{ + MappedLocationsEnabled: false, + MappedLocations: tc.MappedLocations, + }, + }, + } + got := path.getFullPath() + assert.Equal(t, tc.Expected, got) + } +} + +func TestNormalizePath(t *testing.T) { + cases := []struct { + Input string + GOOS string + Expected string + }{ + {Input: "C:\\Users\\Bob\\Foo", GOOS: linuxPlatform, Expected: "C:/Users/Bob/Foo"}, + {Input: "C:\\Users\\Bob\\Foo", GOOS: windowsPlatform, Expected: "c:/users/bob/foo"}, + {Input: "~\\Bob\\Foo", GOOS: linuxPlatform, Expected: "/usr/home/Bob/Foo"}, + {Input: "~\\Bob\\Foo", GOOS: windowsPlatform, Expected: "/usr/home/bob/foo"}, + {Input: "/foo/~/bar", GOOS: linuxPlatform, Expected: "/foo/~/bar"}, + {Input: "/foo/~/bar", GOOS: windowsPlatform, Expected: "/foo/~/bar"}, + {Input: "~/baz", GOOS: linuxPlatform, Expected: "/usr/home/baz"}, + {Input: "~/baz", GOOS: windowsPlatform, Expected: "/usr/home/baz"}, + } + + for _, tc := range cases { + env := new(MockedEnvironment) + env.On("homeDir", nil).Return("/usr/home") + env.On("getRuntimeGOOS", nil).Return(tc.GOOS) + pt := &path{ + env: env, + } + got := pt.normalize(tc.Input) + assert.Equal(t, tc.Expected, got) + } +} + func TestGetFolderPathCustomMappedLocations(t *testing.T) { pwd := "/a/b/c/d" env := new(MockedEnvironment) env.On("getPathSeperator", nil).Return("/") env.On("homeDir", nil).Return("/usr/home") env.On("getcwd", nil).Return(pwd) + env.On("getRuntimeGOOS", nil).Return("") args := &args{ PSWD: &pwd, } @@ -536,6 +608,7 @@ func testWritePathInfo(home, pwd, pathSeparator string) string { env.On("homeDir", nil).Return(home) env.On("getPathSeperator", nil).Return(pathSeparator) env.On("getcwd", nil).Return(pwd) + env.On("getRuntimeGOOS", nil).Return("") args := &args{ PSWD: &pwd, } @@ -635,6 +708,7 @@ func TestGetPwd(t *testing.T) { env.On("getPathSeperator", nil).Return("/") env.On("homeDir", nil).Return("/usr/home") env.On("getcwd", nil).Return(tc.Pwd) + env.On("getRuntimeGOOS", nil).Return("") args := &args{ PSWD: &tc.Pswd, }