Repository: JanDeDobbeleer/oh-my-posh
Pull request #2614 (2022-08-04 19:23:57)

feat(ipify): added offline response
### 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)


### Description
Added offline response for IPify when the HTTPRequest returns an error (httpTimeout)
### Why do that?
Because the  IPify segment can mainly be used for check online connection, personally it seems more useful if I can see when I'm offline than just returning 'null'/nothing to the shell.
![image](https://user-images.githubusercontent.com/40523695/182764456-06558890-02a0-4869-b64d-a795f4bc3b1d.png)

### About the tests:
I couldn't make a useful test that check if it's online or offline, what I did is check if `err == Get "https://api.ipify.org": dial tcp: lookup api.ipify.org: no such host`, then it will `return "OFFLINE"`, however in the case of any other error occurs, it wil `return "", err`

If approved I'll update the docs and include the description of this feature.

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary


Patch:
diff --git a/src/environment/shell.go b/src/environment/shell.go
index 07b8bfab..dbfea3f9 100644
--- a/src/environment/shell.go
+++ b/src/environment/shell.go
@@ -587,6 +587,19 @@ func (env *ShellEnvironment) Shell() string {
 	return env.CmdFlags.Shell
 }
 
+func (env *ShellEnvironment) unWrapError(err error) error {
+	cause := err
+	for {
+		type nested interface{ Unwrap() error }
+		unwrap, ok := cause.(nested)
+		if !ok {
+			break
+		}
+		cause = unwrap.Unwrap()
+	}
+	return cause
+}
+
 func (env *ShellEnvironment) HTTPRequest(targetURL string, body io.Reader, timeout int, requestModifiers ...HTTPRequestModifier) ([]byte, error) {
 	defer env.Trace(time.Now(), "HTTPRequest", targetURL)
 	ctx, cncl := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
@@ -605,7 +618,7 @@ func (env *ShellEnvironment) HTTPRequest(targetURL string, body io.Reader, timeo
 	response, err := client.Do(request)
 	if err != nil {
 		env.Log(Error, "HTTPRequest", err.Error())
-		return nil, err
+		return nil, env.unWrapError(err)
 	}
 	// anything inside the range [200, 299] is considered a success
 	if response.StatusCode < 200 || response.StatusCode >= 300 {
diff --git a/src/segments/ipify.go b/src/segments/ipify.go
index 6e139a63..9aefa4aa 100644
--- a/src/segments/ipify.go
+++ b/src/segments/ipify.go
@@ -1,6 +1,7 @@
 package segments
 
 import (
+	"net"
 	"oh-my-posh/environment"
 	"oh-my-posh/properties"
 )
@@ -13,6 +14,8 @@ type IPify struct {
 
 const (
 	IpifyURL properties.Property = "url"
+
+	OFFLINE = "OFFLINE"
 )
 
 func (i *IPify) Template() string {
@@ -46,6 +49,9 @@ func (i *IPify) getResult() (string, error) {
 	httpTimeout := i.props.GetInt(properties.HTTPTimeout, properties.DefaultHTTPTimeout)
 
 	body, err := i.env.HTTPRequest(url, nil, httpTimeout)
+	if dnsErr, OK := err.(*net.DNSError); OK && dnsErr.IsNotFound {
+		return OFFLINE, nil
+	}
 	if err != nil {
 		return "", err
 	}


Test patch:
diff --git a/src/segments/ipify_test.go b/src/segments/ipify_test.go
index f701f11d..95575e95 100644
--- a/src/segments/ipify_test.go
+++ b/src/segments/ipify_test.go
@@ -2,6 +2,7 @@ package segments
 
 import (
 	"errors"
+	"net"
 	"oh-my-posh/mock"
 	"oh-my-posh/properties"
 	"testing"
@@ -36,11 +37,18 @@ func TestIpifySegment(t *testing.T) {
 			Template:        "Ext. IP: {{.IP}}",
 		},
 		{
-			Case:            "Error in retrieving data",
+			Case:            "Network Error",
 			Response:        "nonsense",
-			Error:           errors.New("Something went wrong"),
+			ExpectedString:  "",
+			Error:           errors.New("network is unreachable"),
 			ExpectedEnabled: false,
 		},
+		{
+			Case:            "Offline",
+			ExpectedString:  OFFLINE,
+			Error:           &net.DNSError{IsNotFound: true},
+			ExpectedEnabled: true,
+		},
 	}
 
 	for _, tc := range cases {