Repository: JanDeDobbeleer/oh-my-posh
Pull request #647 (2021-04-15 17:52:12)

battery improvements
### Prerequisites

- [x] I have read and understand the `CONTRIBUTING` guide
- [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

[Description of the change]

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 30669ff1..a038b85d 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -7,7 +7,7 @@
       "request": "launch",
       "mode": "debug",
       "program": "${workspaceRoot}/src",
-      "args": ["--config=${workspaceRoot}/themes/jandedobbeleer.omp.json"]
+      "args": ["--config=/Users/jan/.jandedobbeleer.omp.json"]
     },
     {
       "name": "Launch tests",
diff --git a/src/environment.go b/src/environment.go
index 51506ca8..9b1bb941 100644
--- a/src/environment.go
+++ b/src/environment.go
@@ -64,7 +64,7 @@ type environmentInfo interface {
 	lastErrorCode() int
 	executionTime() float64
 	getArgs() *args
-	getBatteryInfo() (*battery.Battery, error)
+	getBatteryInfo() ([]*battery.Battery, error)
 	getShellName() string
 	getWindowTitle(imageName, windowTitleRegex string) (string, error)
 	doGet(url string) ([]byte, error)
@@ -246,30 +246,8 @@ func (env *environment) getArgs() *args {
 	return env.args
 }
 
-func (env *environment) getBatteryInfo() (*battery.Battery, error) {
-	getMostLogicalState := func(currentState battery.State, state battery.State) battery.State {
-		if currentState == battery.Unknown {
-			return state
-		}
-		if currentState == battery.Empty|battery.Full && state == battery.Charging|battery.Discharging {
-			return state
-		}
-		return battery.Charging
-	}
-	batteries, err := battery.GetAll()
-	if err != nil {
-		return nil, err
-	}
-	if batteries == nil {
-		return nil, &noBatteryError{}
-	}
-	batt := &battery.Battery{}
-	for _, bt := range batteries {
-		batt.Current += bt.Current
-		batt.Full += bt.Full
-		batt.State = getMostLogicalState(batt.State, bt.State)
-	}
-	return batt, nil
+func (env *environment) getBatteryInfo() ([]*battery.Battery, error) {
+	return battery.GetAll()
 }
 
 func (env *environment) getShellName() string {
diff --git a/src/segment_battery.go b/src/segment_battery.go
index 0a5ae8a4..054f52bd 100644
--- a/src/segment_battery.go
+++ b/src/segment_battery.go
@@ -33,12 +33,18 @@ const (
 )
 
 func (b *batt) enabled() bool {
-	var err error
-	b.Battery, err = b.env.getBatteryInfo()
+	batteries, err := b.env.getBatteryInfo()
 	if !b.enabledWhileError(err) {
 		return false
 	}
 
+	b.Battery = &battery.Battery{}
+	for _, bt := range batteries {
+		b.Battery.Current += bt.Current
+		b.Battery.Full += bt.Full
+		b.Battery.State = b.mapMostLogicalState(b.Battery.State, bt.State)
+	}
+
 	display := b.props.getBool(DisplayCharging, true)
 	if !display && (b.Battery.State == battery.Charging || b.Battery.State == battery.Full) {
 		return false
@@ -93,6 +99,25 @@ func (b *batt) enabledWhileError(err error) bool {
 	return true
 }
 
+func (b *batt) mapMostLogicalState(currentState, newState battery.State) battery.State {
+	switch currentState {
+	case battery.Discharging:
+		return battery.Discharging
+	case battery.Empty:
+		return newState
+	case battery.Charging:
+		if newState == battery.Discharging {
+			return battery.Discharging
+		}
+		return battery.Charging
+	case battery.Unknown:
+		return newState
+	case battery.Full:
+		return newState
+	}
+	return newState
+}
+
 func (b *batt) string() string {
 	segmentTemplate := b.props.getString(SegmentTemplate, "{{.Icon}}{{ if not .Error }}{{.Percentage}}{{ end }}{{.Error}}")
 	template := &textTemplate{
diff --git a/themes/schema.json b/themes/schema.json
index f8e76c1a..c08ed228 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -357,6 +357,9 @@
                     "title": "Display while charging",
                     "description": "displays the battery status while charging (Charging or Full)",
                     "default": true
+                  },
+                  "template": {
+                    "$ref": "#/definitions/template"
                   }
                 }
               }


Test patch:
diff --git a/src/properties_test.go b/src/properties_test.go
index f040300d..a39d6326 100644
--- a/src/properties_test.go
+++ b/src/properties_test.go
@@ -7,7 +7,8 @@ import (
 )
 
 const (
-	expected = "expected"
+	expected      = "expected"
+	expectedColor = "#768954"
 )
 
 func TestGetString(t *testing.T) {
diff --git a/src/segment_battery_test.go b/src/segment_battery_test.go
index b345b84c..76a95320 100644
--- a/src/segment_battery_test.go
+++ b/src/segment_battery_test.go
@@ -9,193 +9,139 @@ import (
 )
 
 const (
-	expectedColor = "#768954"
+	chargingColor    = "#123456"
+	dischargingColor = "#765432"
+	chargedColor     = "#248644"
 )
 
-func setupBatteryTests(state battery.State, batteryLevel float64, props *properties) *batt {
-	env := &MockedEnvironment{}
-	bt := &battery.Battery{
-		State:   state,
-		Full:    100,
-		Current: batteryLevel,
-	}
-	env.On("getBatteryInfo", nil).Return(bt, nil)
-	b := &batt{
-		props: props,
-		env:   env,
-	}
-	b.enabled()
-	return b
-}
-
-func TestBatteryCharging(t *testing.T) {
-	props := &properties{
-		values: map[Property]interface{}{
-			ChargingIcon: "charging ",
-		},
-	}
-	b := setupBatteryTests(battery.Charging, 80, props)
-	assert.Equal(t, "charging 80", b.string())
-}
-
-func TestBatteryCharged(t *testing.T) {
-	props := &properties{
-		values: map[Property]interface{}{
-			ChargedIcon: "charged ",
-		},
-	}
-	b := setupBatteryTests(battery.Full, 100, props)
-	assert.Equal(t, "charged 100", b.string())
-}
-
-func TestBatteryDischarging(t *testing.T) {
-	props := &properties{
-		values: map[Property]interface{}{
-			DischargingIcon: "going down ",
+func TestBatterySegmentSingle(t *testing.T) {
+	cases := []struct {
+		Case            string
+		BatteryState    battery.State
+		BatteryLevel    float64
+		ExpectedString  string
+		ExpectedEnabled bool
+		ExpectedColor   string
+		ColorBackground bool
+		DisplayError    bool
+		Error           error
+		DisableCharging bool
+	}{
+		{Case: "80% charging", BatteryState: battery.Charging, BatteryLevel: 80, ExpectedString: "charging 80", ExpectedEnabled: true},
+		{Case: "battery full", BatteryState: battery.Full, BatteryLevel: 100, ExpectedString: "charged 100", ExpectedEnabled: true},
+		{Case: "70% discharging", BatteryState: battery.Discharging, BatteryLevel: 70, ExpectedString: "going down 70", ExpectedEnabled: true},
+		{
+			Case:            "discharging background color",
+			BatteryState:    battery.Discharging,
+			BatteryLevel:    70,
+			ExpectedString:  "going down 70",
+			ExpectedEnabled: true,
+			ColorBackground: true,
+			ExpectedColor:   dischargingColor,
 		},
-	}
-	b := setupBatteryTests(battery.Discharging, 70, props)
-	assert.Equal(t, "going down 70", b.string())
-}
-
-func TestBatteryBackgroundColor(t *testing.T) {
-	expected := expectedColor
-	props := &properties{
-		background: "#111111",
-		values: map[Property]interface{}{
-			DischargingIcon:  "going down ",
-			ColorBackground:  true,
-			DischargingColor: expected,
+		{
+			Case:            "charging background color",
+			BatteryState:    battery.Charging,
+			BatteryLevel:    70,
+			ExpectedString:  "charging 70",
+			ExpectedEnabled: true,
+			ColorBackground: true,
+			ExpectedColor:   chargingColor,
 		},
-	}
-	b := setupBatteryTests(battery.Discharging, 70, props)
-	b.string()
-	assert.Equal(t, expected, props.background)
-}
-
-func TestBatteryBackgroundColorInvalid(t *testing.T) {
-	expected := expectedColor
-	props := &properties{
-		background: expected,
-		values: map[Property]interface{}{
-			DischargingIcon:  "going down ",
-			ColorBackground:  true,
-			DischargingColor: "derp",
+		{
+			Case:            "charged background color",
+			BatteryState:    battery.Full,
+			BatteryLevel:    70,
+			ExpectedString:  "charged 70",
+			ExpectedEnabled: true,
+			ColorBackground: true,
+			ExpectedColor:   chargedColor,
 		},
-	}
-	b := setupBatteryTests(battery.Discharging, 70, props)
-	b.string()
-	assert.Equal(t, expected, props.background)
-}
-
-func TestBatteryForegroundColor(t *testing.T) {
-	expected := expectedColor
-	props := &properties{
-		foreground: "#111111",
-		values: map[Property]interface{}{
-			DischargingIcon:  "going down ",
-			ColorBackground:  false,
-			DischargingColor: expected,
+		{
+			Case:            "discharging foreground color",
+			BatteryState:    battery.Discharging,
+			BatteryLevel:    70,
+			ExpectedString:  "going down 70",
+			ExpectedEnabled: true,
+			ExpectedColor:   dischargingColor,
 		},
-	}
-	b := setupBatteryTests(battery.Discharging, 70, props)
-	b.string()
-	assert.Equal(t, expected, props.foreground)
-}
-
-func TestBatteryForegroundColorInvalid(t *testing.T) {
-	expected := expectedColor
-	props := &properties{
-		foreground: expected,
-		values: map[Property]interface{}{
-			DischargingIcon:  "going down ",
-			ColorBackground:  false,
-			DischargingColor: "derp",
+		{
+			Case:            "charging foreground color",
+			BatteryState:    battery.Charging,
+			BatteryLevel:    70,
+			ExpectedString:  "charging 70",
+			ExpectedEnabled: true,
+			ExpectedColor:   chargingColor,
 		},
-	}
-	b := setupBatteryTests(battery.Discharging, 70, props)
-	b.string()
-	assert.Equal(t, expected, props.foreground)
-}
-
-func TestBatteryError(t *testing.T) {
-	env := &MockedEnvironment{}
-	err := errors.New("oh snap")
-	env.On("getBatteryInfo", nil).Return(&battery.Battery{}, err)
-	b := &batt{
-		props: &properties{
-			values: map[Property]interface{}{
-				DisplayError: true,
-			},
+		{
+			Case:            "charged foreground color",
+			BatteryState:    battery.Full,
+			BatteryLevel:    70,
+			ExpectedString:  "charged 70",
+			ExpectedEnabled: true,
+			ExpectedColor:   chargedColor,
+		},
+		{Case: "battery error", DisplayError: true, Error: errors.New("oh snap"), ExpectedString: "oh snap", ExpectedEnabled: true},
+		{Case: "battery error disabled", Error: errors.New("oh snap")},
+		{Case: "no batteries", DisplayError: true, Error: &noBatteryError{}},
+		{Case: "display charging disabled: charging", BatteryState: battery.Charging, DisableCharging: true},
+		{Case: "display charging disabled: charged", BatteryState: battery.Full, DisableCharging: true},
+		{
+			Case:            "display charging disabled: discharging",
+			BatteryState:    battery.Discharging,
+			BatteryLevel:    70,
+			ExpectedString:  "going down 70",
+			ExpectedEnabled: true,
+			DisableCharging: true,
 		},
-		env: env,
 	}
-	assert.True(t, b.enabled())
-	assert.Equal(t, "oh snap", b.string())
-}
 
-func TestBatteryErrorHidden(t *testing.T) {
-	env := &MockedEnvironment{}
-	err := errors.New("oh snap")
-	env.On("getBatteryInfo", nil).Return(&battery.Battery{}, err)
-	b := &batt{
-		props: &properties{
-			values: map[Property]interface{}{
-				DisplayError: false,
+	for _, tc := range cases {
+		env := &MockedEnvironment{}
+		batteries := []*battery.Battery{
+			{
+				Full:    100,
+				State:   tc.BatteryState,
+				Current: tc.BatteryLevel,
 			},
-		},
-		env: env,
-	}
-	assert.False(t, b.enabled())
-}
-
-func TestBatteryNoBattery(t *testing.T) {
-	env := &MockedEnvironment{}
-	err := &noBatteryError{}
-	env.On("getBatteryInfo", nil).Return(&battery.Battery{}, err)
-	b := &batt{
-		props: &properties{
+		}
+		props := &properties{
+			background: "#111111",
+			foreground: "#ffffff",
 			values: map[Property]interface{}{
-				DisplayError: true,
+				ChargingIcon:     "charging ",
+				ChargedIcon:      "charged ",
+				DischargingIcon:  "going down ",
+				DischargingColor: dischargingColor,
+				ChargedColor:     chargedColor,
+				ChargingColor:    chargingColor,
+				ColorBackground:  tc.ColorBackground,
+				DisplayError:     tc.DisplayError,
 			},
-		},
-		env: env,
-	}
-	assert.False(t, b.enabled())
-}
-
-func TestBatteryDischargingAndDisplayChargingDisabled(t *testing.T) {
-	props := &properties{
-		values: map[Property]interface{}{
-			DischargingIcon: "going down ",
-			DisplayCharging: false,
-		},
-	}
-	b := setupBatteryTests(battery.Discharging, 70, props)
-	assert.Equal(t, true, b.enabled())
-	assert.Equal(t, "going down 70", b.string())
-}
-
-func TestBatteryChargingAndDisplayChargingDisabled(t *testing.T) {
-	props := &properties{
-		values: map[Property]interface{}{
-			ChargingIcon:    "charging ",
-			DisplayCharging: false,
-		},
-	}
-	b := setupBatteryTests(battery.Charging, 80, props)
-	assert.Equal(t, false, b.enabled())
-}
-
-func TestBatteryChargedAndDisplayChargingDisabled(t *testing.T) {
-	props := &properties{
-		values: map[Property]interface{}{
-			ChargedIcon:     "charged ",
-			DisplayCharging: false,
-		},
+		}
+		if tc.DisableCharging {
+			props.values[DisplayCharging] = false
+		}
+		env.On("getBatteryInfo", nil).Return(batteries, tc.Error)
+		b := &batt{
+			props: props,
+			env:   env,
+		}
+		enabled := b.enabled()
+		assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case)
+		if !enabled {
+			continue
+		}
+		assert.Equal(t, tc.ExpectedString, b.string(), tc.Case)
+		if len(tc.ExpectedColor) == 0 {
+			continue
+		}
+		actualColor := b.props.foreground
+		if tc.ColorBackground {
+			actualColor = b.props.background
+		}
+		assert.Equal(t, tc.ExpectedColor, actualColor, tc.Case)
 	}
-	b := setupBatteryTests(battery.Full, 100, props)
-	assert.Equal(t, false, b.enabled())
 }
 
 func TestGetBatteryColors(t *testing.T) {
@@ -267,3 +213,25 @@ func TestGetBatteryColors(t *testing.T) {
 		assert.Equal(t, tc.ExpectedColor, color, tc.Case)
 	}
 }
+
+func TestMapBatteriesState(t *testing.T) {
+	cases := []struct {
+		Case          string
+		ExpectedState battery.State
+		CurrentState  battery.State
+		NewState      battery.State
+	}{
+		{Case: "charging > charged", ExpectedState: battery.Charging, CurrentState: battery.Full, NewState: battery.Charging},
+		{Case: "charging < discharging", ExpectedState: battery.Discharging, CurrentState: battery.Discharging, NewState: battery.Charging},
+		{Case: "charging == charging", ExpectedState: battery.Charging, CurrentState: battery.Charging, NewState: battery.Charging},
+		{Case: "discharging > charged", ExpectedState: battery.Discharging, CurrentState: battery.Full, NewState: battery.Discharging},
+		{Case: "discharging > unknown", ExpectedState: battery.Discharging, CurrentState: battery.Unknown, NewState: battery.Discharging},
+		{Case: "discharging > full", ExpectedState: battery.Discharging, CurrentState: battery.Full, NewState: battery.Discharging},
+		{Case: "discharging > charging 2", ExpectedState: battery.Discharging, CurrentState: battery.Charging, NewState: battery.Discharging},
+		{Case: "discharging > empty", ExpectedState: battery.Discharging, CurrentState: battery.Empty, NewState: battery.Discharging},
+	}
+	for _, tc := range cases {
+		batt := &batt{}
+		assert.Equal(t, tc.ExpectedState, batt.mapMostLogicalState(tc.CurrentState, tc.NewState), tc.Case)
+	}
+}
diff --git a/src/segment_path_test.go b/src/segment_path_test.go
index d23712b0..f5aa0385 100644
--- a/src/segment_path_test.go
+++ b/src/segment_path_test.go
@@ -107,9 +107,9 @@ func (env *MockedEnvironment) getArgs() *args {
 	return arguments.Get(0).(*args)
 }
 
-func (env *MockedEnvironment) getBatteryInfo() (*battery.Battery, error) {
+func (env *MockedEnvironment) getBatteryInfo() ([]*battery.Battery, error) {
 	args := env.Called(nil)
-	return args.Get(0).(*battery.Battery), args.Error(1)
+	return args.Get(0).([]*battery.Battery), args.Error(1)
 }
 
 func (env *MockedEnvironment) getShellName() string {