Repository: JanDeDobbeleer/oh-my-posh
Pull request #4396 (2023-10-28 09:13:52)

feat: add gb carbon intensity segment
### 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).
- [x] Docs have been added/updated (for bug fixes / features).

### Description

<!--
copilot:summary
-->
### <samp>🤖 Generated by Copilot at 2e222df</samp>

This pull request adds a new segment type, `gbcarbonintensity`, to the oh-my-posh project. The segment displays the carbon intensity of the electricity grid in Great Britain, using data from an API. The pull request includes changes to the `engine` package, the JSON schema file, the unit tests, the segment implementation, the documentation, and the website sidebar.

### How

<!--
copilot:walkthrough
-->
### <samp>🤖 Generated by Copilot at 2e222df</samp>

*  Add a new segment type `GBCARBONINTENSITY` to display the GB carbon intensity data ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4396/files?diff=unified&w=0#diff-cd158856c1b1aa6b2acece8041eb07352e4247b700e0fc62798a6be35d5f1c7dR143-R144), [link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4396/files?diff=unified&w=0#diff-892c540c9bfebcb3bb8a0be1714201cd17eb6f0d40367c09f80d56e883b7335eR270))
*  Implement the `GbCarbonIntensity` struct and its methods in `src/segments/gb_carbon_intensity.go` ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4396/files?diff=unified&w=0#diff-a77acd4f0484172c3bfe64b83171f98f8d0549f2d74098a34965d9be570cf14eR1-R166))
*  Add unit tests for the `GbCarbonIntensity` struct and its methods in `src/segments/gb_carbon_intensity_test.go` ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4396/files?diff=unified&w=0#diff-8ea3c9c1edf78796b043f50199f3cbf5703442667dcbf0c389b30d72596ca419R1-R266))
*  Add a new entry to the `Segments` map to associate the segment type with the segment writer ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4396/files?diff=unified&w=0#diff-cd158856c1b1aa6b2acece8041eb07352e4247b700e0fc62798a6be35d5f1c7dL258-R339))
*  Add a new conditional schema for the segment configuration in `themes/schema.json` ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4396/files?diff=unified&w=0#diff-892c540c9bfebcb3bb8a0be1714201cd17eb6f0d40367c09f80d56e883b7335eR852-R876))
*  Add the documentation for the segment in `website/docs/segments/gbcarbonintensity.mdx` ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4396/files?diff=unified&w=0#diff-0076d86ff519093be1d2c5e684349e9328ad62b417d83b3dba150344e7741461R1-R72))
*  Add the segment documentation page to the sidebar navigation in `website/sidebars.js` ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4396/files?diff=unified&w=0#diff-4caaf3187919c98f42856c140e32964b0829fc19165f3c989a6cd4ea088a7164R77))

<!---

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.

-->

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary


JanDeDobbeleer: @AlexJPotter I would rename this to `CarbonIntensity` and add a disclaimer it's only supported for the UK for now. That way we can add other countries/regions as well without breaking changes. The implementation can stay UK specific, until we have other contributions/possibilities.
AlexJPotter: > @AlexJPotter I would rename this to `CarbonIntensity` and add a disclaimer it's only supported for the UK for now. That way we can add other countries/regions as well without breaking changes. The implementation can stay UK specific, until we have other contributions/possibilities.

Thanks for taking a look @JanDeDobbeleer - I've made the refactor and pushed the changes 😃 

Patch:
diff --git a/src/engine/segment.go b/src/engine/segment.go
index a2107745..e4403ea8 100644
--- a/src/engine/segment.go
+++ b/src/engine/segment.go
@@ -108,6 +108,8 @@ const (
 	BREWFATHER SegmentType = "brewfather"
 	// Buf segment writes the active buf version
 	BUF SegmentType = "buf"
+	// CARBONINTENSITY writes the actual and forecast carbon intensity in gCO2/kWh
+	CARBONINTENSITY SegmentType = "carbonintensity"
 	// cds (SAP CAP) version
 	CDS SegmentType = "cds"
 	// Cloud Foundry segment
@@ -255,85 +257,86 @@ const (
 // Segments contains all available prompt segment writers.
 // Consumers of the library can also add their own segment writer.
 var Segments = map[SegmentType]func() SegmentWriter{
-	ANGULAR:       func() SegmentWriter { return &segments.Angular{} },
-	ARGOCD:        func() SegmentWriter { return &segments.Argocd{} },
-	AWS:           func() SegmentWriter { return &segments.Aws{} },
-	AZ:            func() SegmentWriter { return &segments.Az{} },
-	AZFUNC:        func() SegmentWriter { return &segments.AzFunc{} },
-	BATTERY:       func() SegmentWriter { return &segments.Battery{} },
-	BREWFATHER:    func() SegmentWriter { return &segments.Brewfather{} },
-	BUF:           func() SegmentWriter { return &segments.Buf{} },
-	CDS:           func() SegmentWriter { return &segments.Cds{} },
-	CF:            func() SegmentWriter { return &segments.Cf{} },
-	CFTARGET:      func() SegmentWriter { return &segments.CfTarget{} },
-	CMD:           func() SegmentWriter { return &segments.Cmd{} },
-	CONNECTION:    func() SegmentWriter { return &segments.Connection{} },
-	CRYSTAL:       func() SegmentWriter { return &segments.Crystal{} },
-	CMAKE:         func() SegmentWriter { return &segments.Cmake{} },
-	DART:          func() SegmentWriter { return &segments.Dart{} },
-	DENO:          func() SegmentWriter { return &segments.Deno{} },
-	DOCKER:        func() SegmentWriter { return &segments.Docker{} },
-	DOTNET:        func() SegmentWriter { return &segments.Dotnet{} },
-	EXECUTIONTIME: func() SegmentWriter { return &segments.Executiontime{} },
-	ELIXIR:        func() SegmentWriter { return &segments.Elixir{} },
-	EXIT:          func() SegmentWriter { return &segments.Status{} },
-	FLUTTER:       func() SegmentWriter { return &segments.Flutter{} },
-	FOSSIL:        func() SegmentWriter { return &segments.Fossil{} },
-	GCP:           func() SegmentWriter { return &segments.Gcp{} },
-	GIT:           func() SegmentWriter { return &segments.Git{} },
-	GITVERSION:    func() SegmentWriter { return &segments.GitVersion{} },
-	GOLANG:        func() SegmentWriter { return &segments.Golang{} },
-	HASKELL:       func() SegmentWriter { return &segments.Haskell{} },
-	HELM:          func() SegmentWriter { return &segments.Helm{} },
-	IPIFY:         func() SegmentWriter { return &segments.IPify{} },
-	ITERM:         func() SegmentWriter { return &segments.ITerm{} },
-	JAVA:          func() SegmentWriter { return &segments.Java{} },
-	JULIA:         func() SegmentWriter { return &segments.Julia{} },
-	KOTLIN:        func() SegmentWriter { return &segments.Kotlin{} },
-	KUBECTL:       func() SegmentWriter { return &segments.Kubectl{} },
-	LUA:           func() SegmentWriter { return &segments.Lua{} },
-	MERCURIAL:     func() SegmentWriter { return &segments.Mercurial{} },
-	NBGV:          func() SegmentWriter { return &segments.Nbgv{} },
-	NIGHTSCOUT:    func() SegmentWriter { return &segments.Nightscout{} },
-	NODE:          func() SegmentWriter { return &segments.Node{} },
-	NPM:           func() SegmentWriter { return &segments.Npm{} },
-	NX:            func() SegmentWriter { return &segments.Nx{} },
-	OS:            func() SegmentWriter { return &segments.Os{} },
-	OWM:           func() SegmentWriter { return &segments.Owm{} },
-	PATH:          func() SegmentWriter { return &segments.Path{} },
-	PERL:          func() SegmentWriter { return &segments.Perl{} },
-	PHP:           func() SegmentWriter { return &segments.Php{} },
-	PLASTIC:       func() SegmentWriter { return &segments.Plastic{} },
-	PROJECT:       func() SegmentWriter { return &segments.Project{} },
-	PYTHON:        func() SegmentWriter { return &segments.Python{} },
-	QUASAR:        func() SegmentWriter { return &segments.Quasar{} },
-	R:             func() SegmentWriter { return &segments.R{} },
-	ROOT:          func() SegmentWriter { return &segments.Root{} },
-	RUBY:          func() SegmentWriter { return &segments.Ruby{} },
-	RUST:          func() SegmentWriter { return &segments.Rust{} },
-	SAPLING:       func() SegmentWriter { return &segments.Sapling{} },
-	SESSION:       func() SegmentWriter { return &segments.Session{} },
-	SHELL:         func() SegmentWriter { return &segments.Shell{} },
-	SITECORE:      func() SegmentWriter { return &segments.Sitecore{} },
-	SPOTIFY:       func() SegmentWriter { return &segments.Spotify{} },
-	STATUS:        func() SegmentWriter { return &segments.Status{} },
-	STRAVA:        func() SegmentWriter { return &segments.Strava{} },
-	SVN:           func() SegmentWriter { return &segments.Svn{} },
-	SWIFT:         func() SegmentWriter { return &segments.Swift{} },
-	SYSTEMINFO:    func() SegmentWriter { return &segments.SystemInfo{} },
-	TERRAFORM:     func() SegmentWriter { return &segments.Terraform{} },
-	TEXT:          func() SegmentWriter { return &segments.Text{} },
-	TIME:          func() SegmentWriter { return &segments.Time{} },
-	UI5TOOLING:    func() SegmentWriter { return &segments.UI5Tooling{} },
-	UMBRACO:       func() SegmentWriter { return &segments.Umbraco{} },
-	UNITY:         func() SegmentWriter { return &segments.Unity{} },
-	UPGRADE:       func() SegmentWriter { return &segments.Upgrade{} },
-	VALA:          func() SegmentWriter { return &segments.Vala{} },
-	WAKATIME:      func() SegmentWriter { return &segments.Wakatime{} },
-	WINREG:        func() SegmentWriter { return &segments.WindowsRegistry{} },
-	WITHINGS:      func() SegmentWriter { return &segments.Withings{} },
-	XMAKE:         func() SegmentWriter { return &segments.XMake{} },
-	YTM:           func() SegmentWriter { return &segments.Ytm{} },
+	ANGULAR:         func() SegmentWriter { return &segments.Angular{} },
+	ARGOCD:          func() SegmentWriter { return &segments.Argocd{} },
+	AWS:             func() SegmentWriter { return &segments.Aws{} },
+	AZ:              func() SegmentWriter { return &segments.Az{} },
+	AZFUNC:          func() SegmentWriter { return &segments.AzFunc{} },
+	BATTERY:         func() SegmentWriter { return &segments.Battery{} },
+	BREWFATHER:      func() SegmentWriter { return &segments.Brewfather{} },
+	BUF:             func() SegmentWriter { return &segments.Buf{} },
+	CARBONINTENSITY: func() SegmentWriter { return &segments.CarbonIntensity{} },
+	CDS:             func() SegmentWriter { return &segments.Cds{} },
+	CF:              func() SegmentWriter { return &segments.Cf{} },
+	CFTARGET:        func() SegmentWriter { return &segments.CfTarget{} },
+	CMD:             func() SegmentWriter { return &segments.Cmd{} },
+	CONNECTION:      func() SegmentWriter { return &segments.Connection{} },
+	CRYSTAL:         func() SegmentWriter { return &segments.Crystal{} },
+	CMAKE:           func() SegmentWriter { return &segments.Cmake{} },
+	DART:            func() SegmentWriter { return &segments.Dart{} },
+	DENO:            func() SegmentWriter { return &segments.Deno{} },
+	DOCKER:          func() SegmentWriter { return &segments.Docker{} },
+	DOTNET:          func() SegmentWriter { return &segments.Dotnet{} },
+	EXECUTIONTIME:   func() SegmentWriter { return &segments.Executiontime{} },
+	ELIXIR:          func() SegmentWriter { return &segments.Elixir{} },
+	EXIT:            func() SegmentWriter { return &segments.Status{} },
+	FLUTTER:         func() SegmentWriter { return &segments.Flutter{} },
+	FOSSIL:          func() SegmentWriter { return &segments.Fossil{} },
+	GCP:             func() SegmentWriter { return &segments.Gcp{} },
+	GIT:             func() SegmentWriter { return &segments.Git{} },
+	GITVERSION:      func() SegmentWriter { return &segments.GitVersion{} },
+	GOLANG:          func() SegmentWriter { return &segments.Golang{} },
+	HASKELL:         func() SegmentWriter { return &segments.Haskell{} },
+	HELM:            func() SegmentWriter { return &segments.Helm{} },
+	IPIFY:           func() SegmentWriter { return &segments.IPify{} },
+	ITERM:           func() SegmentWriter { return &segments.ITerm{} },
+	JAVA:            func() SegmentWriter { return &segments.Java{} },
+	JULIA:           func() SegmentWriter { return &segments.Julia{} },
+	KOTLIN:          func() SegmentWriter { return &segments.Kotlin{} },
+	KUBECTL:         func() SegmentWriter { return &segments.Kubectl{} },
+	LUA:             func() SegmentWriter { return &segments.Lua{} },
+	MERCURIAL:       func() SegmentWriter { return &segments.Mercurial{} },
+	NBGV:            func() SegmentWriter { return &segments.Nbgv{} },
+	NIGHTSCOUT:      func() SegmentWriter { return &segments.Nightscout{} },
+	NODE:            func() SegmentWriter { return &segments.Node{} },
+	NPM:             func() SegmentWriter { return &segments.Npm{} },
+	NX:              func() SegmentWriter { return &segments.Nx{} },
+	OS:              func() SegmentWriter { return &segments.Os{} },
+	OWM:             func() SegmentWriter { return &segments.Owm{} },
+	PATH:            func() SegmentWriter { return &segments.Path{} },
+	PERL:            func() SegmentWriter { return &segments.Perl{} },
+	PHP:             func() SegmentWriter { return &segments.Php{} },
+	PLASTIC:         func() SegmentWriter { return &segments.Plastic{} },
+	PROJECT:         func() SegmentWriter { return &segments.Project{} },
+	PYTHON:          func() SegmentWriter { return &segments.Python{} },
+	QUASAR:          func() SegmentWriter { return &segments.Quasar{} },
+	R:               func() SegmentWriter { return &segments.R{} },
+	ROOT:            func() SegmentWriter { return &segments.Root{} },
+	RUBY:            func() SegmentWriter { return &segments.Ruby{} },
+	RUST:            func() SegmentWriter { return &segments.Rust{} },
+	SAPLING:         func() SegmentWriter { return &segments.Sapling{} },
+	SESSION:         func() SegmentWriter { return &segments.Session{} },
+	SHELL:           func() SegmentWriter { return &segments.Shell{} },
+	SITECORE:        func() SegmentWriter { return &segments.Sitecore{} },
+	SPOTIFY:         func() SegmentWriter { return &segments.Spotify{} },
+	STATUS:          func() SegmentWriter { return &segments.Status{} },
+	STRAVA:          func() SegmentWriter { return &segments.Strava{} },
+	SVN:             func() SegmentWriter { return &segments.Svn{} },
+	SWIFT:           func() SegmentWriter { return &segments.Swift{} },
+	SYSTEMINFO:      func() SegmentWriter { return &segments.SystemInfo{} },
+	TERRAFORM:       func() SegmentWriter { return &segments.Terraform{} },
+	TEXT:            func() SegmentWriter { return &segments.Text{} },
+	TIME:            func() SegmentWriter { return &segments.Time{} },
+	UI5TOOLING:      func() SegmentWriter { return &segments.UI5Tooling{} },
+	UMBRACO:         func() SegmentWriter { return &segments.Umbraco{} },
+	UNITY:           func() SegmentWriter { return &segments.Unity{} },
+	UPGRADE:         func() SegmentWriter { return &segments.Upgrade{} },
+	VALA:            func() SegmentWriter { return &segments.Vala{} },
+	WAKATIME:        func() SegmentWriter { return &segments.Wakatime{} },
+	WINREG:          func() SegmentWriter { return &segments.WindowsRegistry{} },
+	WITHINGS:        func() SegmentWriter { return &segments.Withings{} },
+	XMAKE:           func() SegmentWriter { return &segments.XMake{} },
+	YTM:             func() SegmentWriter { return &segments.Ytm{} },
 }
 
 func (segment *Segment) style() SegmentStyle {
diff --git a/src/segments/carbon_intensity.go b/src/segments/carbon_intensity.go
new file mode 100644
index 00000000..35abd2e0
--- /dev/null
+++ b/src/segments/carbon_intensity.go
@@ -0,0 +1,155 @@
+package segments
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/jandedobbeleer/oh-my-posh/src/platform"
+	"github.com/jandedobbeleer/oh-my-posh/src/properties"
+)
+
+type CarbonIntensity struct {
+	props properties.Properties
+	env   platform.Environment
+
+	TrendIcon string
+
+	CarbonIntensityData
+}
+
+type CarbonIntensityResponse struct {
+	Data []CarbonIntensityPeriod `json:"data"`
+}
+
+type CarbonIntensityPeriod struct {
+	From      string               `json:"from"`
+	To        string               `json:"to"`
+	Intensity *CarbonIntensityData `json:"intensity"`
+}
+
+type CarbonIntensityData struct {
+	Forecast Number `json:"forecast"`
+	Actual   Number `json:"actual"`
+	Index    Index  `json:"index"`
+}
+
+type Number int
+
+func (n Number) String() string {
+	if n == 0 {
+		return "??"
+	}
+
+	return fmt.Sprintf("%d", n)
+}
+
+type Index string
+
+func (i Index) Icon() string {
+	switch i {
+	case "very low":
+		return "↓↓"
+	case "low":
+		return "↓"
+	case "moderate":
+		return "•"
+	case "high":
+		return "↑"
+	case "very high":
+		return "↑↑"
+	default:
+		return ""
+	}
+}
+
+func (d *CarbonIntensity) Enabled() bool {
+	err := d.setStatus()
+
+	if err != nil {
+		d.env.Error(err)
+		return false
+	}
+
+	return true
+}
+
+func (d *CarbonIntensity) Template() string {
+	return " CO₂ {{ .Index.Icon }}{{ .Actual.String }} {{ .TrendIcon }} {{ .Forecast.String }} "
+}
+
+func (d *CarbonIntensity) updateCache(responseBody []byte, url string, cacheTimeoutInMinutes int) {
+	if cacheTimeoutInMinutes > 0 {
+		d.env.Cache().Set(url, string(responseBody), cacheTimeoutInMinutes)
+	}
+}
+
+func (d *CarbonIntensity) getResult() (*CarbonIntensityResponse, error) {
+	cacheTimeoutInMinutes := d.props.GetInt(properties.CacheTimeout, properties.DefaultCacheTimeout)
+
+	response := new(CarbonIntensityResponse)
+	url := "https://api.carbonintensity.org.uk/intensity"
+
+	if cacheTimeoutInMinutes > 0 {
+		cachedValue, foundInCache := d.env.Cache().Get(url)
+
+		if foundInCache {
+			err := json.Unmarshal([]byte(cachedValue), response)
+			if err == nil {
+				return response, nil
+			}
+			// If there was an error, just fall through to refetching
+		}
+	}
+
+	httpTimeout := d.props.GetInt(properties.HTTPTimeout, properties.DefaultHTTPTimeout)
+
+	body, err := d.env.HTTPRequest(url, nil, httpTimeout)
+	if err != nil {
+		d.updateCache(body, url, cacheTimeoutInMinutes)
+		return new(CarbonIntensityResponse), err
+	}
+
+	err = json.Unmarshal(body, &response)
+	if err != nil {
+		d.updateCache(body, url, cacheTimeoutInMinutes)
+		return new(CarbonIntensityResponse), err
+	}
+
+	return response, nil
+}
+
+func (d *CarbonIntensity) setStatus() error {
+	response, err := d.getResult()
+	if err != nil {
+		return err
+	}
+
+	if len(response.Data) == 0 {
+		d.Actual = 0
+		d.Forecast = 0
+		d.Index = "??"
+		d.TrendIcon = "→"
+		return nil
+	}
+
+	d.CarbonIntensityData = *response.Data[0].Intensity
+
+	if d.Forecast > d.Actual {
+		d.TrendIcon = "↗"
+	}
+
+	if d.Forecast < d.Actual {
+		d.TrendIcon = "↘"
+	}
+
+	if d.Forecast == d.Actual || d.Actual == 0 || d.Forecast == 0 {
+		d.TrendIcon = "→"
+	}
+
+	return nil
+}
+
+func (d *CarbonIntensity) Init(props properties.Properties, env platform.Environment) {
+	d.props = props
+	d.env = env
+}
diff --git a/themes/schema.json b/themes/schema.json
index d4fc42ed..4729a034 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -254,6 +254,7 @@
             "azfunc",
             "angular",
             "battery",
+            "carbonintensity",
             "command",
             "connection",
             "crystal",
@@ -608,6 +609,31 @@
             }
           }
         },
+        {
+          "if": {
+            "properties": {
+              "type": {
+                "const": "carbonintensity"
+              }
+            }
+          },
+          "then": {
+            "title": "Carbon Intensity Segment",
+            "description": "Displays the actual and forecast carbon intensity in gCO2/kWh using the Carbon Intensity API",
+            "properties": {
+              "properties": {
+                "properties": {
+                  "http_timeout": {
+                    "$ref": "#/definitions/http_timeout"
+                  },
+                  "cache_timeout": {
+                    "$ref": "#/definitions/cache_timeout"
+                  }
+                }
+              }
+            }
+          }
+        },
         {
           "if": {
             "properties": {
diff --git a/website/docs/segments/carbonintensity.mdx b/website/docs/segments/carbonintensity.mdx
new file mode 100644
index 00000000..65b4cacb
--- /dev/null
+++ b/website/docs/segments/carbonintensity.mdx
@@ -0,0 +1,85 @@
+---
+id: carbonintensity
+title: Carbon Intensity
+sidebar_label: Carbon Intensity
+---
+
+## What
+
+Shows the actual and forecast carbon intensity in gCO2/kWh using data from the [Carbon Intensity API][carbonintensity-api].
+
+:::note
+Note that this segment only provides data for Great Britain at the moment. Support for other countries may become available in the future.
+:::
+
+## Sample Configuration
+
+:::caution
+The API can be slow. It's recommended to set the `http_timeout` property to a large value (e.g. `5000`).
+:::
+
+import Config from "@site/src/components/Config.js";
+
+<Config
+  data={{
+    type: "carbonintensity",
+    style: "powerline",
+    powerline_symbol: "\uE0B0",
+    foreground: "#000000",
+    background: "#ffffff",
+    background_templates: [
+      '{{if eq "very low" .Index}}#a3e635{{end}}',
+      '{{if eq "low" .Index}}#bef264{{end}}',
+      '{{if eq "moderate" .Index}}#fbbf24{{end}}',
+      '{{if eq "high" .Index}}#ef4444{{end}}',
+      '{{if eq "very high" .Index}}#dc2626{{end}}',
+    ],
+    template:
+      " CO₂ {{ .Index.Icon }}{{ .Actual.String }} {{ .TrendIcon }} {{ .Forecast.String }} ",
+    properties: {
+      http_timeout: 5000,
+      cache_timeout: 10,
+    },
+  }}
+/>
+
+## Properties
+
+| Name            | Type  | Description                                                                                                                                      |
+| --------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `http_timeout`  | `int` | Timeout (in milliseconds) for HTTP requests. The default is 20ms, but you may need to set this to as high as 5000ms to handle slow API requests. |
+| `cache_timeout` | `int` | Timeout (in minutes) for the response cache. The default is 10m. A value of 0 disables the cache.                                                |
+
+## Template ([info][templates])
+
+:::note default template
+
+```template
+CO₂ {{ .Index.Icon }}{{ .Actual.String }} {{ .TrendIcon }} {{ .Forecast.String }}
+```
+
+:::
+
+### Properties
+
+| Name         | Type     | Description                                                                                                                                                             |
+| ------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `.Forecast`  | `Number` | The forecast carbon intensity in gCO2/kWh. Equal to `0` if no data is available.                                                                                        |
+| `.Actual`    | `Number` | The actual carbon intensity in gCO2/kWh. Equal to `0` if no data is available.                                                                                          |
+| `.Index`     | `Index`  | A rating of the current carbon intensity. Possible values are `"very low"`, `"low"`, `"moderate"`, `"high"`, or `"very high"`. Equal to `"??"` if no data is available. |
+| `.TrendIcon` | `string` | An icon representation of the predicted trend in carbon intensity based on the Actual and Forecast values. Possible values are `"↗"`, `"↘"`, or `"→"`.                  |
+
+#### Number
+
+| Name      | Type     | Description                        |
+| --------- | -------- | ---------------------------------- |
+| `.String` | `string` | string representation of the value |
+
+#### Index
+
+| Name    | Type     | Description                      |
+| ------- | -------- | -------------------------------- |
+| `.Icon` | `string` | icon representation of the value |
+
+[templates]: /docs/configuration/templates
+[carbonintensity-api]: https://carbon-intensity.github.io/api-definitions
diff --git a/website/sidebars.js b/website/sidebars.js
index dfeba6bb..5468a315 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -59,6 +59,7 @@ module.exports = {
         "segments/battery",
         "segments/brewfather",
         "segments/buf",
+        "segments/carbonintensity",
         "segments/cds",
         "segments/cf",
         "segments/cftarget",


Test patch:
diff --git a/src/segments/carbon_intensity_test.go b/src/segments/carbon_intensity_test.go
new file mode 100644
index 00000000..c64b2cd4
--- /dev/null
+++ b/src/segments/carbon_intensity_test.go
@@ -0,0 +1,266 @@
+package segments
+
+import (
+	"errors"
+	"fmt"
+	"testing"
+
+	"github.com/jandedobbeleer/oh-my-posh/src/mock"
+	"github.com/jandedobbeleer/oh-my-posh/src/properties"
+
+	"github.com/stretchr/testify/assert"
+	mock2 "github.com/stretchr/testify/mock"
+)
+
+const (
+	CARBONINTENSITYURL = "https://api.carbonintensity.org.uk/intensity"
+)
+
+func TestCarbonIntensitySegmentSingle(t *testing.T) {
+	cases := []struct {
+		Case            string
+		HasError        bool
+		HasData         bool
+		Actual          int
+		Forecast        int
+		Index           string
+		ExpectedString  string
+		ExpectedEnabled bool
+		Template        string
+	}{
+		{
+			Case:            "Very Low, Going Down",
+			HasError:        false,
+			HasData:         true,
+			Actual:          20,
+			Forecast:        10,
+			Index:           "very low",
+			ExpectedString:  "CO₂ ↓↓20 ↘ 10",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Very Low, Staying Same",
+			HasError:        false,
+			HasData:         true,
+			Actual:          20,
+			Forecast:        20,
+			Index:           "very low",
+			ExpectedString:  "CO₂ ↓↓20 → 20",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Very Low, Going Up",
+			HasError:        false,
+			HasData:         true,
+			Actual:          20,
+			Forecast:        30,
+			Index:           "very low",
+			ExpectedString:  "CO₂ ↓↓20 ↗ 30",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Low, Going Down",
+			HasError:        false,
+			HasData:         true,
+			Actual:          100,
+			Forecast:        50,
+			Index:           "low",
+			ExpectedString:  "CO₂ ↓100 ↘ 50",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Low, Staying Same",
+			HasError:        false,
+			HasData:         true,
+			Actual:          100,
+			Forecast:        100,
+			Index:           "low",
+			ExpectedString:  "CO₂ ↓100 → 100",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Low, Going Up",
+			HasError:        false,
+			HasData:         true,
+			Actual:          100,
+			Forecast:        150,
+			Index:           "low",
+			ExpectedString:  "CO₂ ↓100 ↗ 150",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Moderate, Going Down",
+			HasError:        false,
+			HasData:         true,
+			Actual:          150,
+			Forecast:        100,
+			Index:           "moderate",
+			ExpectedString:  "CO₂ •150 ↘ 100",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Moderate, Staying Same",
+			HasError:        false,
+			HasData:         true,
+			Actual:          150,
+			Forecast:        150,
+			Index:           "moderate",
+			ExpectedString:  "CO₂ •150 → 150",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Moderate, Going Up",
+			HasError:        false,
+			HasData:         true,
+			Actual:          150,
+			Forecast:        200,
+			Index:           "moderate",
+			ExpectedString:  "CO₂ •150 ↗ 200",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "High, Going Down",
+			HasError:        false,
+			HasData:         true,
+			Actual:          200,
+			Forecast:        150,
+			Index:           "high",
+			ExpectedString:  "CO₂ ↑200 ↘ 150",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "High, Staying Same",
+			HasError:        false,
+			HasData:         true,
+			Actual:          200,
+			Forecast:        200,
+			Index:           "high",
+			ExpectedString:  "CO₂ ↑200 → 200",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "High, Going Up",
+			HasError:        false,
+			HasData:         true,
+			Actual:          200,
+			Forecast:        300,
+			Index:           "high",
+			ExpectedString:  "CO₂ ↑200 ↗ 300",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Missing Actual",
+			HasError:        false,
+			HasData:         true,
+			Actual:          0, // Missing data will be parsed to the default value of 0
+			Forecast:        300,
+			Index:           "high",
+			ExpectedString:  "CO₂ ↑?? → 300",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Missing Forecast",
+			HasError:        false,
+			HasData:         true,
+			Actual:          200,
+			Forecast:        0, // Missing data will be parsed to the default value of 0
+			Index:           "high",
+			ExpectedString:  "CO₂ ↑200 → ??",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Missing Index",
+			HasError:        false,
+			HasData:         true,
+			Actual:          200,
+			Forecast:        300,
+			Index:           "", // Missing data will be parsed to the default value of ""
+			ExpectedString:  "CO₂ 200 ↗ 300",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Missing Data",
+			HasError:        false,
+			HasData:         false,
+			Actual:          0,
+			Forecast:        0,
+			Index:           "",
+			ExpectedString:  "CO₂ ?? → ??",
+			ExpectedEnabled: true,
+		},
+		{
+			Case:            "Error",
+			HasError:        true,
+			HasData:         false,
+			Actual:          0,
+			Forecast:        0,
+			Index:           "",
+			ExpectedString:  "",
+			ExpectedEnabled: false,
+		},
+	}
+
+	for _, tc := range cases {
+		env := &mock.MockedEnvironment{}
+		var props = properties.Map{
+			properties.HTTPTimeout:  5000,
+			properties.CacheTimeout: 0,
+		}
+
+		jsonResponse := fmt.Sprintf(
+			`{ "data": [ { "from": "2023-10-27T12:30Z", "to": "2023-10-27T13:00Z", "intensity": { "forecast": %d, "actual": %d, "index": "%s" } } ] }`,
+			tc.Forecast, tc.Actual, tc.Index,
+		)
+
+		if !tc.HasData {
+			jsonResponse = `{ "data": [] }`
+		}
+
+		if tc.HasError {
+			jsonResponse = `{ "error": "Something went wrong" }`
+		}
+
+		responseError := errors.New("Something went wrong")
+		if !tc.HasError {
+			responseError = nil
+		}
+
+		env.On("HTTPRequest", CARBONINTENSITYURL).Return([]byte(jsonResponse), responseError)
+		env.On("Error", mock2.Anything)
+
+		d := &CarbonIntensity{
+			props: props,
+			env:   env,
+		}
+
+		enabled := d.Enabled()
+		assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case)
+		if !enabled {
+			continue
+		}
+
+		if tc.Template == "" {
+			tc.Template = d.Template()
+		}
+		assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, d), tc.Case)
+	}
+}
+
+func TestCarbonIntensitySegmentFromCache(t *testing.T) {
+	response := `{ "data": [ { "from": "2023-10-27T12:30Z", "to": "2023-10-27T13:00Z", "intensity": { "forecast": 199, "actual": 193, "index": "moderate" } } ] }`
+	expectedString := "CO₂ •193 ↗ 199"
+
+	env := &mock.MockedEnvironment{}
+	cache := &mock.MockedCache{}
+
+	d := &CarbonIntensity{
+		props: properties.Map{},
+		env:   env,
+	}
+	cache.On("Get", CARBONINTENSITYURL).Return(response, true)
+	cache.On("Set").Return()
+	env.On("Cache").Return(cache)
+
+	assert.Nil(t, d.setStatus())
+	assert.Equal(t, expectedString, renderTemplate(env, d.Template(), d), "should return the cached response")
+}