From b37e6964df439f0f4428f19e09c61caaa98a714f Mon Sep 17 00:00:00 2001 From: ggottemo Date: Mon, 9 Dec 2024 19:27:06 -0500 Subject: [PATCH 1/2] feat(ZVM): Add ZVM segment and docs --- src/config/segment_types.go | 3 ++ src/segments/zvm.go | 74 ++++++++++++++++++++++++++++ src/segments/zvm_test.go | 80 +++++++++++++++++++++++++++++++ themes/schema.json | 26 +++++++++- website/docs/segments/cli/zvm.mdx | 35 ++++++++++++++ website/sidebars.js | 1 + 6 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 src/segments/zvm.go create mode 100644 src/segments/zvm_test.go create mode 100644 website/docs/segments/cli/zvm.mdx diff --git a/src/config/segment_types.go b/src/config/segment_types.go index 63df09bdc91c..e73ea08e7409 100644 --- a/src/config/segment_types.go +++ b/src/config/segment_types.go @@ -227,6 +227,8 @@ const ( YTM SegmentType = "ytm" // ZIG writes the active zig version ZIG SegmentType = "zig" + // ZVM writes the active zig version used in the zvm environment + ZVM SegmentType = "zvm" ) // Segments contains all available prompt segment writers. @@ -331,6 +333,7 @@ var Segments = map[SegmentType]func() SegmentWriter{ YARN: func() SegmentWriter { return &segments.Yarn{} }, YTM: func() SegmentWriter { return &segments.Ytm{} }, ZIG: func() SegmentWriter { return &segments.Zig{} }, + ZVM: func() SegmentWriter { return &segments.Zvm{} }, } func (segment *Segment) MapSegmentWithWriter(env runtime.Environment) error { diff --git a/src/segments/zvm.go b/src/segments/zvm.go new file mode 100644 index 000000000000..eb6af1040659 --- /dev/null +++ b/src/segments/zvm.go @@ -0,0 +1,74 @@ +package segments + +import ( + "strings" + + "github.com/jandedobbeleer/oh-my-posh/src/properties" + "github.com/jandedobbeleer/oh-my-posh/src/runtime" +) + +type Zvm struct { + props properties.Properties + env runtime.Environment + + Version string + ZigIcon string +} + +func (z *Zvm) SetText(text string) { + z.Version = text +} + +func (z *Zvm) Text() string { + return z.Version +} + +const ( + ZvmIcon properties.Property = "zigicon" +) + +func (z *Zvm) Enabled() bool { + if !z.env.HasCommand("zvm") { + return false + } + version := z.getZvmVersion() + return version != "" +} + +func (z *Zvm) Template() string { + return " {{ if .ZigIcon }}{{ .ZigIcon }} {{ end }}{{ .Version }} " +} + +func (z *Zvm) Init(props properties.Properties, env runtime.Environment) { + z.props = props + z.env = env + + // Initialize the icon from properties + z.ZigIcon = z.props.GetString(ZvmIcon, "ZVM") + z.Version = z.getZvmVersion() +} + +func (z *Zvm) getZvmVersion() string { + output, err := z.env.RunCommand("zvm", "list") + if err != nil { + return "" + } + + // Split output into lines + lines := strings.Split(output, "\n") + + // Look for line containing green color code which indicates active version + // ANSI color code for green is typically \033[32m or \x1b[32m + for _, line := range lines { + if strings.Contains(line, "\x1b[32m") || strings.Contains(line, "\033[32m") { + // Clean ANSI color codes and whitespace + cleaned := strings.ReplaceAll(line, "\x1b[32m", "") + cleaned = strings.ReplaceAll(cleaned, "\033[32m", "") + cleaned = strings.ReplaceAll(cleaned, "\x1b[0m", "") + cleaned = strings.TrimSpace(cleaned) + + return cleaned + } + } + return "" +} diff --git a/src/segments/zvm_test.go b/src/segments/zvm_test.go new file mode 100644 index 000000000000..d9ec2a14c815 --- /dev/null +++ b/src/segments/zvm_test.go @@ -0,0 +1,80 @@ +package segments + +import ( + "testing" + + "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" + + "github.com/jandedobbeleer/oh-my-posh/src/properties" + "github.com/stretchr/testify/assert" +) + +func TestZvm(t *testing.T) { + cases := []struct { + Case string + ExpectedString string + HasCommand bool + ListOutput string + Properties properties.Map + Template string + }{ + { + Case: "no zvm command", + ExpectedString: "", + HasCommand: false, + ListOutput: "", + Properties: properties.Map{}, + Template: " {{ if .ZigIcon }}{{ .ZigIcon }} {{ end }}{{ .Version }} ", + }, + { + Case: "version 0.13.0 active", + ExpectedString: "0.13.0", + HasCommand: true, + ListOutput: "0.11.0\n\x1b[32m0.13.0\x1b[0m\n0.12.0", + Properties: properties.Map{}, + Template: " {{ if .ZigIcon }}{{ .ZigIcon }} {{ end }}{{ .Version }} ", + }, + { + Case: "version 0.13.0 active with icon", + ExpectedString: "0.13.0", + HasCommand: true, + ListOutput: "0.11.0\n\x1b[32m0.13.0\x1b[0m\n0.12.0", + Properties: properties.Map{ + ZvmIcon: "⚡", + }, + Template: " {{ if .ZigIcon }}{{ .ZigIcon }} {{ end }}{{ .Version }} ", + }, + { + Case: "version 0.12.0-dev active", + ExpectedString: "0.12.0-dev.1234+abcdef", + HasCommand: true, + ListOutput: "0.11.0\n\x1b[32m0.12.0-dev.1234+abcdef\x1b[0m\n0.12.0", + Properties: properties.Map{}, + // Change all test cases to expect the actual template + Template: " {{ if .ZigIcon }}{{ .ZigIcon }} {{ end }}{{ .Version }} ", + }, + } + + for _, tc := range cases { + t.Run(tc.Case, func(t *testing.T) { + env := new(mock.Environment) + env.On("HasCommand", "zvm").Return(tc.HasCommand) + env.On("RunCommand", "zvm", []string{"list"}).Return(tc.ListOutput, nil) + + zvm := &Zvm{} + zvm.Init(tc.Properties, env) + + assert.Equal(t, tc.Template, zvm.Template()) + + if tc.HasCommand { + assert.True(t, zvm.Enabled()) + assert.Equal(t, tc.ExpectedString, zvm.Version) + if icon, ok := tc.Properties[ZvmIcon]; ok { + assert.Equal(t, icon, zvm.ZigIcon) + } + } else { + assert.False(t, zvm.Enabled()) + } + }) + } +} diff --git a/themes/schema.json b/themes/schema.json index 1318b394b056..856de43f96d0 100644 --- a/themes/schema.json +++ b/themes/schema.json @@ -405,7 +405,8 @@ "xmake", "yarn", "ytm", - "zig" + "zig", + "zvm" ] }, "style": { @@ -4916,6 +4917,29 @@ } } }, + { + "if": { + "properties": { + "type": { "const": "zvm" } + } + }, + "then": { + "title": "ZVM Segment", + "description": "https://ohmyposh.dev/docs/zvm", + "properties": { + "properties": { + "properties": { + "zigicon": { + "type": "string", + "title": "Zig Icon", + "description": "icon to display before the version", + "default": "ZVM - " + } + } + } + } + } + }, { "if": { "properties": { diff --git a/website/docs/segments/cli/zvm.mdx b/website/docs/segments/cli/zvm.mdx new file mode 100644 index 000000000000..5a0219d4f4ab --- /dev/null +++ b/website/docs/segments/cli/zvm.mdx @@ -0,0 +1,35 @@ +--- +id: zvm +title: ZVM +sidebar_label: ZVM +--- + +## What + +Display the current Zig version being used by zvm (Zig Version Manager). + +## Sample Configuration + + +import Config from '@site/src/components/Config.js'; + + + + +## Properties + +| Name | Type | Description | +| --------- | -------- | -------------------------------------- | +| `zigicon` | `string` | The icon to display before the version | + + +[ZVM](https://github.com/tristanisham/zvm) diff --git a/website/sidebars.js b/website/sidebars.js index d3326e30ea8b..ef4893947d2e 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -91,6 +91,7 @@ module.exports = { "segments/cli/unity", "segments/cli/xmake", "segments/cli/yarn", + "segments/cli/zvm", ] }, { From 581c06220b33b570b5ead815d80f10808f77f882 Mon Sep 17 00:00:00 2001 From: ggottemo Date: Sat, 14 Dec 2024 12:05:16 -0500 Subject: [PATCH 2/2] fix(ZVM): Inherit from language and fix color output Changed behavior to turn off colors for zvm when finding the version --- src/segments/zvm.go | 131 +++++++++++++++++++++++++++++---------- src/segments/zvm_test.go | 116 +++++++++++++++++++++++++++------- themes/schema.json | 2 +- 3 files changed, 195 insertions(+), 54 deletions(-) diff --git a/src/segments/zvm.go b/src/segments/zvm.go index eb6af1040659..ec2dedf20405 100644 --- a/src/segments/zvm.go +++ b/src/segments/zvm.go @@ -7,68 +7,135 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/runtime" ) +const ( + // DefaultZigIcon is the default icon used if none is specified + DefaultZigIcon = "ZVM" + + // PropertyZigIcon is the property key for the zig icon + PropertyZigIcon properties.Property = "zigicon" +) + +// Zvm represents a Zig Version Manager segment type Zvm struct { - props properties.Properties - env runtime.Environment + language + Version string // Public for template access + ZigIcon string // Public for template access + colorCmd *colorCommand +} + +type colorCommand struct { + env runtime.Environment +} + +// colorState represents the ZVM color configuration state +type colorState struct { + enabled bool + valid bool +} + +func (c *colorCommand) detectColorState() colorState { + output, err := c.env.RunCommand("zvm", "--color") + if err != nil { + return colorState{valid: false} + } + + output = strings.ToLower(strings.TrimSpace(output)) + switch output { + case "on", "yes", "y", "enabled", "true": + return colorState{enabled: true, valid: true} + case "off", "no", "n", "disabled", "false": + return colorState{enabled: false, valid: true} + default: + return colorState{valid: false} + } +} - Version string - ZigIcon string +func (c *colorCommand) setColor(enabled bool) error { + value := "false" + if enabled { + value = "true" + } + _, err := c.env.RunCommand("zvm", "--color", value) + return err } +// SetText sets the version text func (z *Zvm) SetText(text string) { z.Version = text } +// Text returns the current version func (z *Zvm) Text() string { return z.Version } -const ( - ZvmIcon properties.Property = "zigicon" -) - -func (z *Zvm) Enabled() bool { - if !z.env.HasCommand("zvm") { - return false - } - version := z.getZvmVersion() - return version != "" -} - +// Template returns the template string for the segment func (z *Zvm) Template() string { return " {{ if .ZigIcon }}{{ .ZigIcon }} {{ end }}{{ .Version }} " } +// Init initializes the segment with the given properties and environment func (z *Zvm) Init(props properties.Properties, env runtime.Environment) { z.props = props z.env = env + z.colorCmd = &colorCommand{env: env} - // Initialize the icon from properties - z.ZigIcon = z.props.GetString(ZvmIcon, "ZVM") - z.Version = z.getZvmVersion() + z.ZigIcon = z.props.GetString(PropertyZigIcon, DefaultZigIcon) + + // Only try to get version if zvm command exists + if z.env.HasCommand("zvm") { + z.Version = z.getZvmVersion() + } +} + +// Enabled returns true if the segment should be enabled +func (z *Zvm) Enabled() bool { + if !z.env.HasCommand("zvm") { + return false + } + return z.Version != "" } +// getZvmVersion returns the current active Zvm version func (z *Zvm) getZvmVersion() string { + // Detect current color state + originalState := z.colorCmd.detectColorState() + + // If we couldn't detect the state, proceed with color disabled + if !originalState.valid { + if err := z.colorCmd.setColor(false); err != nil { + return "" + } + defer func() { + _ = z.colorCmd.setColor(true) // Best effort to restore color + }() + } else if originalState.enabled { + // Temporarily disable colors if they were enabled + if err := z.colorCmd.setColor(false); err != nil { + return "" + } + defer func() { + _ = z.colorCmd.setColor(originalState.enabled) // Restore original state + }() + } + + // Get version list output, err := z.env.RunCommand("zvm", "list") if err != nil { return "" } - // Split output into lines - lines := strings.Split(output, "\n") - - // Look for line containing green color code which indicates active version - // ANSI color code for green is typically \033[32m or \x1b[32m - for _, line := range lines { - if strings.Contains(line, "\x1b[32m") || strings.Contains(line, "\033[32m") { - // Clean ANSI color codes and whitespace - cleaned := strings.ReplaceAll(line, "\x1b[32m", "") - cleaned = strings.ReplaceAll(cleaned, "\033[32m", "") - cleaned = strings.ReplaceAll(cleaned, "\x1b[0m", "") - cleaned = strings.TrimSpace(cleaned) + return parseActiveVersion(output) +} - return cleaned +// parseActiveVersion extracts the active version from zvm list output +func parseActiveVersion(output string) string { + words := strings.Fields(output) + for _, word := range words { + if !strings.Contains(word, "[x]") { + continue } + return strings.TrimSpace(strings.ReplaceAll(word, "[x]", "")) } return "" } diff --git a/src/segments/zvm_test.go b/src/segments/zvm_test.go index d9ec2a14c815..f23cec7783ca 100644 --- a/src/segments/zvm_test.go +++ b/src/segments/zvm_test.go @@ -3,9 +3,8 @@ package segments import ( "testing" - "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" - "github.com/jandedobbeleer/oh-my-posh/src/properties" + "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/stretchr/testify/assert" ) @@ -14,52 +13,77 @@ func TestZvm(t *testing.T) { Case string ExpectedString string HasCommand bool + ColorState string ListOutput string Properties properties.Map Template string + ExpectedIcon string }{ { Case: "no zvm command", ExpectedString: "", HasCommand: false, + ColorState: "", ListOutput: "", Properties: properties.Map{}, Template: " {{ if .ZigIcon }}{{ .ZigIcon }} {{ end }}{{ .Version }} ", + ExpectedIcon: DefaultZigIcon, }, { - Case: "version 0.13.0 active", + Case: "version with colors enabled", ExpectedString: "0.13.0", HasCommand: true, - ListOutput: "0.11.0\n\x1b[32m0.13.0\x1b[0m\n0.12.0", + ColorState: "on", + ListOutput: "0.11.0\n[x]0.13.0\n0.12.0", Properties: properties.Map{}, Template: " {{ if .ZigIcon }}{{ .ZigIcon }} {{ end }}{{ .Version }} ", + ExpectedIcon: DefaultZigIcon, }, { - Case: "version 0.13.0 active with icon", + Case: "version with colors disabled", ExpectedString: "0.13.0", HasCommand: true, - ListOutput: "0.11.0\n\x1b[32m0.13.0\x1b[0m\n0.12.0", - Properties: properties.Map{ - ZvmIcon: "⚡", - }, - Template: " {{ if .ZigIcon }}{{ .ZigIcon }} {{ end }}{{ .Version }} ", + ColorState: "off", + ListOutput: "0.11.0\n[x]0.13.0\n0.12.0", + Properties: properties.Map{}, + Template: " {{ if .ZigIcon }}{{ .ZigIcon }} {{ end }}{{ .Version }} ", + ExpectedIcon: DefaultZigIcon, }, { - Case: "version 0.12.0-dev active", - ExpectedString: "0.12.0-dev.1234+abcdef", + Case: "version with custom icon", + ExpectedString: "0.13.0", HasCommand: true, - ListOutput: "0.11.0\n\x1b[32m0.12.0-dev.1234+abcdef\x1b[0m\n0.12.0", - Properties: properties.Map{}, - // Change all test cases to expect the actual template - Template: " {{ if .ZigIcon }}{{ .ZigIcon }} {{ end }}{{ .Version }} ", + ColorState: "on", + ListOutput: "0.11.0\n[x]0.13.0\n0.12.0", + Properties: properties.Map{ + PropertyZigIcon: "⚡", + }, + Template: " {{ if .ZigIcon }}{{ .ZigIcon }} {{ end }}{{ .Version }} ", + ExpectedIcon: "⚡", }, } for _, tc := range cases { t.Run(tc.Case, func(t *testing.T) { env := new(mock.Environment) + + // Mock HasCommand first env.On("HasCommand", "zvm").Return(tc.HasCommand) - env.On("RunCommand", "zvm", []string{"list"}).Return(tc.ListOutput, nil) + + // Only set up other mocks if HasCommand is true + if tc.HasCommand { + // Mock color detection + env.On("RunCommand", "zvm", []string{"--color"}).Return(tc.ColorState, nil) + + // Mock color state changes based on detected state + if tc.ColorState == "on" { + env.On("RunCommand", "zvm", []string{"--color", "false"}).Return("", nil) + env.On("RunCommand", "zvm", []string{"--color", "true"}).Return("", nil) + } + + // Mock version list command + env.On("RunCommand", "zvm", []string{"list"}).Return(tc.ListOutput, nil) + } zvm := &Zvm{} zvm.Init(tc.Properties, env) @@ -68,13 +92,63 @@ func TestZvm(t *testing.T) { if tc.HasCommand { assert.True(t, zvm.Enabled()) - assert.Equal(t, tc.ExpectedString, zvm.Version) - if icon, ok := tc.Properties[ZvmIcon]; ok { - assert.Equal(t, icon, zvm.ZigIcon) - } + assert.Equal(t, tc.ExpectedString, zvm.Text()) + assert.Equal(t, tc.ExpectedIcon, zvm.ZigIcon) } else { assert.False(t, zvm.Enabled()) + assert.Empty(t, zvm.Text()) } + + // Verify all expected calls were made + env.AssertExpectations(t) + }) + } +} + +func TestColorStateDetection(t *testing.T) { + cases := []struct { + Case string + ColorOutput string + Expected colorState + }{ + { + Case: "enabled - on", + ColorOutput: "on", + Expected: colorState{enabled: true, valid: true}, + }, + { + Case: "enabled - yes", + ColorOutput: "yes", + Expected: colorState{enabled: true, valid: true}, + }, + { + Case: "disabled - off", + ColorOutput: "off", + Expected: colorState{enabled: false, valid: true}, + }, + { + Case: "disabled - no", + ColorOutput: "no", + Expected: colorState{enabled: false, valid: true}, + }, + { + Case: "invalid state", + ColorOutput: "invalid", + Expected: colorState{enabled: false, valid: false}, + }, + } + + for _, tc := range cases { + t.Run(tc.Case, func(t *testing.T) { + env := new(mock.Environment) + env.On("RunCommand", "zvm", []string{"--color"}).Return(tc.ColorOutput, nil) + + cmd := &colorCommand{env: env} + state := cmd.detectColorState() + + assert.Equal(t, tc.Expected.enabled, state.enabled) + assert.Equal(t, tc.Expected.valid, state.valid) + env.AssertExpectations(t) }) } } diff --git a/themes/schema.json b/themes/schema.json index 856de43f96d0..cf7dcd410b28 100644 --- a/themes/schema.json +++ b/themes/schema.json @@ -4933,7 +4933,7 @@ "type": "string", "title": "Zig Icon", "description": "icon to display before the version", - "default": "ZVM - " + "default": "ZVM" } } }