Repository: JanDeDobbeleer/oh-my-posh
Pull request #1742 (2022-02-10 22:47:57)

feat: UI5 tooling segment
### Prerequisites

- [x] I have read and understood the `CONTRIBUTING` guide
- [x] The commit message follows the [conventional commits][cc] guidelines
- [x] Tests for the changes have been added (for bug fixes/features)
- [x] Docs have been added / updated (for bug fixes/features)

### Description

A new segment for UI5 tooling - https://sap.github.io/ui5-tooling/
The check is based on a configuration file, by default it is ui5.yaml, might be also ui5-dist.yaml or any other name, which you can provide via --config param; but most of the times it has the "ui5" text in it. 

By default the file resides in the UI5 project root, but I added additional check to detect it from subdirectories. Usually you have the structure like:
- myproject
  - src
    - views
    - controllers
    ...

The check from subdirectories is up to 4 levels up, which should be enough for the most projects. 
From my daily experience - you can work with a large number of UI5 apps even within one project and jump frequently between them. Having the information exposed whether the app has the tooling configured is useful (you can do things differently). 

Best Regards,
Jacek





Patch:
diff --git a/docs/docs/segment-ui5tooling.md b/docs/docs/segment-ui5tooling.md
new file mode 100644
index 00000000..2cd83622
--- /dev/null
+++ b/docs/docs/segment-ui5tooling.md
@@ -0,0 +1,61 @@
+---
+id: ui5tooling
+title: UI5 Tooling
+sidebar_label: UI5 Tooling
+---
+
+## What
+
+Display the active [UI5 tooling][ui5-homepage] version (global or local if present -
+see [the documentation][ui5-version-help]).
+
+## Sample Configuration
+
+```json
+{
+  "background": "#f5a834",
+  "foreground": "#100e23",
+  "powerline_symbol": "\ue0b0",
+  "properties": {
+    "template": " \ufab6ui5 {{ .Full }} "
+  },
+  "style": "powerline",
+  "type": "ui5tooling"
+}
+```
+
+## Properties
+
+- home_enabled: `boolean` - display the segment in the HOME folder or not - defaults to `false`
+- fetch_version: `boolean` - display the UI5 tooling version - defaults to `true`
+- display_error: `boolean` - show the error context when failing to retrieve the version information - defaults to `true`
+- missing_command_text: `string` - text to display when the java command is missing - defaults to empty
+- display_mode: `string` - determines when the segment is displayed
+  - `always`: the segment is always displayed
+  - `files`: the segment is only displayed when `*ui5*.y(a)ml` file is present in the current folder
+  - `context`: (default) the segment is only displayed when `*ui5*.y(a)ml` file is present in the current folder
+    or it has been found in the parent folders (check up to 4 levels)
+
+## Template ([info][templates])
+
+:::note default template
+
+```template
+{{ if .Error }}{{ .Error }}{{ else }}{{ .Full }}{{ end }}
+```
+
+:::
+
+## Template Properties
+
+- `.Full`: `string` - the full version
+- `.Major`: `string` - major number
+- `.Minor`: `string` - minor number
+- `.Patch`: `string` - patch number
+- `.Prerelease`: `string` - prerelease info text
+- `.BuildMetadata`: `string` - build metadata
+- `.Error`: `string` - when fetching the version string errors
+
+[templates]: /docs/config-templates
+[ui5-homepage]: https://sap.github.io/ui5-tooling
+[ui5-version-help]: https://sap.github.io/ui5-tooling/pages/CLI/#ui5-versions
diff --git a/docs/sidebars.js b/docs/sidebars.js
index b3550d80..5b248084 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -76,6 +76,7 @@ module.exports = {
         "terraform",
         "text",
         "time",
+        "ui5tooling",
         "wakatime",
         "wifi",
         "winreg",
diff --git a/src/engine/segment.go b/src/engine/segment.go
index 4acdab52..f7ab8f11 100644
--- a/src/engine/segment.go
+++ b/src/engine/segment.go
@@ -149,6 +149,8 @@ const (
 	IPIFY SegmentType = "ipify"
 	// HASKELL segment
 	HASKELL SegmentType = "haskell"
+	// UI5 Tooling segment
+	UI5TOOLING SegmentType = "ui5tooling"
 )
 
 func (segment *Segment) shouldIncludeFolder() bool {
@@ -266,6 +268,7 @@ func (segment *Segment) mapSegmentWithWriter(env environment.Environment) error
 		BREWFATHER:    &segments.Brewfather{},
 		IPIFY:         &segments.IPify{},
 		HASKELL:       &segments.Haskell{},
+		UI5TOOLING:    &segments.UI5Tooling{},
 	}
 	if segment.Properties == nil {
 		segment.Properties = make(properties.Map)
diff --git a/src/environment/shell.go b/src/environment/shell.go
index e0b8d54f..8c3eea59 100644
--- a/src/environment/shell.go
+++ b/src/environment/shell.go
@@ -150,6 +150,7 @@ type Environment interface {
 	HasFilesInDir(dir, pattern string) bool
 	HasFolder(folder string) bool
 	HasParentFilePath(path string) (fileInfo *FileInfo, err error)
+	HasFileInParentDirs(pattern string, depth uint) bool
 	HasCommand(command string) bool
 	FileContent(file string) string
 	FolderList(path string) []string
@@ -319,6 +320,25 @@ func (env *ShellEnvironment) HasFilesInDir(dir, pattern string) bool {
 	return len(matches) > 0
 }
 
+func (env *ShellEnvironment) HasFileInParentDirs(pattern string, depth uint) bool {
+	defer env.trace(time.Now(), "HasFileInParent", pattern, fmt.Sprint(depth))
+	currentFolder := env.Pwd()
+
+	for c := 0; c < int(depth); c++ {
+		if env.HasFilesInDir(currentFolder, pattern) {
+			return true
+		}
+
+		if dir := filepath.Dir(currentFolder); dir != currentFolder {
+			currentFolder = dir
+		} else {
+			return false
+		}
+	}
+
+	return false
+}
+
 func (env *ShellEnvironment) HasFolder(folder string) bool {
 	defer env.trace(time.Now(), "HasFolder", folder)
 	_, err := os.Stat(folder)
diff --git a/src/mock/environment.go b/src/mock/environment.go
index 147dd608..c7547180 100644
--- a/src/mock/environment.go
+++ b/src/mock/environment.go
@@ -209,3 +209,8 @@ func (env *MockedEnvironment) MockGitCommand(dir, returnValue string, args ...st
 	args = append([]string{"-C", dir, "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...)
 	env.On("RunCommand", "git", args).Return(returnValue, nil)
 }
+
+func (env *MockedEnvironment) HasFileInParentDirs(pattern string, depth uint) bool {
+	args := env.Called(pattern, depth)
+	return args.Bool(0)
+}
diff --git a/src/segments/ui5tooling.go b/src/segments/ui5tooling.go
new file mode 100644
index 00000000..ccd920ac
--- /dev/null
+++ b/src/segments/ui5tooling.go
@@ -0,0 +1,49 @@
+package segments
+
+import (
+	"oh-my-posh/environment"
+	"oh-my-posh/properties"
+)
+
+const UI5ToolingYamlPattern = "*ui5*.y*ml"
+
+type UI5Tooling struct {
+	language
+	HasUI5YamlInParentDir bool
+}
+
+func (u *UI5Tooling) Template() string {
+	return languageTemplate
+}
+
+func (u *UI5Tooling) Init(props properties.Properties, env environment.Environment) {
+	u.language = language{
+		env:         env,
+		props:       props,
+		extensions:  []string{UI5ToolingYamlPattern},
+		loadContext: u.loadContext,
+		inContext:   u.inContext,
+		displayMode: props.GetString(DisplayMode, DisplayModeContext),
+		commands: []*cmd{
+			{
+				executable: "ui5",
+				args:       []string{"--version"},
+				regex:      `(?:(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+))))`,
+			},
+		},
+		versionURLTemplate: "https://github.com/SAP/ui5-cli/releases/tag/v{{ .Full }}",
+	}
+}
+
+func (u *UI5Tooling) Enabled() bool {
+	return u.language.Enabled()
+}
+
+func (u *UI5Tooling) loadContext() {
+	// for searching ui5 yaml from subdirectories of UI5 project root - up to 4 levels
+	u.HasUI5YamlInParentDir = u.env.HasFileInParentDirs(UI5ToolingYamlPattern, 4)
+}
+
+func (u *UI5Tooling) inContext() bool {
+	return u.HasUI5YamlInParentDir
+}
diff --git a/themes/schema.json b/themes/schema.json
index f6d37ff4..b89c436a 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -183,7 +183,8 @@
             "winreg",
             "plastic",
             "ipify",
-            "haskell"
+            "haskell",
+            "ui5tooling"
           ]
         },
         "style": {
@@ -1907,6 +1908,32 @@
               }
             }
           }
+        },
+        {
+          "if": {
+            "properties": {
+              "type": { "const": "ui5tooling" }
+            }
+          },
+          "then": {
+            "title": "UI5 tooling CLI segment",
+            "description": "https://ohmyposh.dev/docs/ui5tooling",
+            "properties": {
+              "properties": {
+                "properties": {
+                  "fetch_version": {
+                    "$ref": "#/definitions/fetch_version"
+                  },
+                  "display_mode": {
+                    "$ref": "#/definitions/display_mode"
+                  },
+                  "missing_command_text": {
+                    "$ref": "#/definitions/missing_command_text"
+                  }
+                }
+              }
+            }
+          }
         }
       ]
     }


Test patch:
diff --git a/src/segments/ui5tooling_test.go b/src/segments/ui5tooling_test.go
new file mode 100644
index 00000000..6af56631
--- /dev/null
+++ b/src/segments/ui5tooling_test.go
@@ -0,0 +1,168 @@
+package segments
+
+import (
+	"fmt"
+	"oh-my-posh/environment"
+	"oh-my-posh/mock"
+	"oh-my-posh/properties"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	WorkingDirRoot = "/home/user/dev/my-app"
+)
+
+type testCase struct {
+	Case            string
+	Template        string
+	ExpectedString  string
+	ExpectedEnabled bool
+	UI5YamlFilename string
+	WorkingDir      string
+	Version         string
+	DisplayMode     string
+}
+
+func TestUI5Tooling(t *testing.T) {
+	cases := []testCase{
+		{
+			Case:            "1) ui5tooling 2.12.1 - file ui5.yaml present in cwd; DisplayMode = files",
+			ExpectedString:  "2.12.1",
+			ExpectedEnabled: true,
+			UI5YamlFilename: "ui5.yaml",
+			Version:         `2.12.1 (from C:\somewhere\cli\bin\ui5.js)`,
+			DisplayMode:     DisplayModeFiles,
+		},
+		{
+			Case:            "2) ui5tooling 2.12.2 - file ui5.yaml present in cwd; default display mode (context)",
+			ExpectedString:  "2.12.2",
+			ExpectedEnabled: true,
+			UI5YamlFilename: "ui5.yaml",
+			Version:         `2.12.2 (from C:\somewhere\cli\bin\ui5.js)`,
+		},
+		{
+			Case:            "3) ui5tooling 2.12.3 - file ui5.yaml present; cwd is sub dir, default display mode (context)",
+			ExpectedString:  "2.12.3",
+			WorkingDir:      WorkingDirRoot + "/subdir",
+			ExpectedEnabled: true,
+			UI5YamlFilename: "ui5.yaml",
+			Version:         `2.12.3 (from C:\somewhere\cli\bin\ui5.js)`,
+		},
+		{
+			Case:            "4) no ui5tooling segment - file ui5.yaml present, cwd is sub dir; display mode = files",
+			ExpectedString:  "",
+			WorkingDir:      WorkingDirRoot + "/subdir",
+			ExpectedEnabled: false,
+			UI5YamlFilename: "ui5.yaml",
+			DisplayMode:     DisplayModeFiles,
+			Version:         `2.12.1 (from C:\somewhere\cli\bin\ui5.js)`,
+		},
+		{
+			Case:            "5) ui5tooling 2.12.4 - file ui5-dist.yml present in cwd",
+			ExpectedString:  "2.12.4",
+			ExpectedEnabled: true,
+			UI5YamlFilename: "ui5-dist.yml",
+			Version:         `2.12.4 (from C:\somewhere\cli\bin\ui5.js)`,
+			DisplayMode:     DisplayModeFiles,
+		},
+		{
+			Case:            "6) no ui5tooling segment - file ui5.yaml not present, display mode = files",
+			ExpectedString:  "",
+			ExpectedEnabled: false,
+			Version:         `2.12.1 (from C:\somewhere\cli\bin\ui5.js)`,
+			DisplayMode:     DisplayModeFiles,
+		},
+		{
+			Case:            "7) no ui5tooling segment - file ui5.yaml not present, default display mode (context)",
+			ExpectedString:  "",
+			ExpectedEnabled: false,
+			Version:         `2.12.1 (from C:\somewhere\cli\bin\ui5.js)`,
+		},
+		{
+			Case:            "8) ui5tooling 11.0.0-rc1, no ui5.yaml file but display mode = always",
+			Template:        "{{ .Major }}",
+			ExpectedString:  "11",
+			ExpectedEnabled: true,
+			Version:         `11.0.0-rc1 (from C:\somewhere\cli\bin\ui5.js)`,
+			DisplayMode:     DisplayModeAlways,
+		},
+	}
+
+	for _, tc := range cases {
+		env := prepareMockedEnvironment(&tc)
+		ui5tooling := &UI5Tooling{}
+
+		if tc.WorkingDir == "" {
+			tc.WorkingDir = WorkingDirRoot
+		}
+
+		if tc.DisplayMode == "" {
+			tc.DisplayMode = DisplayModeContext
+		}
+
+		if tc.Template == "" {
+			tc.Template = ui5tooling.Template()
+		}
+
+		props := properties.Map{
+			DisplayMode: tc.DisplayMode,
+		}
+
+		ui5tooling.Init(props, env)
+		err := mockFilePresence(&tc, ui5tooling, env)
+
+		if err != nil {
+			t.Fail()
+		}
+
+		failMsg := fmt.Sprintf("Failed in case: %s", tc.Case)
+		assert.Equal(t, tc.ExpectedEnabled, ui5tooling.Enabled(), failMsg)
+		assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, ui5tooling), failMsg)
+	}
+}
+
+func prepareMockedEnvironment(tc *testCase) *mock.MockedEnvironment {
+	var env = new(mock.MockedEnvironment)
+	env.On("HasCommand", "ui5").Return(true)
+	env.On("RunCommand", "ui5", []string{"--version"}).Return(tc.Version, nil)
+	env.On("Home").Return("/home/user")
+	env.On("Pwd").Return(WorkingDirRoot)
+
+	env.On("TemplateCache").Return(&environment.TemplateCache{
+		Env: make(map[string]string),
+	})
+
+	return env
+}
+
+func mockFilePresence(tc *testCase, ui5tooling *UI5Tooling, env *mock.MockedEnvironment) error {
+	for _, f := range ui5tooling.language.extensions {
+		match, err := filepath.Match(f, tc.UI5YamlFilename)
+
+		if err != nil {
+			return err
+		}
+
+		if match {
+			if tc.DisplayMode == DisplayModeFiles && tc.WorkingDir == WorkingDirRoot {
+				env.On("HasFiles", f).Return(true)
+				env.On("HasFileInParentDirs", f, uint(4)).Return(false)
+				// mode context, working dir != working dir root
+			} else if tc.DisplayMode == DisplayModeContext {
+				env.On("HasFileInParentDirs", f, uint(4)).Return(false)
+				env.On("HasFiles", f).Return(true)
+			} else {
+				env.On("HasFileInParentDirs", f, uint(4)).Return(false)
+				env.On("HasFiles", f).Return(false)
+			}
+		} else {
+			env.On("HasFileInParentDirs", f, uint(4)).Return(false)
+			env.On("HasFiles", f).Return(false)
+		}
+	}
+
+	return nil
+}