Repository: JanDeDobbeleer/oh-my-posh
Pull request #1493 (2021-12-23 14:51:43)

feat(ipify): segment to show your external IP address
closes #1492

### Prerequisites

- [ ] I have read and understood the `CONTRIBUTING` guide
- [ ] The commit message follows the [conventional commits][cc] guidelines
- [ ] Tests for the changes have been added (for bug fixes/features)
- [ ] Docs have been added / updated (for bug fixes/features)

### Description

[Description of the change]

<!---

Tips:

If you're not comfortable with working with Git, we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/docs/docs/segment-ipify.md b/docs/docs/segment-ipify.md
new file mode 100644
index 00000000..78f6999c
--- /dev/null
+++ b/docs/docs/segment-ipify.md
@@ -0,0 +1,45 @@
+---
+id: ipify
+title: Ipify
+sidebar_label: Ipify
+---
+
+## What
+
+[Ipify][ipify] is a simple Public IP Address API, it returns your public IP Adress in plain text.
+
+## Sample Configuration
+
+```json
+{
+  "type": "ipify",
+  "style": "diamond",
+  "foreground": "#ffffff",
+  "background": "#c386f1",
+  "leading_diamond": "",
+  "trailing_diamond": "\uE0B0",
+  "properties": {
+    "template": "{{ .IP }}",
+    "cache_timeout": 5,
+    "http_timeout": 1000
+  }
+}
+```
+
+## Properties
+
+- url: `string` - The Ipify URL, by default IPv4 is used, use `https://api64.ipify.org` for IPv6 - defaults to `https://api.ipify.org`
+- http_timeout: `int` - How long may the segment wait for a response of the ipify API? -
+  defaults to 20ms
+- cache_timeout: `int` in minutes - How long do you want your IP address cached? -
+  defaults to 10 min
+- template: `string` - a go [text/template][go-text-template] template extended
+  with [sprig][sprig] utilizing the properties below. - defaults to `{{ .IP }}`
+
+## Template Properties
+
+- .IP: `string` - Your external IP address
+
+[go-text-template]: https://golang.org/pkg/text/template/
+[sprig]: https://masterminds.github.io/sprig/
+[ipify]: https://www.ipify.org/
diff --git a/docs/sidebars.js b/docs/sidebars.js
index bb847aac..b45ad988 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -52,6 +52,7 @@ module.exports = {
         "git",
         "poshgit",
         "golang",
+        "ipify",
         "java",
         "julia",
         "kubectl",
diff --git a/src/segment.go b/src/segment.go
index 2859af33..2cdb8eee 100644
--- a/src/segment.go
+++ b/src/segment.go
@@ -141,6 +141,8 @@ const (
 	WinReg SegmentType = "winreg"
 	// Brewfather segment
 	BrewFather SegmentType = "brewfather"
+	// Ipify segment
+	Ipify SegmentType = "ipify"
 )
 
 func (segment *Segment) string() string {
@@ -274,6 +276,7 @@ func (segment *Segment) mapSegmentWithWriter(env environmentInfo) error {
 		WiFi:          &wifi{},
 		WinReg:        &winreg{},
 		BrewFather:    &brewfather{},
+		Ipify:         &ipify{},
 	}
 	if segment.Properties == nil {
 		segment.Properties = make(properties)
diff --git a/src/segment_ipify.go b/src/segment_ipify.go
new file mode 100644
index 00000000..26c323ef
--- /dev/null
+++ b/src/segment_ipify.go
@@ -0,0 +1,72 @@
+package main
+
+type ipify struct {
+	props properties
+	env   environmentInfo
+	IP    string
+}
+
+const (
+	IpifyURL Property = "url"
+)
+
+func (i *ipify) enabled() bool {
+	ip, err := i.getResult()
+	if err != nil {
+		return false
+	}
+	i.IP = ip
+
+	return true
+}
+
+func (i *ipify) string() string {
+	segmentTemplate := i.props.getString(SegmentTemplate, "{{.IP}}")
+	template := &textTemplate{
+		Template: segmentTemplate,
+		Context:  i,
+		Env:      i.env,
+	}
+	text, err := template.render()
+	if err != nil {
+		return err.Error()
+	}
+
+	return text
+}
+
+func (i *ipify) getResult() (string, error) {
+	cacheTimeout := i.props.getInt(CacheTimeout, DefaultCacheTimeout)
+
+	url := i.props.getString(IpifyURL, "https://api.ipify.org")
+
+	if cacheTimeout > 0 {
+		// check if data stored in cache
+		val, found := i.env.cache().get(url)
+		// we got something from te cache
+		if found {
+			return val, nil
+		}
+	}
+
+	httpTimeout := i.props.getInt(HTTPTimeout, DefaultHTTPTimeout)
+
+	body, err := i.env.doGet(url, httpTimeout)
+	if err != nil {
+		return "", err
+	}
+
+	// convert the body to a string
+	response := string(body)
+
+	if cacheTimeout > 0 {
+		// persist public ip in cache
+		i.env.cache().set(url, response, cacheTimeout)
+	}
+	return response, nil
+}
+
+func (i *ipify) init(props properties, env environmentInfo) {
+	i.props = props
+	i.env = env
+}
diff --git a/themes/schema.json b/themes/schema.json
index b105dd60..8e8de393 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -184,7 +184,8 @@
             "wakatime",
             "wifi",
             "winreg",
-            "plastic"
+            "plastic",
+            "ipify"
           ]
         },
         "style": {
@@ -1926,6 +1927,41 @@
               }
             }
           }
+        },
+        {
+          "if": {
+            "properties": {
+              "type": { "const": "ipify" }
+            }
+          },
+          "then": {
+            "title": "Display your external IP Address",
+            "description": "https://ohmyposh.dev/docs/ipify",
+            "properties": {
+              "properties": {
+                "properties": {
+                  "url": {
+                    "type": "string",
+                    "title": "URL",
+                    "description": "The Ipify API URL",
+                    "default": "https://api.ipify.org"
+                  },
+                  "http_timeout": {
+                    "$ref": "#/definitions/http_timeout"
+                  },
+                  "cache_timeout": {
+                    "type": "integer",
+                    "title": "cache timeout",
+                    "description": "The number of minutes the response is cached. A value of 0 disables the cache.",
+                    "default": 10
+                  },
+                  "template": {
+                    "$ref": "#/definitions/template"
+                  }
+                }
+              }
+            }
+          }
         }
       ]
     }


Test patch:
diff --git a/src/segment_ipify_test.go b/src/segment_ipify_test.go
new file mode 100644
index 00000000..7b0ef972
--- /dev/null
+++ b/src/segment_ipify_test.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	IPIFYAPIURL = "https://api.ipify.org"
+)
+
+func TestIpifySegment(t *testing.T) {
+	cases := []struct {
+		Case            string
+		Response        string
+		ExpectedString  string
+		ExpectedEnabled bool
+		Template        string
+		Error           error
+	}{
+		{
+			Case:            "IPv4",
+			Response:        `127.0.0.1`,
+			ExpectedString:  "127.0.0.1",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "IPv6 (with template)",
+			Response:        `0000:aaaa:1111:bbbb:2222:cccc:3333:dddd`,
+			ExpectedString:  "Ext. IP: 0000:aaaa:1111:bbbb:2222:cccc:3333:dddd",
+			ExpectedEnabled: true,
+			Template:        "Ext. IP: {{.IP}}",
+		},
+		{
+			Case:            "Error in retrieving data",
+			Response:        "nonsense",
+			Error:           errors.New("Something went wrong"),
+			ExpectedEnabled: false,
+		},
+	}
+
+	for _, tc := range cases {
+		env := &MockedEnvironment{}
+		var props properties = map[Property]interface{}{
+			CacheTimeout: 0,
+		}
+
+		env.On("doGet", IPIFYAPIURL).Return([]byte(tc.Response), tc.Error)
+
+		if tc.Template != "" {
+			props[SegmentTemplate] = tc.Template
+		}
+
+		o := &ipify{
+			props: props,
+			env:   env,
+		}
+
+		enabled := o.enabled()
+		assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case)
+		if !enabled {
+			continue
+		}
+
+		assert.Equal(t, tc.ExpectedString, o.string(), tc.Case)
+	}
+}