Repository: JanDeDobbeleer/oh-my-posh
Pull request #2540 (2022-07-19 04:58:36)

feat: add lua segment and add properties to language
### 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

Adds a Lua segment which fetches the current [Lua][lua] or [LuaJIT][luajit] version.
Exposes the executable used to retrieve the language version (I will update the other docs if this gets approved)
Adds the ability to define a different `versionURLTemplate` for each command. These will take priority (if present) over the outer `versionURLTemplate` but can still be overwritten by the `version_url_template` property in the theme.

![image](https://user-images.githubusercontent.com/85419773/179667238-4ce90207-62ae-45ac-af32-3b5a5e2eefcf.png)
![image](https://user-images.githubusercontent.com/85419773/179667306-e4a4c635-0459-4e23-ba8d-7cf1eb75c559.png)

Regards,
Jed

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary
[lua]: https://www.lua.org/
[luajit]: https://luajit.org/



Patch:
diff --git a/src/engine/segment.go b/src/engine/segment.go
index 5e3599c3..6ceebd0b 100644
--- a/src/engine/segment.go
+++ b/src/engine/segment.go
@@ -126,6 +126,8 @@ const (
 	KOTLIN SegmentType = "kotlin"
 	// KUBECTL writes the Kubernetes context we're currently in
 	KUBECTL SegmentType = "kubectl"
+	// LUA writes the active lua version
+	LUA SegmentType = "lua"
 	// NBGV writes the nbgv version information
 	NBGV SegmentType = "nbgv"
 	// NIGHTSCOUT is an open source diabetes system
@@ -288,6 +290,7 @@ func (segment *Segment) mapSegmentWithWriter(env environment.Environment) error
 		JULIA:         &segments.Julia{},
 		KOTLIN:        &segments.Kotlin{},
 		KUBECTL:       &segments.Kubectl{},
+		LUA:           &segments.Lua{},
 		NBGV:          &segments.Nbgv{},
 		NIGHTSCOUT:    &segments.Nightscout{},
 		NODE:          &segments.Node{},
diff --git a/src/segments/language.go b/src/segments/language.go
index 6df2c5d2..05e2e013 100644
--- a/src/segments/language.go
+++ b/src/segments/language.go
@@ -28,13 +28,15 @@ type version struct {
 	Prerelease    string
 	BuildMetadata string
 	URL           string
+	Executable    string
 }
 
 type cmd struct {
-	executable string
-	args       []string
-	regex      string
-	getVersion getVersion
+	executable         string
+	args               []string
+	regex              string
+	getVersion         getVersion
+	versionURLTemplate string
 }
 
 func (c *cmd) parse(versionInfo string) (*version, error) {
@@ -180,7 +182,11 @@ func (l *language) setVersion() error {
 			return fmt.Errorf("err parsing info from %s with %s", command.executable, versionStr)
 		}
 		l.version = *version
+		if command.versionURLTemplate != "" {
+			l.versionURLTemplate = command.versionURLTemplate
+		}
 		l.buildVersionURL()
+		l.version.Executable = command.executable
 		return nil
 	}
 	return errors.New(l.props.GetString(MissingCommandText, ""))
diff --git a/src/segments/lua.go b/src/segments/lua.go
new file mode 100644
index 00000000..10a39bb2
--- /dev/null
+++ b/src/segments/lua.go
@@ -0,0 +1,48 @@
+package segments
+
+import (
+	"oh-my-posh/environment"
+	"oh-my-posh/properties"
+)
+
+type Lua struct {
+	language
+}
+
+const (
+	PreferredExecutable properties.Property = "preferred_executable"
+)
+
+func (l *Lua) Template() string {
+	return languageTemplate
+}
+
+func (l *Lua) Init(props properties.Properties, env environment.Environment) {
+	l.language = language{
+		env:        env,
+		props:      props,
+		extensions: []string{"*.lua", "*.rockspec"},
+		folders:    []string{"lua"},
+		commands: []*cmd{
+			{
+				executable:         "lua",
+				args:               []string{"-v"},
+				regex:              `Lua (?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+)(.(?P<patch>[0-9]+))?))`,
+				versionURLTemplate: "https://www.lua.org/manual/{{ .Major }}.{{ .Minor }}/readme.html#changes",
+			},
+			{
+				executable:         "luajit",
+				args:               []string{"-v"},
+				regex:              `LuaJIT (?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+)(.(?P<patch>[0-9]+))?))`,
+				versionURLTemplate: "https://github.com/LuaJIT/LuaJIT/tree/v{{ .Major}}.{{ .Minor}}",
+			},
+		},
+	}
+	if l.props.GetString(PreferredExecutable, "lua") == "luajit" {
+		l.commands = []*cmd{l.commands[1], l.commands[0]}
+	}
+}
+
+func (l *Lua) Enabled() bool {
+	return l.language.Enabled()
+}
diff --git a/themes/schema.json b/themes/schema.json
index d80963cd..7a81d6d2 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -243,8 +243,9 @@
             "iterm",
             "julia",
             "java",
-            "kubectl",
             "kotlin",
+            "kubectl",
+            "lua",
             "node",
             "npm",
             "nx",
@@ -2579,6 +2580,47 @@
             }
           }
         },
+        {
+          "if": {
+            "properties": {
+              "type": {
+                "const": "lua"
+              }
+            }
+          },
+          "then": {
+            "title": "Lua Segment",
+            "description": "https://ohmyposh.dev/docs/segments/lua",
+            "properties": {
+              "properties": {
+                "properties": {
+                  "home_enabled": {
+                    "$ref": "#/definitions/home_enabled"
+                  },
+                  "fetch_version": {
+                    "$ref": "#/definitions/fetch_version"
+                  },
+                  "display_mode": {
+                    "$ref": "#/definitions/display_mode"
+                  },
+                  "missing_command_text": {
+                    "$ref": "#/definitions/missing_command_text"
+                  },
+                  "version_url_template": {
+                    "$ref": "#/definitions/version_url_template"
+                  },
+                  "preferred_executable": {
+                    "type": "string",
+                    "title": "Preferred Executable",
+                    "description": "The preferred executable to use when fetching the version.",
+                    "enum": ["lua", "luajit"],
+                    "default": "lua"
+                  }
+                }
+              }
+            }
+          }
+        },
         {
           "if": {
             "properties": {
diff --git a/website/docs/segments/lua.mdx b/website/docs/segments/lua.mdx
new file mode 100644
index 00000000..b511651f
--- /dev/null
+++ b/website/docs/segments/lua.mdx
@@ -0,0 +1,61 @@
+---
+id: lua
+title: Lua
+sidebar_label: Lua
+---
+
+## What
+
+Display the currently active [Lua][lua] or [LuaJIT][luajit] version.
+
+## Sample Configuration
+
+```json
+{
+  "type": "lua",
+  "style": "powerline",
+  "powerline_symbol": "\ue0b0",
+  "foreground": "white",
+  "background": "blue",
+  "template": " \ue620 {{ .Full }} "
+}
+```
+
+## Properties
+
+- home_enabled: `boolean` - display the segment in the HOME folder or not - defaults to `false`
+- fetch_version: `boolean` - display the lua version - 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 `*.lua`, `*.rockspec` files or the `lua` folder are present (default)
+- version_url_template: `string` - a go [text/template][go-text-template] [template][templates] that creates
+the URL of the version info / release notes
+- preferred_executable: `string` - the preferred executable to use when fetching the version
+  - `lua`: the Lua executable (default)
+  - `luajit`: the LuaJIT executable
+
+## 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
+- `.URL`: `string` - URL of the version info / release notes
+- `.Error`: `string` - error encountered when fetching the version string
+- `.Executable`: `string` - the executable used to fetch the version
+
+[go-text-template]: https://golang.org/pkg/text/template/
+[templates]: /docs/configuration/templates
+[lua]: https://www.lua.org/
+[luajit]: https://luajit.org/
diff --git a/website/sidebars.js b/website/sidebars.js
index 642c3b9d..1e5397a7 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -79,6 +79,7 @@ module.exports = {
         "segments/julia",
         "segments/kotlin",
         "segments/kubectl",
+        "segments/lua",
         "segments/nbgv",
         "segments/nightscout",
         "segments/npm",


Test patch:
diff --git a/src/segments/language_test.go b/src/segments/language_test.go
index 253b6fcf..b791d1a0 100644
--- a/src/segments/language_test.go
+++ b/src/segments/language_test.go
@@ -153,6 +153,7 @@ func TestLanguageEnabledOneExtensionFound(t *testing.T) {
 	lang := bootStrapLanguageTest(args)
 	assert.True(t, lang.Enabled())
 	assert.Equal(t, universion, lang.Full, "unicorn is available and uni files are found")
+	assert.Equal(t, "unicorn", lang.Executable, "unicorn was used")
 }
 
 func TestLanguageDisabledInHome(t *testing.T) {
@@ -191,6 +192,7 @@ func TestLanguageEnabledSecondExtensionFound(t *testing.T) {
 	lang := bootStrapLanguageTest(args)
 	assert.True(t, lang.Enabled())
 	assert.Equal(t, universion, lang.Full, "unicorn is available and corn files are found")
+	assert.Equal(t, "unicorn", lang.Executable, "unicorn was used")
 }
 
 func TestLanguageEnabledSecondCommand(t *testing.T) {
@@ -215,6 +217,7 @@ func TestLanguageEnabledSecondCommand(t *testing.T) {
 	lang := bootStrapLanguageTest(args)
 	assert.True(t, lang.Enabled())
 	assert.Equal(t, universion, lang.Full, "unicorn is available and corn files are found")
+	assert.Equal(t, "corn", lang.Executable, "corn was used")
 }
 
 func TestLanguageEnabledAllExtensionsFound(t *testing.T) {
@@ -234,6 +237,7 @@ func TestLanguageEnabledAllExtensionsFound(t *testing.T) {
 	lang := bootStrapLanguageTest(args)
 	assert.True(t, lang.Enabled())
 	assert.Equal(t, universion, lang.Full, "unicorn is available and uni and corn files are found")
+	assert.Equal(t, "unicorn", lang.Executable, "unicorn was used")
 }
 
 func TestLanguageEnabledNoVersion(t *testing.T) {
@@ -257,6 +261,7 @@ func TestLanguageEnabledNoVersion(t *testing.T) {
 	lang := bootStrapLanguageTest(args)
 	assert.True(t, lang.Enabled())
 	assert.Equal(t, "", lang.Full, "unicorn is available and uni and corn files are found")
+	assert.Equal(t, "", lang.Executable, "no version was found")
 }
 
 func TestLanguageEnabledMissingCommand(t *testing.T) {
@@ -274,6 +279,7 @@ func TestLanguageEnabledMissingCommand(t *testing.T) {
 	lang := bootStrapLanguageTest(args)
 	assert.True(t, lang.Enabled())
 	assert.Equal(t, "", lang.Full, "unicorn is unavailable and uni and corn files are found")
+	assert.Equal(t, "", lang.Executable, "no executable was found")
 }
 
 func TestLanguageEnabledNoVersionData(t *testing.T) {
@@ -297,6 +303,7 @@ func TestLanguageEnabledNoVersionData(t *testing.T) {
 	lang := bootStrapLanguageTest(args)
 	assert.True(t, lang.Enabled())
 	assert.Equal(t, "", lang.Full)
+	assert.Equal(t, "", lang.Executable, "no version was found")
 }
 
 func TestLanguageEnabledMissingCommandCustomText(t *testing.T) {
@@ -438,3 +445,54 @@ func TestLanguageEnabledInHome(t *testing.T) {
 		assert.Equal(t, tc.ExpectedEnabled, lang.Enabled(), tc.Case)
 	}
 }
+
+func TestLanguageInnerHyperlink(t *testing.T) {
+	args := &languageArgs{
+		commands: []*cmd{
+			{
+				executable:         "uni",
+				args:               []string{"--version"},
+				regex:              `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`,
+				versionURLTemplate: "https://uni.org/release/{{ .Full }}",
+			},
+			{
+				executable:         "corn",
+				args:               []string{"--version"},
+				regex:              `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`,
+				versionURLTemplate: "https://unicor.org/doc/{{ .Full }}",
+			},
+		},
+		versionURLTemplate: "This gets replaced with inner template",
+		extensions:         []string{uni, corn},
+		enabledExtensions:  []string{corn},
+		enabledCommands:    []string{"corn"},
+		version:            universion,
+		properties:         properties.Map{},
+	}
+	lang := bootStrapLanguageTest(args)
+	assert.True(t, lang.Enabled())
+	assert.Equal(t, "https://unicor.org/doc/1.3.307", lang.version.URL)
+}
+
+func TestLanguageHyperlinkTemplatePropertyTakesPriority(t *testing.T) {
+	args := &languageArgs{
+		commands: []*cmd{
+			{
+				executable:         "uni",
+				args:               []string{"--version"},
+				regex:              `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`,
+				versionURLTemplate: "https://uni.org/release/{{ .Full }}",
+			},
+		},
+		extensions:        []string{uni},
+		enabledExtensions: []string{uni},
+		enabledCommands:   []string{"uni"},
+		version:           universion,
+		properties: properties.Map{
+			properties.VersionURLTemplate: "https://custom/url/template/{{ .Major }}.{{ .Minor }}",
+		},
+	}
+	lang := bootStrapLanguageTest(args)
+	assert.True(t, lang.Enabled())
+	assert.Equal(t, "https://custom/url/template/1.3", lang.version.URL)
+}
diff --git a/src/segments/lua_test.go b/src/segments/lua_test.go
new file mode 100644
index 00000000..d7f150d5
--- /dev/null
+++ b/src/segments/lua_test.go
@@ -0,0 +1,83 @@
+package segments
+
+import (
+	"fmt"
+	"oh-my-posh/environment"
+	"oh-my-posh/mock"
+	"oh-my-posh/properties"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLua(t *testing.T) {
+	cases := []struct {
+		Case           string
+		ExpectedString string
+		Version        string
+		HasLua         bool
+		HasLuaJit      bool
+		Prefer         string
+		ExpectedURL    string
+	}{
+		{
+			Case:           "Lua 5.4.4 - Prefer Lua",
+			ExpectedString: "5.4.4",
+			Version:        "Lua 5.4.4  Copyright (C) 1994-2022 Lua.org, PUC-Rio",
+			ExpectedURL:    "https://www.lua.org/manual/5.4/readme.html#changes",
+			HasLua:         true,
+			HasLuaJit:      true,
+			Prefer:         "lua",
+		},
+		{
+			Case:           "Lua 5.0 - Prefer luajit but missing so fallback to lua",
+			ExpectedString: "5.0",
+			Version:        "Lua 5.0  Copyright (C) 1994-2003 Tecgraf, PUC-Rio",
+			ExpectedURL:    "https://www.lua.org/manual/5.0/readme.html#changes",
+			HasLua:         true,
+			Prefer:         "luajit",
+		},
+		{
+			Case:           "LuaJIT 2.0.5 - Prefer LuaJIT",
+			ExpectedString: "2.0.5",
+			Version:        "LuaJIT 2.0.5 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/",
+			HasLuaJit:      true,
+			HasLua:         true,
+			ExpectedURL:    "https://github.com/LuaJIT/LuaJIT/tree/v2.0",
+			Prefer:         "luajit",
+		},
+		{
+			Case:           "LuaJIT 2.1.0-beta3 - Prefer Lua but missing so try luajit",
+			ExpectedString: "2.1.0",
+			Version:        "LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/",
+			HasLuaJit:      true,
+			ExpectedURL:    "https://github.com/LuaJIT/LuaJIT/tree/v2.1",
+			Prefer:         "lua",
+		},
+	}
+	for _, tc := range cases {
+		env := new(mock.MockedEnvironment)
+		env.On("HasCommand", "lua").Return(tc.HasLua)
+		env.On("RunCommand", "lua", []string{"-v"}).Return(tc.Version, nil)
+		env.On("HasCommand", "luajit").Return(tc.HasLuaJit)
+		env.On("RunCommand", "luajit", []string{"-v"}).Return(tc.Version, nil)
+		env.On("HasFiles", "*.lua").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[PreferredExecutable] = tc.Prefer
+		l := &Lua{}
+		l.Init(props, env)
+		failMsg := fmt.Sprintf("Failed in case: %s", tc.Case)
+		assert.True(t, l.Enabled(), failMsg)
+		assert.Equal(t, tc.ExpectedString, renderTemplate(env, l.Template(), l), failMsg)
+		assert.Equal(t, tc.ExpectedURL, l.version.URL, failMsg)
+		assert.Equal(t, strings.ToLower(strings.Split(tc.Case, " ")[0]), l.version.Executable, failMsg)
+	}
+}