Repository: JanDeDobbeleer/oh-my-posh
Pull request #1901 (2022-03-11 14:30:38)

feat: package 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

New package segment to show the version of the package file. Created in a generic way so that other types can be added in the future.

![image](https://user-images.githubusercontent.com/2900833/157886933-fabf45d2-a775-4ae0-8ee0-c4aa8ca5c0f6.png)


estruyf: Yes, got the inspiration from Starship, there it's also called "package", but it can be anything else. 

On some cases, the project is the actual package version, but still,  it's a project. Shall we go for "project", leave it, or do you have another suggestion?
estruyf: Updated the files and code to use `project` instead.

Patch:
diff --git a/docs/docs/segment-project.md b/docs/docs/segment-project.md
new file mode 100644
index 00000000..395bf337
--- /dev/null
+++ b/docs/docs/segment-project.md
@@ -0,0 +1,44 @@
+---
+id: project
+title: Project
+sidebar_label: Project
+---
+
+## What
+
+Display the current version of your project defined in the package file.
+
+Supports:
+
+- Node.js project (`package.json`)
+
+## Sample Configuration
+
+```json
+{
+  "type": "project",
+  "style": "powerline",
+  "powerline_symbol": "\uE0B0",
+  "foreground": "#193549",
+  "background": "#ffeb3b",
+  "properties": {
+    "template": " {{ if .Error }}{{ .Error }}{{ else }}{{ if .Version }}\uf487 {{.Version}}{{ end }}{{ end }} "
+  }
+}
+```
+
+## Template ([info][templates])
+
+:::note default template
+
+``` template
+{{ if .Error }}{{ .Error }}{{ else }}{{ if .Version }}\uf487 {{.Version}}{{ end }}{{ end }}
+```
+
+:::
+
+### Properties
+
+- `.Version`: `string` - The version of your project
+
+[templates]: /docs/config-templates
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 4ceff3c7..cc44a85e 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -84,6 +84,7 @@ module.exports = {
         "path",
         "php",
         "plastic",
+        "project",
         "python",
         "root",
         "ruby",
diff --git a/src/engine/segment.go b/src/engine/segment.go
index 9c2049ea..715ed2f9 100644
--- a/src/engine/segment.go
+++ b/src/engine/segment.go
@@ -166,6 +166,8 @@ const (
 	CDS SegmentType = "cds"
 	// npm version
 	NPM SegmentType = "npm"
+	// Project version
+	PROJECT SegmentType = "project"
 )
 
 func (segment *Segment) shouldIncludeFolder() bool {
@@ -245,6 +247,7 @@ func (segment *Segment) background() string {
 func (segment *Segment) mapSegmentWithWriter(env environment.Environment) error {
 	segment.env = env
 	functions := map[SegmentType]SegmentWriter{
+		PROJECT:       &segments.Project{},
 		NPM:           &segments.Npm{},
 		OWM:           &segments.Owm{},
 		SESSION:       &segments.Session{},
diff --git a/src/segments/project.go b/src/segments/project.go
new file mode 100644
index 00000000..58a0f25a
--- /dev/null
+++ b/src/segments/project.go
@@ -0,0 +1,81 @@
+package segments
+
+import (
+	"encoding/json"
+	"oh-my-posh/environment"
+	"oh-my-posh/properties"
+)
+
+type ProjectItem struct {
+	Name    string
+	File    string
+	Fetcher func(item ProjectItem) string
+}
+
+type NodePackageJSON struct {
+	Name        string `json:"name"`
+	Description string `json:"description"`
+	Version     string `json:"version"`
+}
+
+type Project struct {
+	props properties.Properties
+	env   environment.Environment
+
+	projects []*ProjectItem
+	Version  string
+	Error    string
+}
+
+func (n *Project) Enabled() bool {
+	var enabled = false
+	for _, item := range n.projects {
+		if !enabled {
+			enabled = n.hasProjectFile(item)
+		}
+	}
+
+	return enabled
+}
+
+func (n *Project) Template() string {
+	return " {{ if .Error }}{{ .Error }}{{ else }}{{ if .Version }}\uf487 {{.Version}}{{ end }}{{ end }} "
+}
+
+func (n *Project) Init(props properties.Properties, env environment.Environment) {
+	n.props = props
+	n.env = env
+
+	n.projects = []*ProjectItem{
+		{
+			Name:    "node",
+			File:    "package.json",
+			Fetcher: n.getNodePackageVersion,
+		},
+	}
+
+	n.Version = ""
+	for _, item := range n.projects {
+		if n.hasProjectFile(item) {
+			n.Version = item.Fetcher(*item)
+			break
+		}
+	}
+}
+
+func (n *Project) hasProjectFile(p *ProjectItem) bool {
+	return n.env.HasFiles(p.File)
+}
+
+func (n *Project) getNodePackageVersion(item ProjectItem) string {
+	content := n.env.FileContent(item.File)
+
+	var data NodePackageJSON
+	err := json.Unmarshal([]byte(content), &data)
+	if err != nil {
+		n.Error = err.Error()
+		return ""
+	}
+
+	return data.Version
+}
diff --git a/themes/schema.json b/themes/schema.json
index 863fbc88..c87a3aac 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -199,7 +199,8 @@
             "ui5tooling",
             "kotlin",
             "swift",
-            "npm"
+            "npm",
+            "project"
           ]
         },
         "style": {
@@ -265,6 +266,17 @@
         }
       },
       "allOf": [
+        {
+          "if": {
+            "properties": {
+              "type": { "const": "project" }
+            }
+          },
+          "then": {
+            "title": "Project Segment",
+            "description": "https://ohmyposh.dev/docs/project"
+          }
+        },
         {
           "if": {
             "properties": {


Test patch:
diff --git a/src/segments/project_test.go b/src/segments/project_test.go
new file mode 100644
index 00000000..4e096eba
--- /dev/null
+++ b/src/segments/project_test.go
@@ -0,0 +1,43 @@
+package segments
+
+import (
+	"fmt"
+	"oh-my-posh/mock"
+	"oh-my-posh/properties"
+	"testing"
+
+	"github.com/alecthomas/assert"
+)
+
+type MockData struct {
+	Name            string
+	Case            string
+	ExpectedString  string
+	PackageContents string
+	File            string
+}
+
+func getMockedPackageEnv(tc *MockData) (*mock.MockedEnvironment, properties.Map) {
+	env := new(mock.MockedEnvironment)
+	props := properties.Map{}
+	env.On("HasFiles", tc.File).Return(true)
+	env.On("FileContent", tc.File).Return(tc.PackageContents)
+	return env, props
+}
+
+func TestPackage(t *testing.T) {
+	cases := []*MockData{
+		{Case: "1.0.0", ExpectedString: "\uf487 1.0.0", Name: "node", File: "package.json", PackageContents: "{\"version\":\"1.0.0\"}"},
+		{Case: "3.2.1", ExpectedString: "\uf487 3.2.1", Name: "node", File: "package.json", PackageContents: "{\"version\":\"3.2.1\"}"},
+		{Case: "No version present", ExpectedString: "", Name: "node", File: "package.json", PackageContents: "{}"},
+		{Case: "Invalid json", ExpectedString: "invalid character '}' looking for beginning of value", Name: "node", File: "package.json", PackageContents: "}"},
+	}
+
+	for _, tc := range cases {
+		env, props := getMockedPackageEnv(tc)
+		pkg := &Project{}
+		pkg.Init(props, env)
+		assert.True(t, pkg.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case))
+		assert.Equal(t, tc.ExpectedString, renderTemplate(env, pkg.Template(), pkg), fmt.Sprintf("Failed in case: %s", tc.Case))
+	}
+}