Repository: JanDeDobbeleer/oh-my-posh
Pull request #1295 (2021-11-24 09:57:02)

feat: nightscout 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

closes #1288 
<!---

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_nightscout.md b/docs/docs/segment_nightscout.md
new file mode 100644
index 00000000..54456408
--- /dev/null
+++ b/docs/docs/segment_nightscout.md
@@ -0,0 +1,88 @@
+---
+id: nightscout
+title: Nightscout
+sidebar_label: Nightscout
+---
+
+## What
+
+[Nightscout][nightscout] (CGM in the Cloud) is an open source,
+DIY project that allows real time access to a CGM data via an HTTP REST API. It
+is used for secure remote viewing of blood sugar data from anywhere...including
+OhMyPosh segments on the command line!
+
+## Sample Configuration
+
+This example is using mg/dl, you'll want change the numbers for mmol. Your idea
+of "high" or "low" is different from others. You'll also want to think about
+your background and foreground colors. Don't use white text on a yellow
+background, for example.
+
+The foreground_templates example below could be set to just a single color,
+if that color is visible against any of your backgrounds.
+
+```json
+{
+  "type": "nightscout",
+  "style": "diamond",
+  "foreground": "#ffffff",
+  "background": "#ff0000",
+  "background_templates": [
+    "{{ if gt .Sgv 150 }}#FFFF00{{ end }}",
+    "{{ if lt .Sgv 60 }}#FF0000{{ end }}",
+    "#00FF00"
+  ],
+  "foreground_templates": [
+    "{{ if gt .Sgv 150 }}#000000{{ end }}",
+    "{{ if lt .Sgv 60 }}#000000{{ end }}",
+    "#000000"
+  ],
+
+  "leading_diamond": "",
+  "trailing_diamond": "\uE0B0",
+  "properties": {
+    "url": "https://YOURNIGHTSCOUTAPP.herokuapp.com/api/v1/entries.json?count=1&token=APITOKENFROMYOURADMIN",
+    "http_timeout": 1500,
+    "template": " {{.Sgv}}{{.TrendIcon}}"
+  }
+},
+```
+
+## Properties
+
+- url: `string` - Your Nightscout URL, inclding the full path to entries.json
+  AND count=1 AND token. Example above. You'll know this works if you can curl
+  it yourself and get a single value. - defaults to ``
+- http_timeout: `int` - How long do you want to wait before you want to see
+  your prompt more than your sugar? I figure a half second is a good default -
+  defaults to 500ms
+- template: `string` - a go [text/template][go-text-template] template extended
+  with [sprig][sprig] utilizing the properties below.
+  See the example above where I added a syringe.
+  You can change the icon, put the trend elsewhere, add text, however you like!
+  Make sure your NerdFont has the glyph you want or search for one
+  at nerdfonts.com
+- NSCacheTimeout: `int` in minutes - How long do you want your numbers cached? -
+  defaults to 5 min
+
+- NOTE: You can change the icons for trend, put the trend elsewhere, add text,
+  however you like!
+  Make sure your NerdFont has the glyph you want or search for one at
+  nerdfonts.com
+- DoubleUpIcon - defaults to ↑↑
+- SingleUpIcon - defaults to ↑
+- FortyFiveUpIcon - defaults to ↗
+- FlatIcon - defaults to →
+- FortyFiveDownIcon - defaults to ↘
+- SingleDownIcon - defaults to ↓
+- DoubleDownIcon - defaults to ↓↓
+
+## Template Properties
+
+- .Sgv: `int` - Your Serum Glucose Value (your sugar)
+- .TrendIcon: `string` - By default, this will be something like ↑↑ or ↘ etc but you can
+  override them with any glpyh as seen above
+
+[go-text-template]: https://golang.org/pkg/text/template/
+[sprig]: https://masterminds.github.io/sprig/
+[nightscout]: http://www.nightscout.info/
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 693f61c9..17f82305 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -55,6 +55,7 @@ module.exports = {
         "julia",
         "kubectl",
         "nbgv",
+        "nightscout",
         "node",
         "os",
         "owm",
diff --git a/src/segment.go b/src/segment.go
index 2b0dfd58..c75196c9 100644
--- a/src/segment.go
+++ b/src/segment.go
@@ -130,6 +130,8 @@ const (
 	Angular SegmentType = "angular"
 	// PHP writes which php version is currently active
 	PHP SegmentType = "php"
+	// Nightscout is an open source diabetes system
+	Nightscout SegmentType = "nightscout"
 )
 
 func (segment *Segment) string() string {
@@ -263,6 +265,7 @@ func (segment *Segment) mapSegmentWithWriter(env environmentInfo) error {
 		SysInfo:       &sysinfo{},
 		Angular:       &angular{},
 		PHP:           &php{},
+		Nightscout:    &nightscout{},
 	}
 	if writer, ok := functions[segment.Type]; ok {
 		props := &properties{
diff --git a/src/segment_nightscout.go b/src/segment_nightscout.go
new file mode 100644
index 00000000..1047486b
--- /dev/null
+++ b/src/segment_nightscout.go
@@ -0,0 +1,134 @@
+package main
+
+import (
+	"encoding/json"
+)
+
+// segment struct, makes templating easier
+type nightscout struct {
+	props *properties
+	env   environmentInfo
+	// array of nightscoutData (is often just 1, and we will pick the ZEROeth)
+
+	Sgv       int64
+	Direction string
+
+	TrendIcon string
+}
+
+const (
+	// Your complete Nightscout URL and APIKey like this
+	URL Property = "url"
+
+	DoubleUpIcon      Property = "doubleup_icon"
+	SingleUpIcon      Property = "singleup_icon"
+	FortyFiveUpIcon   Property = "fortyfiveup_icon"
+	FlatIcon          Property = "flat_icon"
+	FortyFiveDownIcon Property = "fortyfivedown_icon"
+	SingleDownIcon    Property = "singledown_icon"
+	DoubleDownIcon    Property = "doubledown_icon"
+
+	NSCacheTimeout Property = "cache_timeout"
+)
+
+type nightscoutData struct {
+	Sgv       int64  `json:"sgv"`
+	Direction string `json:"direction"`
+}
+
+func (ns *nightscout) enabled() bool {
+	data, err := ns.getResult()
+	if err != nil {
+		return false
+	}
+	ns.Sgv = data.Sgv
+	ns.Direction = data.Direction
+
+	ns.TrendIcon = ns.getTrendIcon()
+
+	return true
+}
+
+func (ns *nightscout) getTrendIcon() string {
+	switch ns.Direction {
+	case "DoubleUp":
+		return ns.props.getString(DoubleUpIcon, "↑↑")
+	case "SingleUp":
+		return ns.props.getString(SingleUpIcon, "↑")
+	case "FortyFiveUp":
+		return ns.props.getString(FortyFiveUpIcon, "↗")
+	case "Flat":
+		return ns.props.getString(FlatIcon, "→")
+	case "FortyFiveDown":
+		return ns.props.getString(FortyFiveDownIcon, "↘")
+	case "SingleDown":
+		return ns.props.getString(SingleDownIcon, "↓")
+	case "DoubleDown":
+		return ns.props.getString(DoubleDownIcon, "↓↓")
+	default:
+		return ""
+	}
+}
+
+func (ns *nightscout) string() string {
+	segmentTemplate := ns.props.getString(SegmentTemplate, "{{.Sgv}}")
+	template := &textTemplate{
+		Template: segmentTemplate,
+		Context:  ns,
+		Env:      ns.env,
+	}
+	text, err := template.render()
+	if err != nil {
+		return err.Error()
+	}
+
+	return text
+}
+
+func (ns *nightscout) getResult() (*nightscoutData, error) {
+	url := ns.props.getString(URL, "")
+	// natural and understood NS timeout is 5, anything else is unusual
+	cacheTimeout := ns.props.getInt(NSCacheTimeout, 5)
+	response := &nightscoutData{}
+	if cacheTimeout > 0 {
+		// check if data stored in cache
+		val, found := ns.env.cache().get(url)
+		// we got something from the cache
+		if found {
+			err := json.Unmarshal([]byte(val), response)
+			if err != nil {
+				return nil, err
+			}
+			return response, nil
+		}
+	}
+
+	httpTimeout := ns.props.getInt(HTTPTimeout, DefaultHTTPTimeout)
+
+	body, err := ns.env.doGet(url, httpTimeout)
+	if err != nil {
+		return &nightscoutData{}, err
+	}
+	var arr []*nightscoutData
+	err = json.Unmarshal(body, &arr)
+	if err != nil {
+		return &nightscoutData{}, err
+	}
+
+	firstelement := arr[0]
+	firstData, err := json.Marshal(firstelement)
+	if err != nil {
+		return &nightscoutData{}, err
+	}
+
+	if cacheTimeout > 0 {
+		// persist new sugars in cache
+		ns.env.cache().set(url, string(firstData), cacheTimeout)
+	}
+	return firstelement, nil
+}
+
+func (ns *nightscout) init(props *properties, env environmentInfo) {
+	ns.props = props
+	ns.env = env
+}


Test patch:
diff --git a/src/segment_nightscout_test.go b/src/segment_nightscout_test.go
new file mode 100644
index 00000000..b3286904
--- /dev/null
+++ b/src/segment_nightscout_test.go
@@ -0,0 +1,115 @@
+package main
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	NSAPIURL = "FAKE"
+)
+
+func TestNSSegment(t *testing.T) {
+	cases := []struct {
+		Case            string
+		JSONResponse    string
+		ExpectedString  string
+		ExpectedEnabled bool
+		Template        string
+		Error           error
+	}{
+		{
+			Case: "Flat 150",
+			JSONResponse: `
+			[{"_id":"619d6fa819696e8ded5b2206","sgv":150,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"Flat","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
+			Template:        " {{.Sgv}}{{.TrendIcon}}",
+			ExpectedString:  " 150→",
+			ExpectedEnabled: true,
+		},
+		{
+			Case: "DoubleDown 50",
+			JSONResponse: `
+			[{"_id":"619d6fa819696e8ded5b2206","sgv":50,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"DoubleDown","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
+			Template:        " {{.Sgv}}{{.TrendIcon}}",
+			ExpectedString:  " 50↓↓",
+			ExpectedEnabled: true,
+		},
+		{
+			Case: "DoubleUp 250",
+			JSONResponse: `
+			[{"_id":"619d6fa819696e8ded5b2206","sgv":250,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"DoubleUp","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
+			Template:        " {{.Sgv}}{{.TrendIcon}}",
+			ExpectedString:  " 250↑↑",
+			ExpectedEnabled: true,
+		},
+		{
+			Case: "SingleUp 130",
+			JSONResponse: `
+			[{"_id":"619d6fa819696e8ded5b2206","sgv":130,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"SingleUp","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
+			Template:        " {{.Sgv}}{{.TrendIcon}}",
+			ExpectedString:  " 130↑",
+			ExpectedEnabled: true,
+		},
+		{
+			Case: "FortyFiveUp 174",
+			JSONResponse: `
+			[{"_id":"619d6fa819696e8ded5b2206","sgv":174,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"FortyFiveUp","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
+			Template:        " {{.Sgv}}{{.TrendIcon}}",
+			ExpectedString:  " 174↗",
+			ExpectedEnabled: true,
+		},
+		{
+			Case: "FortyFiveDown 61",
+			JSONResponse: `
+			[{"_id":"619d6fa819696e8ded5b2206","sgv":61,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"FortyFiveDown","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
+			Template:        " {{.Sgv}}{{.TrendIcon}}",
+			ExpectedString:  " 61↘",
+			ExpectedEnabled: true,
+		},
+		{
+			Case: "DoubleDown 50",
+			JSONResponse: `
+			[{"_id":"619d6fa819696e8ded5b2206","sgv":50,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"DoubleDown","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
+			Template:        " {{.Sgv}}{{.TrendIcon}}",
+			ExpectedString:  " 50↓↓",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Error in retrieving data",
+			JSONResponse:    "nonsense",
+			Error:           errors.New("Something went wrong"),
+			ExpectedEnabled: false,
+		},
+	}
+
+	for _, tc := range cases {
+		env := &MockedEnvironment{}
+		props := &properties{
+			values: map[Property]interface{}{
+				CacheTimeout: 0,
+				URL:          "FAKE",
+			},
+		}
+
+		env.On("doGet", NSAPIURL).Return([]byte(tc.JSONResponse), tc.Error)
+
+		if tc.Template != "" {
+			props.values[SegmentTemplate] = tc.Template
+		}
+
+		ns := &nightscout{
+			props: props,
+			env:   env,
+		}
+
+		enabled := ns.enabled()
+		assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case)
+		if !enabled {
+			continue
+		}
+
+		assert.Equal(t, tc.ExpectedString, ns.string(), tc.Case)
+	}
+}