Repository: JanDeDobbeleer/oh-my-posh
Pull request #2333 (2022-05-26 12:54:19)

feat: Nx segment
### Prerequisites

- [x] I have read and understood the [contributing guide][CONTRIBUTING.md]
- [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

Add a new segment to show the Nx (monorepo tooling) version.

JanDeDobbeleer: @evanfuture there's some other changes needed but I might just do them as the logic isn't flawed, it just needs to be a little more cross platform proof (and I'm not at my laptop right now).

Patch:
diff --git a/src/engine/segment.go b/src/engine/segment.go
index 8eabe2f0..40066a37 100644
--- a/src/engine/segment.go
+++ b/src/engine/segment.go
@@ -129,6 +129,8 @@ const (
 	NODE SegmentType = "node"
 	// npm version
 	NPM SegmentType = "npm"
+	// NX writes which Nx version us currently active
+	NX SegmentType = "nx"
 	// OS write os specific icon
 	OS SegmentType = "os"
 	// OWM writes the weather coming from openweatherdata
@@ -277,6 +279,7 @@ func (segment *Segment) mapSegmentWithWriter(env environment.Environment) error
 		NIGHTSCOUT:    &segments.Nightscout{},
 		NODE:          &segments.Node{},
 		NPM:           &segments.Npm{},
+		NX:            &segments.Nx{},
 		OS:            &segments.Os{},
 		OWM:           &segments.Owm{},
 		PATH:          &segments.Path{},
diff --git a/src/segments/angular.go b/src/segments/angular.go
index 662ecc2b..170c7a7a 100644
--- a/src/segments/angular.go
+++ b/src/segments/angular.go
@@ -1,10 +1,9 @@
 package segments
 
 import (
-	"encoding/json"
-	"fmt"
 	"oh-my-posh/environment"
 	"oh-my-posh/properties"
+	"path/filepath"
 )
 
 type Angular struct {
@@ -22,28 +21,8 @@ func (a *Angular) Init(props properties.Properties, env environment.Environment)
 		extensions: []string{"angular.json"},
 		commands: []*cmd{
 			{
-				regex: `(?:(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+))))`,
-				getVersion: func() (string, error) {
-					const fileName string = "package.json"
-					const fileFolder string = "/node_modules/@angular/core"
-					angularFilePath := a.language.env.Pwd() + fileFolder
-					if !a.language.env.HasFilesInDir(angularFilePath, fileName) {
-						return "", fmt.Errorf("%s not found in %s", fileName, angularFilePath)
-					}
-					// parse file
-					objmap := map[string]json.RawMessage{}
-					content := a.language.env.FileContent(a.language.env.Pwd() + fileFolder + "/" + fileName)
-					err := json.Unmarshal([]byte(content), &objmap)
-					if err != nil {
-						return "", err
-					}
-					var str string
-					err = json.Unmarshal(objmap["version"], &str)
-					if err != nil {
-						return "", err
-					}
-					return str, nil
-				},
+				regex:      `(?:(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+))))`,
+				getVersion: a.getVersion,
 			},
 		},
 		versionURLTemplate: "https://github.com/angular/angular/releases/tag/{{.Full}}",
@@ -53,3 +32,7 @@ func (a *Angular) Init(props properties.Properties, env environment.Environment)
 func (a *Angular) Enabled() bool {
 	return a.language.Enabled()
 }
+
+func (a *Angular) getVersion() (string, error) {
+	return getNodePackageVersion(a.language.env, filepath.Join("@angular", "core"))
+}
diff --git a/src/segments/nx.go b/src/segments/nx.go
new file mode 100644
index 00000000..5ea84b56
--- /dev/null
+++ b/src/segments/nx.go
@@ -0,0 +1,55 @@
+package segments
+
+import (
+	"encoding/json"
+	"fmt"
+	"oh-my-posh/environment"
+	"oh-my-posh/properties"
+	"path/filepath"
+)
+
+type Nx struct {
+	language
+}
+
+func (a *Nx) Template() string {
+	return languageTemplate
+}
+
+func (a *Nx) Init(props properties.Properties, env environment.Environment) {
+	a.language = language{
+		env:        env,
+		props:      props,
+		extensions: []string{"workspace.json"},
+		commands: []*cmd{
+			{
+				regex:      `(?:(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+))))`,
+				getVersion: a.getVersion,
+			},
+		},
+		versionURLTemplate: "https://github.com/nrwl/nx/releases/tag/{{.Full}}",
+	}
+}
+
+func (a *Nx) Enabled() bool {
+	return a.language.Enabled()
+}
+
+func (a *Nx) getVersion() (string, error) {
+	return getNodePackageVersion(a.language.env, "nx")
+}
+
+func getNodePackageVersion(env environment.Environment, nodePackage string) (string, error) {
+	const fileName string = "package.json"
+	folder := filepath.Join(env.Pwd(), "node_modules", nodePackage)
+	if !env.HasFilesInDir(folder, fileName) {
+		return "", fmt.Errorf("%s not found in %s", fileName, folder)
+	}
+	content := env.FileContent(filepath.Join(folder, fileName))
+	var data ProjectData
+	err := json.Unmarshal([]byte(content), &data)
+	if err != nil {
+		return "", err
+	}
+	return data.Version, nil
+}
diff --git a/themes/atomic.omp.json b/themes/atomic.omp.json
index 3630241c..ad45fe62 100644
--- a/themes/atomic.omp.json
+++ b/themes/atomic.omp.json
@@ -146,6 +146,15 @@
           "trailing_diamond": "\ue0b4 ",
           "type": "angular"
         },
+        {
+          "background": "#1e293b",
+          "foreground": "#ffffff",
+          "leading_diamond": " \ue0b6",
+          "style": "diamond",
+          "template": "{{ if .Error }}{{ .Error }}{{ else }}Nx {{ .Full }}{{ end }}",
+          "trailing_diamond": "\ue0b4 ",
+          "type": "nx"
+        },
         {
           "background": "#945bb3",
           "foreground": "#359a25",
@@ -251,9 +260,7 @@
         },
         {
           "foreground": "#e0f8ff",
-          "foreground_templates": [
-            "{{ if gt .Code 0 }}#ef5350{{ end }}"
-          ],
+          "foreground_templates": ["{{ if gt .Code 0 }}#ef5350{{ end }}"],
           "properties": {
             "always_enabled": true
           },
diff --git a/themes/atomicBit.omp.json b/themes/atomicBit.omp.json
index 789be3b5..ec13de68 100644
--- a/themes/atomicBit.omp.json
+++ b/themes/atomicBit.omp.json
@@ -80,6 +80,12 @@
           "template": "<#ffffff>(</>{{ if .Error }}{{ .Error }}{{ else }}{{ .Full }}{{ end }}<#ffffff>)</>",
           "type": "angular"
         },
+        {
+          "foreground": "#ffffff",
+          "style": "plain",
+          "template": "<#1e293b>(</>{{ if .Error }}{{ .Error }}{{ else }}Nx {{ .Full }}{{ end }}<#1e293b>)</>",
+          "type": "nx"
+        },
         {
           "foreground": "#359a25",
           "style": "plain",
@@ -171,9 +177,7 @@
         },
         {
           "foreground": "#ecf7fa",
-          "foreground_templates": [
-            "{{ if gt .Code 0 }}#ef5350{{ end }}"
-          ],
+          "foreground_templates": ["{{ if gt .Code 0 }}#ef5350{{ end }}"],
           "properties": {
             "always_enabled": true
           },
diff --git a/themes/night-owl.omp.json b/themes/night-owl.omp.json
index f9b4c796..0c5f79d4 100644
--- a/themes/night-owl.omp.json
+++ b/themes/night-owl.omp.json
@@ -184,6 +184,15 @@
           "trailing_diamond": "<transparent,#ffffff>\ue0b2</>",
           "type": "angular"
         },
+        {
+          "background": "#1e293b",
+          "foreground": "#ffffff",
+          "leading_diamond": "\ue0b2",
+          "style": "diamond",
+          "template": "\ue753 {{ if .Error }}{{ .Error }}{{ else }}Nx {{ .Full }}{{ end }} ",
+          "trailing_diamond": "<transparent,#1e293b>\ue0b2</>",
+          "type": "nx"
+        },
         {
           "background": "#945bb3",
           "foreground": "#359a25",
@@ -269,9 +278,7 @@
         },
         {
           "foreground": "#22da6e",
-          "foreground_templates": [
-            "{{ if gt .Code 0 }}#ef5350{{ end }}"
-          ],
+          "foreground_templates": ["{{ if gt .Code 0 }}#ef5350{{ end }}"],
           "properties": {
             "always_enabled": true
           },
diff --git a/themes/schema.json b/themes/schema.json
index d15d6c0e..46a7f1a3 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -59,12 +59,7 @@
       "type": "string",
       "title": "Display Mode",
       "description": "Determines whether the segment is displayed always or only if a file matching the extensions are present in the current folder",
-      "enum": [
-        "always",
-        "files",
-        "environment",
-        "context"
-      ],
+      "enum": ["always", "files", "environment", "context"],
       "default": "context"
     },
     "missing_command_text": {
@@ -114,11 +109,7 @@
             }
           },
           "then": {
-            "required": [
-              "type",
-              "alignment",
-              "segments"
-            ],
+            "required": ["type", "alignment", "segments"],
             "title": "Prompt definition, contains 1 or more segments to render"
           }
         },
@@ -131,10 +122,7 @@
             }
           },
           "then": {
-            "required": [
-              "type",
-              "segments"
-            ],
+            "required": ["type", "segments"],
             "title": "RPrompt definition, contains 1 or more segments to render to the right of the cursor"
           }
         }
@@ -144,20 +132,14 @@
           "type": "string",
           "title": "Block type",
           "description": "https://ohmyposh.dev/docs/configuration/block#type",
-          "enum": [
-            "prompt",
-            "rprompt"
-          ],
+          "enum": ["prompt", "rprompt"],
           "default": "prompt"
         },
         "alignment": {
           "type": "string",
           "title": "Block alignment",
           "description": "https://ohmyposh.dev/docs/configuration/block#alignment",
-          "enum": [
-            "left",
-            "right"
-          ],
+          "enum": ["left", "right"],
           "default": "left"
         },
         "newline": {
@@ -192,10 +174,7 @@
       "title": "A segment",
       "description": "https://ohmyposh.dev/docs/configuration/segment",
       "default": {},
-      "required": [
-        "type",
-        "style"
-      ],
+      "required": ["type", "style"],
       "properties": {
         "type": {
           "type": "string",
@@ -227,6 +206,7 @@
             "kotlin",
             "node",
             "npm",
+            "nx",
             "os",
             "owm",
             "path",
@@ -259,12 +239,7 @@
           "type": "string",
           "title": "Segment Style",
           "description": "https://ohmyposh.dev/docs/configuration/segment#style",
-          "enum": [
-            "plain",
-            "powerline",
-            "diamond",
-            "accordion"
-          ]
+          "enum": ["plain", "powerline", "diamond", "accordion"]
         },
         "foreground": {
           "$ref": "#/definitions/color"
@@ -444,11 +419,7 @@
                     "title": "Source",
                     "description": "https://ohmyposh.dev/docs/az#properties",
                     "default": "first_match",
-                    "enum": [
-                      "first_match",
-                      "cli",
-                      "pwsh"
-                    ]
+                    "enum": ["first_match", "cli", "pwsh"]
                   }
                 }
               }
@@ -1705,11 +1676,7 @@
                     "title": "units",
                     "description": "Units of measurement. Available values are standard (kelvin), metric (celsius), and imperial (fahrenheit). Default is standard",
                     "default": "standard",
-                    "enum": [
-                      "standard",
-                      "metric",
-                      "imperial"
-                    ]
+                    "enum": ["standard", "metric", "imperial"]
                   },
                   "http_timeout": {
                     "$ref": "#/definitions/http_timeout"
@@ -1904,6 +1871,37 @@
             }
           }
         },
+        {
+          "if": {
+            "properties": {
+              "type": {
+                "const": "nx"
+              }
+            }
+          },
+          "then": {
+            "title": "Nx Segment",
+            "description": "https://ohmyposh.dev/docs/segments/nx",
+            "properties": {
+              "properties": {
+                "properties": {
+                  "home_enabled": {
+                    "$ref": "#/definitions/home_enabled"
+                  },
+                  "fetch_version": {
+                    "$ref": "#/definitions/fetch_version"
+                  },
+                  "display_mode": {
+                    "$ref": "#/definitions/display_mode"
+                  },
+                  "version_url_template": {
+                    "$ref": "#/definitions/version_url_template"
+                  }
+                }
+              }
+            }
+          }
+        },
         {
           "if": {
             "properties": {
@@ -2234,11 +2232,7 @@
                     "type": "string",
                     "title": "Use Stack GHC",
                     "description": "Get the GHC version used by Stack. Will decrease performance. Boolean indicating whether stack ghc was used available in template as .StackGhc",
-                    "enum": [
-                      "always",
-                      "package",
-                      "never"
-                    ],
+                    "enum": ["always", "package", "never"],
                     "default": "never"
                   }
                 }
@@ -2463,9 +2457,7 @@
       ]
     }
   },
-  "required": [
-    "blocks"
-  ],
+  "required": ["blocks"],
   "properties": {
     "final_space": {
       "type": "boolean",
@@ -2517,9 +2509,7 @@
             }
           }
         },
-        "required": [
-          "tips"
-        ]
+        "required": ["tips"]
       }
     },
     "transient_prompt": {
diff --git a/website/docs/segments/nx.md b/website/docs/segments/nx.md
new file mode 100644
index 00000000..1ba30f7b
--- /dev/null
+++ b/website/docs/segments/nx.md
@@ -0,0 +1,55 @@
+---
+id: nx
+title: Nx
+sidebar_label: Nx
+---
+
+## What
+
+Display the currently active [Nx][nx-docs] version.
+
+## Sample Configuration
+
+```json
+{
+  "type": "nx",
+  "style": "powerline",
+  "powerline_symbol": "\uE0B0",
+  "foreground": "#000000",
+  "background": "#1976d2",
+  "template": " \uE753 {{ .Full }} "
+}
+```
+
+## Properties
+
+- home_enabled: `boolean` - display the segment in the HOME folder or not - defaults to `false`
+- fetch_version: `boolean` - fetch the active version or not; useful if all you need is an icon indicating `ng`
+- display_mode: `string` - determines when the segment is displayed
+  - `always`: the segment is always displayed
+  - `files`: the segment is only displayed when `workspace.json` file is present (default)
+- version_url_template: `string` - a go [text/template][go-text-template] [template][templates] that creates
+  the URL of the version info / release notes
+
+## Template ([info][templates])
+
+:::note default template
+
+```template
+{{ if .Error }}{{ .Error }}{{ else }}{{ .Full }}{{ end }}
+```
+
+:::
+
+### Properties
+
+- `.Full`: `string` - the full version
+- `.Major`: `string` - major number
+- `.Minor`: `string` - minor number
+- `.Patch`: `string` - patch number
+- `.URL`: `string` - URL of the version info / release notes
+- `.Error`: `string` - error encountered when fetching the version string
+
+[go-text-template]: https://golang.org/pkg/text/template/
+[templates]: /docs/configuration/templates
+[nx-docs]: https://nx.dev
diff --git a/website/sidebars.js b/website/sidebars.js
index ae7ce1b2..d041d111 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -22,7 +22,7 @@ module.exports = {
               ],
             },
             "installation/prompt",
-            "installation/customize"
+            "installation/customize",
           ],
         },
       ],
@@ -81,6 +81,7 @@ module.exports = {
         "segments/nightscout",
         "segments/npm",
         "segments/node",
+        "segments/nx",
         "segments/os",
         "segments/owm",
         "segments/path",


Test patch:
diff --git a/src/segments/angular_test.go b/src/segments/angular_test.go
deleted file mode 100644
index 98af314e..00000000
--- a/src/segments/angular_test.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package segments
-
-import (
-	"fmt"
-	"oh-my-posh/environment"
-	"oh-my-posh/mock"
-	"oh-my-posh/properties"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestAngularCliVersionDisplayed(t *testing.T) {
-	cases := []struct {
-		Case        string
-		FullVersion string
-		Version     string
-	}{
-		{Case: "Angular 13.0.3", FullVersion: "13.0.3", Version: "{ \"name\": \"@angular/core\",\"version\": \"13.0.3\"}"},
-		{Case: "Angular 11.0.1", FullVersion: "11.0.1", Version: "{ \"name\": \"@angular/core\",\"version\": \"11.0.1\"}"},
-	}
-
-	for _, ta := range cases {
-		params := &mockedLanguageParams{
-			extension: "angular.json",
-		}
-
-		var env = new(mock.MockedEnvironment)
-		// mock  getVersion methods
-		env.On("Pwd").Return("/usr/home/dev/my-app")
-		env.On("Home").Return("/usr/home")
-		env.On("HasFiles", params.extension).Return(true)
-		env.On("HasFilesInDir", "/usr/home/dev/my-app/node_modules/@angular/core", "package.json").Return(true)
-		env.On("FileContent", "/usr/home/dev/my-app/node_modules/@angular/core/package.json").Return(ta.Version)
-		env.On("TemplateCache").Return(&environment.TemplateCache{
-			Env: make(map[string]string),
-		})
-		props := properties.Map{}
-		angular := &Angular{}
-		angular.Init(props, env)
-		assert.True(t, angular.Enabled(), fmt.Sprintf("Failed in case: %s", ta.Case))
-		assert.Equal(t, ta.FullVersion, renderTemplate(env, angular.Template(), angular), fmt.Sprintf("Failed in case: %s", ta.Case))
-	}
-}
diff --git a/src/segments/nx_test.go b/src/segments/nx_test.go
new file mode 100644
index 00000000..216e955b
--- /dev/null
+++ b/src/segments/nx_test.go
@@ -0,0 +1,44 @@
+package segments
+
+import (
+	"oh-my-posh/environment"
+	"oh-my-posh/mock"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetNodePackageVersion(t *testing.T) {
+	cases := []struct {
+		Case        string
+		PackageJSON string
+		Version     string
+		ShouldFail  bool
+		NoFiles     bool
+	}{
+		{Case: "14.1.5", Version: "14.1.5", PackageJSON: "{ \"name\": \"nx\",\"version\": \"14.1.5\"}"},
+		{Case: "14.0.0", Version: "14.0.0", PackageJSON: "{ \"name\": \"nx\",\"version\": \"14.0.0\"}"},
+		{Case: "no files", NoFiles: true, ShouldFail: true},
+		{Case: "bad data", ShouldFail: true, PackageJSON: "bad data"},
+	}
+
+	for _, tc := range cases {
+		var env = new(mock.MockedEnvironment)
+		// mock  getVersion methods
+		env.On("Pwd").Return("posh")
+		path := filepath.Join("posh", "node_modules", "nx")
+		env.On("HasFilesInDir", path, "package.json").Return(!tc.NoFiles)
+		env.On("FileContent", filepath.Join(path, "package.json")).Return(tc.PackageJSON)
+		env.On("TemplateCache").Return(&environment.TemplateCache{
+			Env: make(map[string]string),
+		})
+		got, err := getNodePackageVersion(env, "nx")
+		if tc.ShouldFail {
+			assert.Error(t, err, tc.Case)
+			return
+		}
+		assert.Nil(t, err, tc.Case)
+		assert.Equal(t, tc.Version, got, tc.Case)
+	}
+}