From 1ceba7d603a14856aa209c9f224f582fdbc79b8f Mon Sep 17 00:00:00 2001 From: Colt <6819362+cabauman@users.noreply.github.com> Date: Sun, 26 Feb 2023 23:37:37 +0900 Subject: [PATCH] feat: add Unity segment --- src/engine/segment.go | 3 + src/segments/unity.go | 112 +++++++++++++++++++ src/segments/unity_test.go | 188 ++++++++++++++++++++++++++++++++ themes/schema.json | 21 ++++ website/docs/segments/unity.mdx | 59 ++++++++++ website/sidebars.js | 1 + 6 files changed, 384 insertions(+) create mode 100644 src/segments/unity.go create mode 100644 src/segments/unity_test.go create mode 100644 website/docs/segments/unity.mdx diff --git a/src/engine/segment.go b/src/engine/segment.go index ae1d86a8..41f306d6 100644 --- a/src/engine/segment.go +++ b/src/engine/segment.go @@ -224,6 +224,8 @@ const ( TIME SegmentType = "time" // UI5 Tooling segment UI5TOOLING SegmentType = "ui5tooling" + // UNITY writes which Unity version is currently active + UNITY SegmentType = "unity" // VALA writes the active vala version VALA SegmentType = "vala" // WAKATIME writes tracked time spend in dev editors @@ -305,6 +307,7 @@ var Segments = map[SegmentType]func() SegmentWriter{ TEXT: func() SegmentWriter { return &segments.Text{} }, TIME: func() SegmentWriter { return &segments.Time{} }, UI5TOOLING: func() SegmentWriter { return &segments.UI5Tooling{} }, + UNITY: func() SegmentWriter { return &segments.Unity{} }, VALA: func() SegmentWriter { return &segments.Vala{} }, WAKATIME: func() SegmentWriter { return &segments.Wakatime{} }, WINREG: func() SegmentWriter { return &segments.WindowsRegistry{} }, diff --git a/src/segments/unity.go b/src/segments/unity.go new file mode 100644 index 00000000..e6c95ccd --- /dev/null +++ b/src/segments/unity.go @@ -0,0 +1,112 @@ +package segments + +import ( + "errors" + "fmt" + "path/filepath" + "strings" + + "github.com/jandedobbeleer/oh-my-posh/src/platform" + "github.com/jandedobbeleer/oh-my-posh/src/properties" + "github.com/jandedobbeleer/oh-my-posh/src/regex" +) + +type Unity struct { + props properties.Properties + env platform.Environment + + UnityVersion string + CSharpVersion string +} + +func (u *Unity) Enabled() bool { + unityVersion, err := u.GetUnityVersion() + if err != nil { + u.env.Error(err) + return false + } + if len(unityVersion) == 0 { + return false + } + u.UnityVersion = unityVersion + + csharpVersion, err := u.GetCSharpVersion() + if err != nil { + u.env.Error(err) + } + u.CSharpVersion = csharpVersion + + return true +} + +func (u *Unity) GetUnityVersion() (version string, err error) { + projectDir, err := u.env.HasParentFilePath("ProjectSettings") + if err != nil { + u.env.Debug("No ProjectSettings parent folder found") + return + } + + if !u.env.HasFilesInDir(projectDir.Path, "ProjectVersion.txt") { + u.env.Debug("No ProjectVersion.txt file found") + return + } + + versionFilePath := filepath.Join(projectDir.Path, "ProjectVersion.txt") + versionFileText := u.env.FileContent(versionFilePath) + + firstLine := strings.Split(versionFileText, "\n")[0] + versionPrefix := "m_EditorVersion: " + versionPrefixIndex := strings.Index(firstLine, versionPrefix) + if versionPrefixIndex == -1 { + err := errors.New("ProjectSettings/ProjectVersion.txt is missing 'm_EditorVersion: ' prefix") + return "", err + } + + versionStartIndex := versionPrefixIndex + len(versionPrefix) + unityVersion := firstLine[versionStartIndex:] + + return strings.TrimSuffix(unityVersion, "f1"), nil +} + +func (u *Unity) GetCSharpVersion() (version string, err error) { + lastDotIndex := strings.LastIndex(u.UnityVersion, ".") + if lastDotIndex == -1 { + return "", errors.New("lastDotIndex") + } + shortUnityVersion := u.UnityVersion[0:lastDotIndex] + + if val, found := u.env.Cache().Get(shortUnityVersion); found { + csharpVersion := strings.TrimSuffix(val, ".0") + return csharpVersion, nil + } + + url := fmt.Sprintf("https://docs.unity3d.com/%s/Documentation/Manual/CSharpCompiler.html", shortUnityVersion) + httpTimeout := u.props.GetInt(properties.HTTPTimeout, 2000) + + body, err := u.env.HTTPRequest(url, nil, httpTimeout) + if err != nil { + return "", err + } + + pageContent := string(body) + + pattern := `(?P.*)` + matches := regex.FindNamedRegexMatch(pattern, pageContent) + if matches != nil && matches["csharpVersion"] != "" { + csharpVersion := strings.TrimSuffix(matches["csharpVersion"], ".0") + u.env.Cache().Set(shortUnityVersion, csharpVersion, -1) + return csharpVersion, nil + } + + u.env.Cache().Set(shortUnityVersion, "", -1) + return "", nil +} + +func (u *Unity) Template() string { + return " \ue721 {{ .UnityVersion }}{{ if .CSharpVersion }} {{ .CSharpVersion }}{{ end }} " +} + +func (u *Unity) Init(props properties.Properties, env platform.Environment) { + u.props = props + u.env = env +} diff --git a/src/segments/unity_test.go b/src/segments/unity_test.go new file mode 100644 index 00000000..4cb9a75e --- /dev/null +++ b/src/segments/unity_test.go @@ -0,0 +1,188 @@ +package segments + +import ( + "errors" + "fmt" + "path/filepath" + "testing" + + mock2 "github.com/stretchr/testify/mock" + + "github.com/jandedobbeleer/oh-my-posh/src/mock" + "github.com/jandedobbeleer/oh-my-posh/src/platform" + "github.com/jandedobbeleer/oh-my-posh/src/properties" + + "github.com/stretchr/testify/assert" +) + +type CacheGet struct { + key string + val string + found bool +} + +type CacheSet struct { + key string + val string +} + +type HTTPResponse struct { + body string + err error +} + +func TestUnitySegment(t *testing.T) { + cases := []struct { + Case string + ExpectedOutput string + VersionFileText string + CacheGet CacheGet + CacheSet CacheSet + ExpectedToBeEnabled bool + VersionFileExists bool + HTTPResponse HTTPResponse + }{ + { + Case: "C# version cached", + ExpectedOutput: "\ue721 2021.3.16 C# 9", + ExpectedToBeEnabled: true, + VersionFileExists: true, + VersionFileText: "m_EditorVersion: 2021.3.16f1\nm_EditorVersionWithRevision: 2021.3.16f1 (4016570cf34f)", + CacheGet: CacheGet{ + key: "2021.3", + val: "C# 9", + found: true, + }, + }, + { + Case: "C# version not cached", + ExpectedOutput: "\ue721 2021.3.16 C# 9", + ExpectedToBeEnabled: true, + VersionFileExists: true, + VersionFileText: "m_EditorVersion: 2021.3.16f1\nm_EditorVersionWithRevision: 2021.3.16f1 (4016570cf34f)", + CacheGet: CacheGet{ + key: "2021.3", + val: "", + found: false, + }, + CacheSet: CacheSet{ + key: "2021.3", + val: "C# 9", + }, + HTTPResponse: HTTPResponse{ + body: `C# 9.0`, + err: nil, + }, + }, + { + Case: "C# version has a minor version", + ExpectedOutput: "\ue721 2021.3.16 C# 7.3", + ExpectedToBeEnabled: true, + VersionFileExists: true, + VersionFileText: "m_EditorVersion: 2021.3.16f1\nm_EditorVersionWithRevision: 2021.3.16f1 (4016570cf34f)", + CacheGet: CacheGet{ + key: "2021.3", + val: "", + found: false, + }, + CacheSet: CacheSet{ + key: "2021.3", + val: "C# 7.3", + }, + HTTPResponse: HTTPResponse{ + body: `C# 7.3`, + err: nil, + }, + }, + { + Case: "C# version not found in webpage", + ExpectedOutput: "\ue721 2021.3.16", + ExpectedToBeEnabled: true, + VersionFileExists: true, + VersionFileText: "m_EditorVersion: 2021.3.16f1\nm_EditorVersionWithRevision: 2021.3.16f1 (4016570cf34f)", + CacheGet: CacheGet{ + key: "2021.3", + val: "", + found: false, + }, + CacheSet: CacheSet{ + key: "2021.3", + val: "", + }, + HTTPResponse: HTTPResponse{ + body: `

Sorry... that page seems to be missing!

`, + err: nil, + }, + }, + { + Case: "http request fails", + ExpectedOutput: "\ue721 2021.3.16", + ExpectedToBeEnabled: true, + VersionFileExists: true, + VersionFileText: "m_EditorVersion: 2021.3.16f1\nm_EditorVersionWithRevision: 2021.3.16f1 (4016570cf34f)", + CacheGet: CacheGet{ + key: "2021.3", + val: "", + found: false, + }, + HTTPResponse: HTTPResponse{ + body: "", + err: errors.New("FAIL"), + }, + }, + { + Case: "ProjectSettings/ProjectVersion.txt doesn't exist", + ExpectedToBeEnabled: false, + VersionFileExists: false, + }, + { + Case: "ProjectSettings/ProjectVersion.txt is empty", + ExpectedToBeEnabled: false, + VersionFileExists: true, + VersionFileText: "", + }, + { + Case: "ProjectSettings/ProjectVersion.txt does not have expected format", + ExpectedToBeEnabled: false, + VersionFileExists: true, + VersionFileText: "2021.3.16f1", + }, + } + + for _, tc := range cases { + env := new(mock.MockedEnvironment) + env.On("Error", mock2.Anything).Return() + env.On("Debug", mock2.Anything) + + err := errors.New("no match at root level") + var projectDir *platform.FileInfo + if tc.VersionFileExists { + err = nil + projectDir = &platform.FileInfo{ + ParentFolder: "UnityProjectRoot", + Path: "UnityProjectRoot/ProjectSettings", + IsDir: true, + } + env.On("HasFilesInDir", projectDir.Path, "ProjectVersion.txt").Return(tc.VersionFileExists) + versionFilePath := filepath.Join(projectDir.Path, "ProjectVersion.txt") + env.On("FileContent", versionFilePath).Return(tc.VersionFileText) + } + env.On("HasParentFilePath", "ProjectSettings").Return(projectDir, err) + + cache := &mock.MockedCache{} + cache.On("Get", tc.CacheGet.key).Return(tc.CacheGet.val, tc.CacheGet.found) + cache.On("Set", tc.CacheSet.key, tc.CacheSet.val, -1).Return() + env.On("Cache").Return(cache) + + url := fmt.Sprintf("https://docs.unity3d.com/%s/Documentation/Manual/CSharpCompiler.html", tc.CacheGet.key) + env.On("HTTPRequest", url).Return([]byte(tc.HTTPResponse.body), tc.HTTPResponse.err) + + props := properties.Map{} + unity := &Unity{} + unity.Init(props, env) + assert.Equal(t, tc.ExpectedToBeEnabled, unity.Enabled()) + if tc.ExpectedToBeEnabled { + assert.Equal(t, tc.ExpectedOutput, renderTemplate(env, unity.Template(), unity), tc.Case) + } + } +} diff --git a/themes/schema.json b/themes/schema.json index a66a0b4e..0066695b 100644 --- a/themes/schema.json +++ b/themes/schema.json @@ -301,6 +301,7 @@ "text", "terraform", "ui5tooling", + "unity", "wakatime", "winreg", "withings", @@ -2752,6 +2753,26 @@ } } }, + { + "if": { + "properties": { + "type": { "const": "unity" } + } + }, + "then": { + "title": "Unity Segment", + "description": "https://ohmyposh.dev/docs/unity", + "properties": { + "properties": { + "properties": { + "http_timeout": { + "$ref": "#/definitions/http_timeout" + } + } + } + } + } + }, { "if": { "properties": { diff --git a/website/docs/segments/unity.mdx b/website/docs/segments/unity.mdx new file mode 100644 index 00000000..bc60f274 --- /dev/null +++ b/website/docs/segments/unity.mdx @@ -0,0 +1,59 @@ +--- +id: unity +title: Unity +sidebar_label: Unity +--- + +## What + +Display the currently active [Unity][unity] and C# versions. + +The Unity version is displayed regardless of whether or not the C# version fetching succeeds. + +The C# version is fetched from [the docs][unity-csharp-page], but the page only exists as far back as Unity 2018.3. +Earlier versions will not display a C# version. + +A web request only occurs the first time a given `major.minor` Unity version is encountered. +Subsequent invocations return the cached C# version. + +## Sample Configuration + +```json +{ + "type": "unity", + "style": "powerline", + "powerline_symbol": "\ue0b0", + "foreground": "#111111", + "background": "#ffffff", + "properties": { + "http_timeout": 2000 + } +} +``` + +## Properties + +| Name | Type | Description | +| --------------- | -------- | ----------------------------------------------- | +| `http_timeout` | `int` | The default timeout for http request is 2000ms. | + +## Template ([info][templates]) + +:::note default template + +```template +\ue721 {{ .UnityVersion }}{{ if .CSharpVersion }} {{ .CSharpVersion }}{{ end }} +``` + +::: + +### Properties + +| Name | Type | Description | +| ---------------- | -------- | ---------------------------------------- | +| `.UnityVersion` | `string` | the Unity version | +| `.CSharpVersion` | `string` | the C# version | + +[unity]: https://unity.com/ +[templates]: /docs/configuration/templates +[unity-csharp-page]: https://docs.unity3d.com/Manual/CSharpCompiler.html diff --git a/website/sidebars.js b/website/sidebars.js index d8bc0f89..499ab8d6 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -115,6 +115,7 @@ module.exports = { "segments/text", "segments/time", "segments/ui5tooling", + "segments/unity", "segments/vala", "segments/wakatime", "segments/withings",