Repository: JanDeDobbeleer/oh-my-posh
Pull request #1725 (2022-02-08 09:03:13)

feat(haskell): add haskell segment
### Prerequisites

- [x] I have read and understood the `CONTRIBUTING` guide
- [x] The commit message follows the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) guidelines
- [x] Tests for the changes have been added (for bug fixes/features)
- [x] Docs have been added / updated (for bug fixes/features)

### Description

Adds a Haskell segment which fetches the active ghc version. Can also differentiate between ghc on path and the sandboxed ghc used by [stack](https://docs.haskellstack.org/), but using stack ghc causes reduced performance. 
![withStack](https://user-images.githubusercontent.com/85419773/152950870-66abac49-19cf-4596-8794-67bf676b497e.png)
![withoutStack](https://user-images.githubusercontent.com/85419773/152950896-5a5c907b-9b0f-4684-9131-d0ff48a98d00.png)


jedwillick: Thank you for the feedback!
Made the requested changes and fixed the markdown format.
 First time using `go`  so hopefully it wasnt too much of a mess!
JanDeDobbeleer: @jedwillick it was beautiful 👌🏻

Patch:
diff --git a/docs/docs/segment-haskell.md b/docs/docs/segment-haskell.md
new file mode 100644
index 00000000..a9a566fe
--- /dev/null
+++ b/docs/docs/segment-haskell.md
@@ -0,0 +1,63 @@
+---
+id: haskell
+title: Haskell
+sidebar_label: Haskell
+---
+
+## What
+
+Display the currently active Glasgow Haskell Compiler (GHC) version.
+
+## Sample Configuration
+
+```json
+{
+  "type": "haskell",
+  "style": "powerline",
+  "powerline_symbol": "\uE0B0",
+  "foreground": "#906cff",
+  "background": "#100e23",
+  "properties": {
+    "template": " \ue61f {{ .Full }}"
+  }
+}
+```
+
+## Properties
+
+- home_enabled: `boolean` - display the segment in the HOME folder or not - defaults to `false`
+- fetch_version: `boolean` - display the GHC version - defaults to `true`
+- display_error: `boolean` - show the error context when failing to retrieve the version information - defaults to `true`
+- missing_command_text: `string` - text to display when the command is missing - defaults to empty
+- display_mode: `string` - determines when the segment is displayed
+  - `always`: the segment is always displayed
+  - `files`: the segment is only displayed when `*.hs`, `*.lhs`, `stack.yaml`, `package.yaml`, `*.cabal`,
+or `cabal.project` files are present (default)
+- stack_ghc_mode: `string` - determines when to use stack ghc to retrieve the version information.
+Using stack ghc will decrease performance.
+  - `never`: never use stack ghc (default)
+  - `package`: only use stack ghc when `stack.yaml` is in the root of the package
+  - `always`: always use stack ghc
+
+## Template ([info][templates])
+
+:::note default template
+
+```template
+{{ if .Error }}{{ .Error }}{{ else }}{{ .Full }}{{ end }}
+```
+
+:::
+
+### Properties
+
+- `.Full`: `string` - the full version
+- `.Major`: `string` - major number
+- `.Minor`: `string` - minor number
+- `.Patch`: `string` - patch number
+- `.Prerelease`: `string` - prerelease info text
+- `.BuildMetadata`: `string` - build metadata
+- `.Error`: `string` - when fetching the version string errors
+- `.StackGhc`: `boolean` - `true` if stack ghc was used, otherwise `false`
+
+[templates]: /docs/config-templates
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 4fb09b2e..b3550d80 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -51,6 +51,7 @@ module.exports = {
         "git",
         "poshgit",
         "golang",
+        "haskell",
         "ipify",
         "java",
         "julia",
diff --git a/src/engine/segment.go b/src/engine/segment.go
index c6af7420..899210b0 100644
--- a/src/engine/segment.go
+++ b/src/engine/segment.go
@@ -147,6 +147,8 @@ const (
 	BREWFATHER SegmentType = "brewfather"
 	// IPIFY segment
 	IPIFY SegmentType = "ipify"
+	// HASKELL segment
+	HASKELL SegmentType = "haskell"
 )
 
 func (segment *Segment) shouldIncludeFolder() bool {
@@ -263,6 +265,7 @@ func (segment *Segment) mapSegmentWithWriter(env environment.Environment) error
 		WINREG:        &segments.WindowsRegistry{},
 		BREWFATHER:    &segments.Brewfather{},
 		IPIFY:         &segments.IPify{},
+		HASKELL:       &segments.Haskell{},
 	}
 	if segment.Properties == nil {
 		segment.Properties = make(properties.Map)
diff --git a/src/segments/haskell.go b/src/segments/haskell.go
new file mode 100644
index 00000000..4bba90a1
--- /dev/null
+++ b/src/segments/haskell.go
@@ -0,0 +1,59 @@
+package segments
+
+import (
+	"oh-my-posh/environment"
+	"oh-my-posh/properties"
+)
+
+type Haskell struct {
+	language
+
+	StackGhc bool
+}
+
+const (
+	StackGhcMode properties.Property = "stack_ghc_mode"
+)
+
+func (h *Haskell) Template() string {
+	return languageTemplate
+}
+
+func (h *Haskell) Init(props properties.Properties, env environment.Environment) {
+	ghcRegex := `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`
+	ghcCmd := &cmd{
+		executable: "ghc",
+		args:       []string{"--numeric-version"},
+		regex:      ghcRegex,
+	}
+
+	stackGhcCmd := &cmd{
+		executable: "stack",
+		args:       []string{"ghc", "--", "--numeric-version"},
+		regex:      ghcRegex,
+	}
+
+	h.language = language{
+		env:                env,
+		props:              props,
+		extensions:         []string{"*.hs", "*.lhs", "stack.yaml", "package.yaml", "*.cabal", "cabal.project"},
+		commands:           []*cmd{ghcCmd},
+		versionURLTemplate: "https://www.haskell.org/ghc/download_ghc_{{ .Major }}_{{ .Minor }}_{{ .Patch }}.html",
+	}
+
+	switch h.props.GetString(StackGhcMode, "never") {
+	case "always":
+		h.language.commands = []*cmd{stackGhcCmd}
+		h.StackGhc = true
+	case "package":
+		_, err := h.language.env.HasParentFilePath("stack.yaml")
+		if err == nil {
+			h.language.commands = []*cmd{stackGhcCmd}
+			h.StackGhc = true
+		}
+	}
+}
+
+func (h *Haskell) Enabled() bool {
+	return h.language.Enabled()
+}
diff --git a/themes/schema.json b/themes/schema.json
index 7612eac0..d5005d46 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -182,7 +182,8 @@
             "wifi",
             "winreg",
             "plastic",
-            "ipify"
+            "ipify",
+            "haskell"
           ]
         },
         "style": {
@@ -1872,6 +1873,39 @@
               }
             }
           }
+        },
+        {
+          "if": {
+            "properties": {
+              "type": { "const": "haskell" }
+            }
+          },
+          "then": {
+            "title": "Haskell Segment",
+            "description": "https://ohmyposh.dev/docs/haskell",
+            "properties": {
+              "properties": {
+                "properties": {
+                  "fetch_version": {
+                    "$ref": "#/definitions/fetch_version"
+                  },
+                  "stack_ghc_mode": {
+                    "type": "string",
+                    "title": "Use Stack GHC",
+                    "description": "Get the GHC version used by Stack. Will decrease performance. Boolean indicating whether stack ghc was used available in template as .StackGhc",
+                    "enum": ["always", "package", "never"],
+                    "default": "never"
+                  },
+                  "display_mode": {
+                    "$ref": "#/definitions/display_mode"
+                  },
+                  "missing_command_text": {
+                    "$ref": "#/definitions/missing_command_text"
+                  }
+                }
+              }
+            }
+          }
         }
       ]
     }


Test patch:
diff --git a/src/segments/haskell_test.go b/src/segments/haskell_test.go
new file mode 100644
index 00000000..3752059f
--- /dev/null
+++ b/src/segments/haskell_test.go
@@ -0,0 +1,97 @@
+package segments
+
+import (
+	"errors"
+	"fmt"
+	"oh-my-posh/environment"
+	"oh-my-posh/mock"
+	"oh-my-posh/properties"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestHaskell(t *testing.T) {
+	cases := []struct {
+		Case            string
+		ExpectedString  string
+		GhcVersion      string
+		StackGhcVersion string
+		StackGhcMode    string
+		InStackPackage  bool
+		StackGhc        bool
+	}{
+		{
+			Case:            "GHC 8.10.7",
+			ExpectedString:  "8.10.7",
+			GhcVersion:      "8.10.7",
+			StackGhcVersion: "9.0.2",
+			StackGhcMode:    "never",
+		},
+		{
+			Case:            "Stack GHC Mode - Always",
+			ExpectedString:  "9.0.2",
+			GhcVersion:      "8.10.7",
+			StackGhcVersion: "9.0.2",
+			StackGhcMode:    "always",
+			StackGhc:        true,
+		},
+		{
+			Case:            "Stack GHC Mode - Package",
+			ExpectedString:  "9.0.2",
+			GhcVersion:      "8.10.7",
+			StackGhcVersion: "9.0.2",
+			StackGhcMode:    "package",
+			InStackPackage:  true,
+			StackGhc:        true,
+		},
+		{
+			Case:            "Stack GHC Mode - Package no stack.yaml",
+			ExpectedString:  "8.10.7",
+			GhcVersion:      "8.10.7",
+			StackGhcVersion: "9.0.2",
+			StackGhcMode:    "package",
+		},
+	}
+
+	for _, tc := range cases {
+		env := new(mock.MockedEnvironment)
+		if tc.StackGhcMode == "always" || (tc.StackGhcMode == "package" && tc.InStackPackage) {
+			env.On("HasCommand", "stack").Return(true)
+			env.On("RunCommand", "stack", []string{"ghc", "--", "--numeric-version"}).Return(tc.StackGhcVersion, nil)
+		} else {
+			env.On("HasCommand", "ghc").Return(true)
+			env.On("RunCommand", "ghc", []string{"--numeric-version"}).Return(tc.GhcVersion, nil)
+		}
+		fileInfo := &environment.FileInfo{
+			Path:         "../stack.yaml",
+			ParentFolder: "./",
+			IsDir:        false,
+		}
+		if tc.InStackPackage {
+			var err error
+			env.On("HasParentFilePath", "stack.yaml").Return(fileInfo, err)
+		} else {
+			env.On("HasParentFilePath", "stack.yaml").Return(fileInfo, errors.New("no match"))
+		}
+		env.On("HasFiles", "*.hs").Return(true)
+		env.On("Pwd").Return("/usr/home/project")
+		env.On("Home").Return("/usr/home")
+		env.On("TemplateCache").Return(&environment.TemplateCache{
+			Env: make(map[string]string),
+		})
+
+		props := properties.Map{
+			properties.FetchVersion: true,
+		}
+		props[StackGhcMode] = tc.StackGhcMode
+
+		h := &Haskell{}
+		h.Init(props, env)
+
+		failMsg := fmt.Sprintf("Failed in case: %s", tc.Case)
+		assert.True(t, h.Enabled(), failMsg)
+		assert.Equal(t, tc.ExpectedString, renderTemplate(env, h.Template(), h), failMsg)
+		assert.Equal(t, tc.StackGhc, h.StackGhc, failMsg)
+	}
+}