feat: dotnet segment for .NET SDK display

New segment for .NET SDK version (or unsupported version) display.

Includes update for handling command execution errors so segments
can act differently based on exit codes. Using a custom error
type to make it testable rather than passing the OS error directly
to the segment.
This commit is contained in:
Travis Illig 2020-10-16 08:43:02 -07:00 committed by Jan De Dobbeleer
parent 23233bc383
commit 5844faa54d
18 changed files with 224 additions and 35 deletions

View file

@ -0,0 +1,31 @@
---
id: dotnet
title: Dotnet
sidebar_label: Dotnet
---
## What
Display the currently active .NET SDK version.
## Sample Configuration
```json
{
"type": "dotnet",
"style": "powerline",
"powerline_symbol": "",
"foreground": "#000000",
"background": "#00ffff",
"properties": {
"prefix": " \uE77F "
}
}
```
## Properties
- display_version: `boolean` - display the active version or not; useful if all you need is an icon indicating `dotnet`
is present - defaults to `true`
- unsupported_version_icon: `string` - text/icon that is displayed when the active .NET SDK version (e.g., one specified
by `global.json`) is not installed/supported - defaults to `\u2327` (X in a rectangle box)

View file

@ -17,6 +17,7 @@ module.exports = {
"az", "az",
"battery", "battery",
"command", "command",
"dotnet",
"environment", "environment",
"exit", "exit",
"git", "git",

View file

@ -1,6 +1,8 @@
package main package main
import ( import (
"errors"
"fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
@ -26,7 +28,7 @@ type environmentInfo interface {
getHostName() (string, error) getHostName() (string, error)
getRuntimeGOOS() string getRuntimeGOOS() string
hasCommand(command string) bool hasCommand(command string) bool
runCommand(command string, args ...string) string runCommand(command string, args ...string) (string, error)
runShellCommand(shell string, command string) string runShellCommand(shell string, command string) string
lastErrorCode() int lastErrorCode() int
getArgs() *args getArgs() *args
@ -39,6 +41,14 @@ type environment struct {
cwd string cwd string
} }
type commandError struct {
exitCode int
}
func (e *commandError) Error() string {
return fmt.Sprintf("%d", e.exitCode)
}
func (env *environment) getenv(key string) string { func (env *environment) getenv(key string) string {
return os.Getenv(key) return os.Getenv(key)
} }
@ -110,12 +120,18 @@ func (env *environment) getRuntimeGOOS() string {
return runtime.GOOS return runtime.GOOS
} }
func (env *environment) runCommand(command string, args ...string) string { func (env *environment) runCommand(command string, args ...string) (string, error) {
out, err := exec.Command(command, args...).Output() out, err := exec.Command(command, args...).Output()
if err != nil {
return "" var exerr *exec.ExitError
if errors.As(err, &exerr) {
return "", &commandError{exitCode: exerr.ExitCode()}
} }
return strings.TrimSpace(string(out)) if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
} }
func (env *environment) runShellCommand(shell string, command string) string { func (env *environment) runShellCommand(shell string, command string) string {

View file

@ -20,6 +20,8 @@ const (
ColorBackground Property = "color_background" ColorBackground Property = "color_background"
//IgnoreFolders folders to ignore and not run the segment logic //IgnoreFolders folders to ignore and not run the segment logic
IgnoreFolders Property = "ignore_folders" IgnoreFolders Property = "ignore_folders"
//DisplayVersion show the version number or not
DisplayVersion Property = "display_version"
) )
type properties struct { type properties struct {

View file

@ -67,6 +67,8 @@ const (
Az SegmentType = "az" Az SegmentType = "az"
//Kubectl writes the Kubernetes context we're currently in //Kubectl writes the Kubernetes context we're currently in
Kubectl SegmentType = "kubectl" Kubectl SegmentType = "kubectl"
//Dotnet writes which dotnet version is currently active
Dotnet SegmentType = "dotnet"
//Powerline writes it Powerline style //Powerline writes it Powerline style
Powerline SegmentStyle = "powerline" Powerline SegmentStyle = "powerline"
//Plain writes it without ornaments //Plain writes it without ornaments
@ -123,6 +125,7 @@ func (segment *Segment) mapSegmentWithWriter(env environmentInfo) error {
EnvVar: &envvar{}, EnvVar: &envvar{},
Az: &az{}, Az: &az{},
Kubectl: &kubectl{}, Kubectl: &kubectl{},
Dotnet: &dotnet{},
} }
if writer, ok := functions[segment.Type]; ok { if writer, ok := functions[segment.Type]; ok {
props := &properties{ props := &properties{

View file

@ -40,7 +40,7 @@ func (a *az) enabled() bool {
return false return false
} }
output := a.env.runCommand("az", "account", "show", "--query=[name,id]", "-o=tsv") output, _ := a.env.runCommand("az", "account", "show", "--query=[name,id]", "-o=tsv")
if output == "" { if output == "" {
return false return false
} }

58
segment_dotnet.go Normal file
View file

@ -0,0 +1,58 @@
package main
import (
"errors"
)
type dotnet struct {
props *properties
env environmentInfo
activeVersion string
unsupportedVersion bool
}
const (
//UnsupportedDotnetVersionIcon is displayed when the dotnet version in
//the current folder isn't supported by the installed dotnet SDK set.
UnsupportedDotnetVersionIcon Property = "unsupported_version_icon"
)
func (d *dotnet) string() string {
if d.unsupportedVersion {
return d.props.getString(UnsupportedDotnetVersionIcon, "\u2327")
}
if d.props.getBool(DisplayVersion, true) {
return d.activeVersion
}
return ""
}
func (d *dotnet) init(props *properties, env environmentInfo) {
d.props = props
d.env = env
}
func (d *dotnet) enabled() bool {
if !d.env.hasCommand("dotnet") {
return false
}
output, err := d.env.runCommand("dotnet", "--version")
if err == nil {
d.activeVersion = output
return true
}
// Exit code 145 is a special indicator that dotnet
// ran, but the current project config settings specify
// use of an SDK that isn't installed.
var exerr *commandError
if errors.As(err, &exerr) && exerr.exitCode == 145 {
d.unsupportedVersion = true
return true
}
return false
}

81
segment_dotnet_test.go Normal file
View file

@ -0,0 +1,81 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
type dotnetArgs struct {
enabled bool
version string
unsupported bool
unsupportedIcon string
displayVersion bool
}
func bootStrapDotnetTest(args *dotnetArgs) *dotnet {
env := new(MockedEnvironment)
env.On("hasCommand", "dotnet").Return(args.enabled)
if args.unsupported {
err := &commandError{exitCode: 145}
env.On("runCommand", "dotnet", []string{"--version"}).Return("", err)
} else {
env.On("runCommand", "dotnet", []string{"--version"}).Return(args.version, nil)
}
props := &properties{
values: map[Property]interface{}{
DisplayVersion: args.displayVersion,
UnsupportedDotnetVersionIcon: args.unsupportedIcon,
},
}
a := &dotnet{
env: env,
props: props,
}
return a
}
func TestEnabledDotnetNotFound(t *testing.T) {
args := &dotnetArgs{
enabled: false,
}
dotnet := bootStrapDotnetTest(args)
assert.False(t, dotnet.enabled())
}
func TestDotnetVersionNotDisplayed(t *testing.T) {
args := &dotnetArgs{
enabled: true,
displayVersion: false,
version: "3.1.402",
}
dotnet := bootStrapDotnetTest(args)
assert.True(t, dotnet.enabled())
assert.Equal(t, "", dotnet.string())
}
func TestDotnetVersionDisplayed(t *testing.T) {
expected := "3.1.402"
args := &dotnetArgs{
enabled: true,
displayVersion: true,
version: expected,
}
dotnet := bootStrapDotnetTest(args)
assert.True(t, dotnet.enabled())
assert.Equal(t, expected, dotnet.string())
}
func TestDotnetVersionUnsupported(t *testing.T) {
expected := "x"
args := &dotnetArgs{
enabled: true,
displayVersion: true,
unsupported: true,
unsupportedIcon: expected,
}
dotnet := bootStrapDotnetTest(args)
assert.True(t, dotnet.enabled())
assert.Equal(t, expected, dotnet.string())
}

View file

@ -101,7 +101,7 @@ func (g *git) enabled() bool {
if !g.env.hasCommand("git") { if !g.env.hasCommand("git") {
return false return false
} }
output := g.env.runCommand("git", "rev-parse", "--is-inside-work-tree") output, _ := g.env.runCommand("git", "rev-parse", "--is-inside-work-tree")
return output == "true" return output == "true"
} }
@ -183,7 +183,8 @@ func (g *git) setGitStatus() {
func (g *git) getGitCommandOutput(args ...string) string { func (g *git) getGitCommandOutput(args ...string) string {
args = append([]string{"-c", "core.quotepath=false", "-c", "color.status=false"}, args...) args = append([]string{"-c", "core.quotepath=false", "-c", "color.status=false"}, args...)
return g.env.runCommand("git", args...) val, _ := g.env.runCommand("git", args...)
return val
} }
func (g *git) getGitHEADContext(ref string) string { func (g *git) getGitHEADContext(ref string) string {

View file

@ -18,7 +18,7 @@ func TestEnabledGitNotFound(t *testing.T) {
func TestEnabledInWorkingDirectory(t *testing.T) { func TestEnabledInWorkingDirectory(t *testing.T) {
env := new(MockedEnvironment) env := new(MockedEnvironment)
env.On("hasCommand", "git").Return(true) env.On("hasCommand", "git").Return(true)
env.On("runCommand", "git", []string{"rev-parse", "--is-inside-work-tree"}).Return("true") env.On("runCommand", "git", []string{"rev-parse", "--is-inside-work-tree"}).Return("true", nil)
g := &git{ g := &git{
env: env, env: env,
} }
@ -30,7 +30,7 @@ func TestGetGitOutputForCommand(t *testing.T) {
commandArgs := []string{"symbolic-ref", "--short", "HEAD"} commandArgs := []string{"symbolic-ref", "--short", "HEAD"}
want := "je suis le output" want := "je suis le output"
env := new(MockedEnvironment) env := new(MockedEnvironment)
env.On("runCommand", "git", append(args, commandArgs...)).Return(want) env.On("runCommand", "git", append(args, commandArgs...)).Return(want, nil)
g := &git{ g := &git{
env: env, env: env,
} }
@ -70,12 +70,12 @@ func setupHEADContextEnv(context *detachedContext) *git {
env.On("getFileContent", "/.git/MERGE_HEAD").Return(context.mergeHEAD) env.On("getFileContent", "/.git/MERGE_HEAD").Return(context.mergeHEAD)
env.On("hasFiles", "/.git/CHERRY_PICK_HEAD").Return(context.cherryPick) env.On("hasFiles", "/.git/CHERRY_PICK_HEAD").Return(context.cherryPick)
env.On("hasFiles", "/.git/MERGE_HEAD").Return(context.merge) env.On("hasFiles", "/.git/MERGE_HEAD").Return(context.merge)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rev-parse", "--short", "HEAD"}).Return(context.currentCommit) env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rev-parse", "--short", "HEAD"}).Return(context.currentCommit, nil)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "describe", "--tags", "--exact-match"}).Return(context.tagName) env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "describe", "--tags", "--exact-match"}).Return(context.tagName, nil)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.origin}).Return(context.origin) env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.origin}).Return(context.origin, nil)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.onto}).Return(context.onto) env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.onto}).Return(context.onto, nil)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.cherryPickSHA}).Return(context.cherryPickSHA) env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.cherryPickSHA}).Return(context.cherryPickSHA, nil)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.mergeHEAD}).Return(context.mergeHEAD) env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.mergeHEAD}).Return(context.mergeHEAD, nil)
g := &git{ g := &git{
env: env, env: env,
repo: &gitRepo{ repo: &gitRepo{
@ -200,7 +200,7 @@ func TestGetGitHEADContextMergeTag(t *testing.T) {
func TestGetStashContextZeroEntries(t *testing.T) { func TestGetStashContextZeroEntries(t *testing.T) {
want := "" want := ""
env := new(MockedEnvironment) env := new(MockedEnvironment)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rev-list", "--walk-reflogs", "--count", "refs/stash"}).Return("") env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rev-list", "--walk-reflogs", "--count", "refs/stash"}).Return("", nil)
g := &git{ g := &git{
env: env, env: env,
} }
@ -211,7 +211,7 @@ func TestGetStashContextZeroEntries(t *testing.T) {
func TestGetStashContextMultipleEntries(t *testing.T) { func TestGetStashContextMultipleEntries(t *testing.T) {
want := "2" want := "2"
env := new(MockedEnvironment) env := new(MockedEnvironment)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rev-list", "--walk-reflogs", "--count", "refs/stash"}).Return("2") env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rev-list", "--walk-reflogs", "--count", "refs/stash"}).Return("2", nil)
g := &git{ g := &git{
env: env, env: env,
} }

View file

@ -19,6 +19,6 @@ func (k *kubectl) enabled() bool {
if !k.env.hasCommand("kubectl") { if !k.env.hasCommand("kubectl") {
return false return false
} }
k.contextName = k.env.runCommand("kubectl", "config", "current-context") k.contextName, _ = k.env.runCommand("kubectl", "config", "current-context")
return true return true
} }

View file

@ -14,7 +14,7 @@ type kubectlArgs struct {
func bootStrapKubectlTest(args *kubectlArgs) *kubectl { func bootStrapKubectlTest(args *kubectlArgs) *kubectl {
env := new(MockedEnvironment) env := new(MockedEnvironment)
env.On("hasCommand", "kubectl").Return(args.enabled) env.On("hasCommand", "kubectl").Return(args.enabled)
env.On("runCommand", "kubectl", []string{"config", "current-context"}).Return(args.contextName) env.On("runCommand", "kubectl", []string{"config", "current-context"}).Return(args.contextName, nil)
k := &kubectl{ k := &kubectl{
env: env, env: env,
props: &properties{}, props: &properties{},

View file

@ -6,11 +6,6 @@ type node struct {
nodeVersion string nodeVersion string
} }
const (
//DisplayVersion show the version number or not
DisplayVersion Property = "display_version"
)
func (n *node) string() string { func (n *node) string() string {
if n.props.getBool(DisplayVersion, true) { if n.props.getBool(DisplayVersion, true) {
return n.nodeVersion return n.nodeVersion
@ -30,6 +25,6 @@ func (n *node) enabled() bool {
if !n.env.hasCommand("node") { if !n.env.hasCommand("node") {
return false return false
} }
n.nodeVersion = n.env.runCommand("node", "--version") n.nodeVersion, _ = n.env.runCommand("node", "--version")
return true return true
} }

View file

@ -17,7 +17,7 @@ type nodeArgs struct {
func bootStrapNodeTest(args *nodeArgs) *node { func bootStrapNodeTest(args *nodeArgs) *node {
env := new(MockedEnvironment) env := new(MockedEnvironment)
env.On("hasCommand", "node").Return(args.enabled) env.On("hasCommand", "node").Return(args.enabled)
env.On("runCommand", "node", []string{"--version"}).Return(args.nodeVersion) env.On("runCommand", "node", []string{"--version"}).Return(args.nodeVersion, nil)
env.On("hasFiles", "*.js").Return(args.hasJS) env.On("hasFiles", "*.js").Return(args.hasJS)
env.On("hasFiles", "*.ts").Return(args.hasTS) env.On("hasFiles", "*.ts").Return(args.hasTS)
props := &properties{ props := &properties{

View file

@ -68,9 +68,9 @@ func (env *MockedEnvironment) hasCommand(command string) bool {
return args.Bool(0) return args.Bool(0)
} }
func (env *MockedEnvironment) runCommand(command string, args ...string) string { func (env *MockedEnvironment) runCommand(command string, args ...string) (string, error) {
arguments := env.Called(command, args) arguments := env.Called(command, args)
return arguments.String(0) return arguments.String(0), arguments.Error(1)
} }
func (env *MockedEnvironment) runShellCommand(shell string, command string) string { func (env *MockedEnvironment) runShellCommand(shell string, command string) string {

View file

@ -38,7 +38,7 @@ func (p *python) enabled() bool {
"python", "python",
} }
for index, python := range pythonVersions { for index, python := range pythonVersions {
version := p.env.runCommand(python, "--version") version, _ := p.env.runCommand(python, "--version")
if version != "" { if version != "" {
rawVersion := strings.TrimLeft(version, "Python") rawVersion := strings.TrimLeft(version, "Python")
p.pythonVersion = strings.Trim(rawVersion, " ") p.pythonVersion = strings.Trim(rawVersion, " ")

View file

@ -15,8 +15,8 @@ type pythonArgs struct {
pathSeparator string pathSeparator string
pythonVersion string pythonVersion string
python3Version string python3Version string
hasPyFiles bool hasPyFiles bool
hasNotebookFiles bool hasNotebookFiles bool
} }
func newPythonArgs() *pythonArgs { func newPythonArgs() *pythonArgs {
@ -37,8 +37,8 @@ func bootStrapPythonTest(args *pythonArgs) *python {
env := new(MockedEnvironment) env := new(MockedEnvironment)
env.On("hasFiles", "*.py").Return(args.hasPyFiles) env.On("hasFiles", "*.py").Return(args.hasPyFiles)
env.On("hasFiles", "*.ipynb").Return(args.hasNotebookFiles) env.On("hasFiles", "*.ipynb").Return(args.hasNotebookFiles)
env.On("runCommand", "python", []string{"--version"}).Return(args.pythonVersion) env.On("runCommand", "python", []string{"--version"}).Return(args.pythonVersion, nil)
env.On("runCommand", "python3", []string{"--version"}).Return(args.python3Version) env.On("runCommand", "python3", []string{"--version"}).Return(args.python3Version, nil)
env.On("getenv", "VIRTUAL_ENV").Return(args.virtualEnvName) env.On("getenv", "VIRTUAL_ENV").Return(args.virtualEnvName)
env.On("getenv", "CONDA_ENV_PATH").Return(args.condaEnvName) env.On("getenv", "CONDA_ENV_PATH").Return(args.condaEnvName)
env.On("getenv", "CONDA_DEFAULT_ENV").Return(args.condaDefaultName) env.On("getenv", "CONDA_DEFAULT_ENV").Return(args.condaDefaultName)

View file

@ -61,5 +61,6 @@ func (s *spotify) init(props *properties, env environmentInfo) {
} }
func (s *spotify) runAppleScriptCommand(command string) string { func (s *spotify) runAppleScriptCommand(command string) string {
return s.env.runCommand("osascript", "-e", command) val, _ := s.env.runCommand("osascript", "-e", command)
return val
} }