Skip to content

Commit

Permalink
feat(darwin): manually parse battery info
Browse files Browse the repository at this point in the history
resolves #2139
  • Loading branch information
JanDeDobbeleer committed Apr 24, 2022
1 parent 5bf0c76 commit 07d6f74
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 120 deletions.
5 changes: 3 additions & 2 deletions src/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import (
"github.com/spf13/cobra"
)

// Version number of oh-my-posh
var (
config string
displayVersion bool
cliVersion string

// Version number of oh-my-posh
cliVersion string
)

var rootCmd = &cobra.Command{
Expand Down
64 changes: 64 additions & 0 deletions src/environment/darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//go:build darwin

package environment

import (
"errors"
"oh-my-posh/regex"
"strconv"
"strings"
"time"

"github.com/distatus/battery"
)

func mapMostLogicalState(state string) battery.State {
switch state {
case "charging":
return battery.Charging
case "discharging":
return battery.Discharging
case "AC attached":
return battery.NotCharging
case "full":
return battery.Full
case "empty":
return battery.Empty
case "charged":
return battery.Full
default:
return battery.Unknown
}
}

func (env *ShellEnvironment) parseBatteryOutput(output string) (*BatteryInfo, error) {
matches := regex.FindNamedRegexMatch(`(?P<PERCENTAGE>[0-9]{1,3})%; (?P<STATE>[a-zA-Z\s]+);`, output)
if len(matches) != 2 {
msg := "Unable to find battery state based on output"
env.log(Error, "BatteryInfo", msg)
return nil, errors.New(msg)
}
var percentage int
var err error
if percentage, err = strconv.Atoi(matches["PERCENTAGE"]); err != nil {
env.log(Error, "BatteryInfo", err.Error())
return nil, errors.New("Unable to parse battery percentage")
}
return &BatteryInfo{
Percentage: percentage,
State: mapMostLogicalState(matches["STATE"]),
}, nil
}

func (env *ShellEnvironment) BatteryState() (*BatteryInfo, error) {
defer env.trace(time.Now(), "BatteryInfo")
output, err := env.RunCommand("pmset", "-g", "batt")
if err != nil {
env.log(Error, "BatteryInfo", err.Error())
return nil, err
}
if !strings.Contains(output, "Battery") {
return nil, errors.New("No battery found")
}
return env.parseBatteryOutput(output)
}
61 changes: 61 additions & 0 deletions src/environment/darwin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//go:build darwin

package environment

import (
"testing"

"github.com/distatus/battery"
"github.com/stretchr/testify/assert"
)

func TestParseBatteryOutput(t *testing.T) {
cases := []struct {
Case string
Output string
ExpectedState battery.State
ExpectedPercentage int
ExpectError bool
}{
{
Case: "charging",
Output: "99%; charging;",
ExpectedState: battery.Charging,
ExpectedPercentage: 99,
},
{
Case: "charging 1%",
Output: "1%; charging;",
ExpectedState: battery.Charging,
ExpectedPercentage: 1,
},
{
Case: "not charging 80%",
Output: "81%; AC attached;",
ExpectedState: battery.NotCharging,
ExpectedPercentage: 81,
},
{
Case: "charged",
Output: "100%; charged;",
ExpectedState: battery.Full,
ExpectedPercentage: 100,
},
{
Case: "discharging",
Output: "100%; discharging;",
ExpectedState: battery.Discharging,
ExpectedPercentage: 100,
},
}
for _, tc := range cases {
env := ShellEnvironment{}
info, err := env.parseBatteryOutput(tc.Output)
if tc.ExpectError {
assert.Error(t, err, tc.Case)
return
}
assert.Equal(t, tc.ExpectedState, info.State, tc.Case)
assert.Equal(t, tc.ExpectedPercentage, info.Percentage, tc.Case)
}
}
96 changes: 0 additions & 96 deletions src/environment/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"io/fs"
"io/ioutil"
"log"
"math"
"net/http"
"oh-my-posh/regex"
"os"
Expand Down Expand Up @@ -566,101 +565,6 @@ func (env *ShellEnvironment) Flags() *Flags {
return env.CmdFlags
}

func mapMostLogicalState(currentState, newState battery.State) battery.State {
switch currentState {
case battery.Discharging, battery.NotCharging:
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 (env *ShellEnvironment) BatteryState() (*BatteryInfo, error) {
defer env.trace(time.Now(), "BatteryInfo")

parseBatteryInfo := func(batteries []*battery.Battery) *BatteryInfo {
var info BatteryInfo
var current, total float64
var state battery.State
for _, bt := range batteries {
current += bt.Current
total += bt.Full
state = mapMostLogicalState(state, bt.State)
}
batteryPercentage := current / total * 100
info.Percentage = int(math.Min(100, batteryPercentage))
info.State = state
return &info
}

batteries, err := battery.GetAll()
// actual error, return it
if err != nil && len(batteries) == 0 {
env.log(Error, "BatteryInfo", err.Error())
return nil, err
}
// there are no batteries found
if len(batteries) == 0 {
return nil, &NoBatteryError{}
}
// some batteries fail to get retrieved, filter them out if present
validBatteries := []*battery.Battery{}
for _, batt := range batteries {
if batt != nil {
validBatteries = append(validBatteries, batt)
}
}
// clean minor errors
unableToRetrieveBatteryInfo := "A device which does not exist was specified."
unknownChargeRate := "Unknown value received"
var fatalErr battery.Errors
ignoreErr := func(err error) bool {
if e, ok := err.(battery.ErrPartial); ok {
// ignore unknown charge rate value error
if e.Current == nil &&
e.Design == nil &&
e.DesignVoltage == nil &&
e.Full == nil &&
e.State == nil &&
e.Voltage == nil &&
e.ChargeRate != nil &&
e.ChargeRate.Error() == unknownChargeRate {
return true
}
}
return false
}
if batErr, ok := err.(battery.Errors); ok {
for _, err := range batErr {
if !ignoreErr(err) {
fatalErr = append(fatalErr, err)
}
}
}

// when battery info fails to get retrieved but there is at least one valid battery, return it without error
if len(validBatteries) > 0 && fatalErr != nil && strings.Contains(fatalErr.Error(), unableToRetrieveBatteryInfo) {
return parseBatteryInfo(validBatteries), nil
}
// another error occurred (possibly unmapped use-case), return it
if fatalErr != nil {
env.log(Error, "BatteryInfo", fatalErr.Error())
return nil, fatalErr
}
// everything is fine
return parseBatteryInfo(validBatteries), nil
}

func (env *ShellEnvironment) Shell() string {
defer env.trace(time.Now(), "Shell")
if env.CmdFlags.Shell != "" {
Expand Down
22 changes: 0 additions & 22 deletions src/environment/shell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package environment
import (
"testing"

"github.com/distatus/battery"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -98,24 +97,3 @@ func TestDirMatchesOneOfRegexInvertedNonEscaped(t *testing.T) {
}()
_ = dirMatchesOneOf("Projects/oh-my-posh", "", LinuxPlatform, []string{"(?!Projects/).*"})
}

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 {
assert.Equal(t, tc.ExpectedState, mapMostLogicalState(tc.CurrentState, tc.NewState), tc.Case)
}
}
106 changes: 106 additions & 0 deletions src/environment/windows_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//go:build !darwin

package environment

import (
"math"
"strings"
"time"

"github.com/distatus/battery"
)

func mapMostLogicalState(currentState, newState battery.State) battery.State {
switch currentState {
case battery.Discharging, battery.NotCharging:
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 (env *ShellEnvironment) BatteryState() (*BatteryInfo, error) {
defer env.trace(time.Now(), "BatteryInfo")

parseBatteryInfo := func(batteries []*battery.Battery) *BatteryInfo {
var info BatteryInfo
var current, total float64
var state battery.State
for _, bt := range batteries {
current += bt.Current
total += bt.Full
state = mapMostLogicalState(state, bt.State)
}
batteryPercentage := current / total * 100
info.Percentage = int(math.Min(100, batteryPercentage))
info.State = state
return &info
}

batteries, err := battery.GetAll()
// actual error, return it
if err != nil && len(batteries) == 0 {
env.log(Error, "BatteryInfo", err.Error())
return nil, err
}
// there are no batteries found
if len(batteries) == 0 {
return nil, &NoBatteryError{}
}
// some batteries fail to get retrieved, filter them out if present
validBatteries := []*battery.Battery{}
for _, batt := range batteries {
if batt != nil {
validBatteries = append(validBatteries, batt)
}
}
// clean minor errors
unableToRetrieveBatteryInfo := "A device which does not exist was specified."
unknownChargeRate := "Unknown value received"
var fatalErr battery.Errors
ignoreErr := func(err error) bool {
if e, ok := err.(battery.ErrPartial); ok {
// ignore unknown charge rate value error
if e.Current == nil &&
e.Design == nil &&
e.DesignVoltage == nil &&
e.Full == nil &&
e.State == nil &&
e.Voltage == nil &&
e.ChargeRate != nil &&
e.ChargeRate.Error() == unknownChargeRate {
return true
}
}
return false
}
if batErr, ok := err.(battery.Errors); ok {
for _, err := range batErr {
if !ignoreErr(err) {
fatalErr = append(fatalErr, err)
}
}
}

// when battery info fails to get retrieved but there is at least one valid battery, return it without error
if len(validBatteries) > 0 && fatalErr != nil && strings.Contains(fatalErr.Error(), unableToRetrieveBatteryInfo) {
return parseBatteryInfo(validBatteries), nil
}
// another error occurred (possibly unmapped use-case), return it
if fatalErr != nil {
env.log(Error, "BatteryInfo", fatalErr.Error())
return nil, fatalErr
}
// everything is fine
return parseBatteryInfo(validBatteries), nil
}

0 comments on commit 07d6f74

Please sign in to comment.