Repository: JanDeDobbeleer/oh-my-posh
Pull request #3741 (2023-04-21 19:26:48)

feat: allow segment to override global properties
feat: allow segment to override global properties



Patch:
diff --git a/src/template/text.go b/src/template/text.go
index d04bd84f..ff1c26f1 100644
--- a/src/template/text.go
+++ b/src/template/text.go
@@ -3,6 +3,7 @@ package template
 import (
 	"bytes"
 	"errors"
+	"reflect"
 	"strings"
 	"text/template"
 
@@ -14,6 +15,29 @@ const (
 	// Errors to show when the template handling fails
 	InvalidTemplate   = "invalid template text"
 	IncorrectTemplate = "unable to create text based on template"
+
+	globalRef = ".$"
+)
+
+var (
+	knownVariables = []string{
+		"Root",
+		"PWD",
+		"Folder",
+		"Shell",
+		"ShellVersion",
+		"UserName",
+		"HostName",
+		"Env",
+		"Data",
+		"Code",
+		"OS",
+		"WSL",
+		"Segments",
+		"Templates",
+		"PromptCount",
+		"Var",
+	}
 )
 
 type Text struct {
@@ -73,26 +97,7 @@ func (t *Text) Render() (string, error) {
 }
 
 func (t *Text) cleanTemplate() {
-	knownVariables := []string{
-		"Root",
-		"PWD",
-		"Folder",
-		"Shell",
-		"ShellVersion",
-		"UserName",
-		"HostName",
-		"Env",
-		"Data",
-		"Code",
-		"OS",
-		"WSL",
-		"Segments",
-		"Templates",
-		"PromptCount",
-		"Var",
-	}
-
-	knownVariable := func(variable string) bool {
+	isKnownVariable := func(variable string) bool {
 		variable = strings.TrimPrefix(variable, ".")
 		splitted := strings.Split(variable, ".")
 		if len(splitted) == 0 {
@@ -111,6 +116,9 @@ func (t *Text) cleanTemplate() {
 		return false
 	}
 
+	fields := make(fields)
+	fields.init(t.Context)
+
 	var result, property string
 	var inProperty, inTemplate bool
 	for i, char := range t.Template {
@@ -149,9 +157,17 @@ func (t *Text) cleanTemplate() {
 				continue
 			}
 			// end of a variable, needs to be appended
-			if !knownVariable(property) {
+			if !isKnownVariable(property) {
 				result += ".Data" + property
 			} else {
+				// check if we have the same property in Data
+				// and replace it with the Data property so it
+				// can take precedence
+				if fields.hasField(property) {
+					property = ".Data" + property
+				}
+				// remove the global reference so we can use it directly
+				property = strings.TrimPrefix(property, globalRef)
 				result += property
 			}
 			property = ""
@@ -169,3 +185,34 @@ func (t *Text) cleanTemplate() {
 	// return the result and remaining unresolved property
 	t.Template = result + property
 }
+
+type fields map[string]bool
+
+func (f *fields) init(data interface{}) {
+	if data == nil {
+		return
+	}
+
+	val := reflect.TypeOf(data)
+	switch val.Kind() { //nolint:exhaustive
+	case reflect.Struct:
+		fieldsNum := val.NumField()
+		for i := 0; i < fieldsNum; i++ {
+			(*f)[val.Field(i).Name] = true
+		}
+	case reflect.Map:
+		m, ok := data.(map[string]interface{})
+		if !ok {
+			return
+		}
+		for key := range m {
+			(*f)[key] = true
+		}
+	}
+}
+
+func (f fields) hasField(field string) bool {
+	field = strings.TrimPrefix(field, ".")
+	_, ok := f[field]
+	return ok
+}
diff --git a/website/docs/configuration/templates.mdx b/website/docs/configuration/templates.mdx
index 6328107f..08ff1e0e 100644
--- a/website/docs/configuration/templates.mdx
+++ b/website/docs/configuration/templates.mdx
@@ -13,6 +13,10 @@ offers a few standard properties to work with.
 
 ## Global properties
 
+These properties can be used anywhere, in any segment. If a segment contains a property with the same name,
+the segment property value will be used instead. In case you want to use the global property, you can prefix
+it with `.$` to reference it directly.
+
 | Name            | Type      | Description                                                       |
 | --------------- | --------- | ----------------------------------------------------------------- |
 | `.Root`         | `boolean` | is the current user root/admin or not                             |


Test patch:
diff --git a/src/template/text_test.go b/src/template/text_test.go
index 39940be6..9da299ba 100644
--- a/src/template/text_test.go
+++ b/src/template/text_test.go
@@ -209,11 +209,40 @@ func TestRenderTemplateEnvVar(t *testing.T) {
 		{Case: "no env var", Expected: "hello world", Template: "{{.Text}} world", Context: struct{ Text string }{Text: "hello"}},
 		{Case: "map", Expected: "hello world", Template: "{{.Text}} world", Context: map[string]interface{}{"Text": "hello"}},
 		{Case: "empty map", Expected: " world", Template: "{{.Text}} world", Context: map[string]string{}},
+		{
+			Case:     "Struct with duplicate property",
+			Expected: "posh",
+			Template: "{{ .OS }}",
+			Context:  struct{ OS string }{OS: "posh"},
+			Env:      map[string]string{"HELLO": "hello"},
+		},
+		{
+			Case:     "Struct with duplicate property, but global override",
+			Expected: "darwin",
+			Template: "{{ .$.OS }}",
+			Context:  struct{ OS string }{OS: "posh"},
+			Env:      map[string]string{"HELLO": "hello"},
+		},
+		{
+			Case:     "Map with duplicate property",
+			Expected: "posh",
+			Template: "{{ .OS }}",
+			Context:  map[string]interface{}{"OS": "posh"},
+			Env:      map[string]string{"HELLO": "hello"},
+		},
+		{
+			Case:     "Non-supported map",
+			Expected: "darwin",
+			Template: "{{ .OS }}",
+			Context:  map[int]interface{}{},
+			Env:      map[string]string{"HELLO": "hello"},
+		},
 	}
 	for _, tc := range cases {
 		env := &mock.MockedEnvironment{}
 		env.On("TemplateCache").Return(&platform.TemplateCache{
 			Env: tc.Env,
+			OS:  "darwin",
 		})
 		env.On("Error", mock2.Anything)
 		tmpl := &Text{