feat: include battery lib

resolves #2479
This commit is contained in:
Jan De Dobbeleer 2022-07-05 16:27:25 +02:00 committed by Jan De Dobbeleer
parent 253c71e0cf
commit 17857db340
21 changed files with 880 additions and 218 deletions

View file

@ -0,0 +1,60 @@
// Copyright (C) 2016-2017 Karol 'Kenji Takahashi' Woźniak
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package battery
type Info struct {
Percentage int
State State
}
type NoBatteryError struct{}
func (m *NoBatteryError) Error() string {
return "no battery"
}
// State type enumerates possible battery states.
type State int
var states = [...]string{
Unknown: "Unknown",
Empty: "Empty",
Full: "Full",
Charging: "Charging",
Discharging: "Discharging",
NotCharging: "Not Charging",
}
func (s State) String() string {
return states[s]
}
// Possible state values.
// Unknown can mean either controller returned unknown, or
// not able to retrieve state due to some error.
const (
Unknown State = iota
Empty
Full
Charging
Discharging
NotCharging
)

View file

@ -0,0 +1,56 @@
package battery
import (
"errors"
"oh-my-posh/environment/cmd"
"oh-my-posh/regex"
"strconv"
"strings"
)
func mapMostLogicalState(state string) State {
switch state {
case "charging":
return Charging
case "discharging":
return Discharging
case "AC attached":
return NotCharging
case "full":
return Full
case "empty":
return Empty
case "charged":
return Full
default:
return Unknown
}
}
func parseBatteryOutput(output string) (*Info, 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"
return nil, errors.New(msg)
}
var percentage int
var err error
if percentage, err = strconv.Atoi(matches["PERCENTAGE"]); err != nil {
return nil, errors.New("Unable to parse battery percentage")
}
return &Info{
Percentage: percentage,
State: mapMostLogicalState(matches["STATE"]),
}, nil
}
func Get() (*Info, error) {
output, err := cmd.Run("pmset", "-g", "batt")
if err != nil {
return nil, err
}
if !strings.Contains(output, "Battery") {
return nil, ErrNotFound
}
return parseBatteryOutput(output)
}

View file

@ -1,11 +1,8 @@
//go:build darwin
package environment
package battery
import (
"testing"
"github.com/distatus/battery"
"github.com/stretchr/testify/assert"
)
@ -13,44 +10,43 @@ func TestParseBatteryOutput(t *testing.T) {
cases := []struct {
Case string
Output string
ExpectedState battery.State
ExpectedState State
ExpectedPercentage int
ExpectError bool
}{
{
Case: "charging",
Output: "99%; charging;",
ExpectedState: battery.Charging,
ExpectedState: Charging,
ExpectedPercentage: 99,
},
{
Case: "charging 1%",
Output: "1%; charging;",
ExpectedState: battery.Charging,
ExpectedState: Charging,
ExpectedPercentage: 1,
},
{
Case: "not charging 80%",
Output: "81%; AC attached;",
ExpectedState: battery.NotCharging,
ExpectedState: NotCharging,
ExpectedPercentage: 81,
},
{
Case: "charged",
Output: "100%; charged;",
ExpectedState: battery.Full,
ExpectedState: Full,
ExpectedPercentage: 100,
},
{
Case: "discharging",
Output: "100%; discharging;",
ExpectedState: battery.Discharging,
ExpectedState: Discharging,
ExpectedPercentage: 100,
},
}
for _, tc := range cases {
env := ShellEnvironment{}
info, err := env.parseBatteryOutput(tc.Output)
info, err := parseBatteryOutput(tc.Output)
if tc.ExpectError {
assert.Error(t, err, tc.Case)
return

View file

@ -0,0 +1,156 @@
// battery
// Copyright (C) 2016-2017 Karol 'Kenji Takahashi' Woźniak
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package battery
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
)
const sysfs = "/sys/class/power_supply"
func newState(name string) (State, error) {
for i, state := range states {
if strings.EqualFold(name, state) {
return State(i), nil
}
}
return Unknown, fmt.Errorf("Invalid state `%s`", name)
}
func readFloat(path, filename string) (float64, error) {
str, err := ioutil.ReadFile(filepath.Join(path, filename))
if err != nil {
return 0, err
}
if len(str) == 0 {
return 0, ErrNotFound
}
num, err := strconv.ParseFloat(string(str[:len(str)-1]), 64)
if err != nil {
return 0, err
}
return num / 1000, nil // Convert micro->milli
}
func readAmp(path, filename string, volts float64) (float64, error) {
val, err := readFloat(path, filename)
if err != nil {
return 0, err
}
return val * volts, nil
}
func isBattery(path string) bool {
t, err := ioutil.ReadFile(filepath.Join(path, "type"))
return err == nil && string(t) == "Battery\n"
}
func getBatteryFiles() ([]string, error) {
files, err := ioutil.ReadDir(sysfs)
if err != nil {
return nil, err
}
var bFiles []string
for _, file := range files {
path := filepath.Join(sysfs, file.Name())
if isBattery(path) {
bFiles = append(bFiles, path)
}
}
if len(bFiles) == 0 {
return nil, &NoBatteryError{}
}
return bFiles, nil
}
func getByPath(path string) (*battery, error) {
b := &battery{}
var err error
if b.Current, err = readFloat(path, "energy_now"); err == nil {
if b.Full, err = readFloat(path, "energy_full"); err != nil {
return nil, errors.New("unable to parse energy_full")
}
} else {
currentDoesNotExist := os.IsNotExist(err)
if b.Voltage, err = readFloat(path, "voltage_now"); err != nil {
return nil, errors.New("unable to parse voltage_now")
}
b.Voltage /= 1000
if currentDoesNotExist {
if b.Current, err = readAmp(path, "charge_now", b.Voltage); err != nil {
return nil, errors.New("unable to parse charge_now")
}
if b.Full, err = readAmp(path, "charge_full", b.Voltage); err != nil {
return nil, errors.New("unable to parse charge_full")
}
} else {
if b.Full, err = readFloat(path, "energy_full"); err != nil {
return nil, errors.New("unable to parse energy_full")
}
}
}
state, err := ioutil.ReadFile(filepath.Join(path, "status"))
if err != nil || len(state) == 0 {
return nil, errors.New("unable to parse or invalid status")
}
if b.State, err = newState(string(state[:len(state)-1])); err != nil {
return nil, errors.New("unable to map to new state")
}
return b, nil
}
func systemGetAll() ([]*battery, error) {
bFiles, err := getBatteryFiles()
if err != nil {
return nil, err
}
var batteries []*battery
var errs Errors
for _, bFile := range bFiles {
b, err := getByPath(bFile)
if err != nil {
errs = append(errs, err)
continue
}
batteries = append(batteries, b)
}
if len(batteries) == 0 {
return nil, errs
}
return batteries, nil
}

View file

@ -0,0 +1,321 @@
// battery
// Copyright (C) 2016-2017 Karol 'Kenji Takahashi' Woźniak
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package battery
import (
"errors"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
type batteryQueryInformation struct {
BatteryTag uint32
InformationLevel int32
AtRate int32
}
type batteryInformation struct {
Capabilities uint32
Technology uint8
Reserved [3]uint8
Chemistry [4]uint8
DesignedCapacity uint32
FullChargedCapacity uint32
DefaultAlert1 uint32
DefaultAlert2 uint32
CriticalBias uint32
CycleCount uint32
}
type batteryWaitStatus struct {
BatteryTag uint32
Timeout uint32
PowerState uint32
LowCapacity uint32
HighCapacity uint32
}
type batteryStatus struct {
PowerState uint32
Capacity uint32
Voltage uint32
Rate int32
}
type guid struct {
Data1 uint32
Data2 uint16
Data3 uint16
Data4 [8]byte
}
type spDeviceInterfaceData struct {
cbSize uint32
InterfaceClassGuid guid // nolint:revive
Flags uint32
Reserved uint
}
var guidDeviceBattery = guid{
0x72631e54,
0x78A4,
0x11d0,
[8]byte{0xbc, 0xf7, 0x00, 0xaa, 0x00, 0xb7, 0xb3, 0x2a},
}
func uint32ToFloat64(num uint32) (float64, error) {
if num == 0xffffffff { // BATTERY_UNKNOWN_CAPACITY
return 0, errors.New("Unknown value received")
}
return float64(num), nil
}
func setupDiSetup(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) (uintptr, error) {
r1, _, errno := syscall.Syscall6(proc.Addr(), nargs, a1, a2, a3, a4, a5, a6)
if windows.Handle(r1) == windows.InvalidHandle {
if errno != 0 {
return 0, error(errno)
}
return 0, syscall.EINVAL
}
return r1, nil
}
func setupDiCall(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) syscall.Errno {
r1, _, errno := syscall.Syscall6(proc.Addr(), nargs, a1, a2, a3, a4, a5, a6)
if r1 == 0 {
if errno != 0 {
return errno
}
return syscall.EINVAL
}
return 0
}
var setupapi = &windows.LazyDLL{Name: "setupapi.dll", System: true}
var setupDiGetClassDevsW = setupapi.NewProc("SetupDiGetClassDevsW")
var setupDiEnumDeviceInterfaces = setupapi.NewProc("SetupDiEnumDeviceInterfaces")
var setupDiGetDeviceInterfaceDetailW = setupapi.NewProc("SetupDiGetDeviceInterfaceDetailW")
var setupDiDestroyDeviceInfoList = setupapi.NewProc("SetupDiDestroyDeviceInfoList")
func readState(powerState uint32) State {
switch {
case powerState&0x00000004 != 0:
return Charging
case powerState&0x00000008 != 0:
return Empty
case powerState&0x00000002 != 0:
return Discharging
case powerState&0x00000001 != 0:
return Full
default:
return Unknown
}
}
func systemGet(idx int) (*battery, error) {
hdev, err := setupDiSetup(
setupDiGetClassDevsW,
4,
uintptr(unsafe.Pointer(&guidDeviceBattery)),
0,
0,
2|16, // DIGCF_PRESENT|DIGCF_DEVICEINTERFACE
0, 0,
)
if err != nil {
return nil, err
}
defer func() {
_, _, _ = syscall.Syscall(setupDiDestroyDeviceInfoList.Addr(), 1, hdev, 0, 0)
}()
var did spDeviceInterfaceData
did.cbSize = uint32(unsafe.Sizeof(did))
errno := setupDiCall(
setupDiEnumDeviceInterfaces,
5,
hdev,
0,
uintptr(unsafe.Pointer(&guidDeviceBattery)),
uintptr(idx),
uintptr(unsafe.Pointer(&did)),
0,
)
if errno == 259 { // ERROR_NO_MORE_ITEMS
return nil, ErrNotFound
}
if errno != 0 {
return nil, errno
}
var cbRequired uint32
errno = setupDiCall(
setupDiGetDeviceInterfaceDetailW,
6,
hdev,
uintptr(unsafe.Pointer(&did)),
0,
0,
uintptr(unsafe.Pointer(&cbRequired)),
0,
)
if errno != 0 && errno != 122 { // ERROR_INSUFFICIENT_BUFFER
return nil, errno
}
// The god damn struct with ANYSIZE_ARRAY of utf16 in it is crazy.
// So... let's emulate it with array of uint16 ;-D.
// Keep in mind that the first two elements are actually cbSize.
didd := make([]uint16, cbRequired/2)
cbSize := (*uint32)(unsafe.Pointer(&didd[0]))
if unsafe.Sizeof(uint(0)) == 8 {
*cbSize = 8
} else {
*cbSize = 6
}
errno = setupDiCall(
setupDiGetDeviceInterfaceDetailW,
6,
hdev,
uintptr(unsafe.Pointer(&did)),
uintptr(unsafe.Pointer(&didd[0])),
uintptr(cbRequired),
uintptr(unsafe.Pointer(&cbRequired)),
0,
)
if errno != 0 {
return nil, errno
}
devicePath := &didd[2:][0]
handle, err := windows.CreateFile(
devicePath,
windows.GENERIC_READ|windows.GENERIC_WRITE,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE,
nil,
windows.OPEN_EXISTING,
windows.FILE_ATTRIBUTE_NORMAL,
0,
)
if err != nil {
return nil, err
}
defer func() {
_ = windows.CloseHandle(handle)
}()
var dwOut uint32
var dwWait uint32
var bqi batteryQueryInformation
err = windows.DeviceIoControl(
handle,
2703424, // IOCTL_BATTERY_QUERY_TAG
(*byte)(unsafe.Pointer(&dwWait)),
uint32(unsafe.Sizeof(dwWait)),
(*byte)(unsafe.Pointer(&bqi.BatteryTag)),
uint32(unsafe.Sizeof(bqi.BatteryTag)),
&dwOut,
nil,
)
if err != nil {
return nil, err
}
if bqi.BatteryTag == 0 {
return nil, errors.New("BatteryTag not returned")
}
b := &battery{}
var bi batteryInformation
err = windows.DeviceIoControl(
handle,
2703428, // IOCTL_BATTERY_QUERY_INFORMATION
(*byte)(unsafe.Pointer(&bqi)),
uint32(unsafe.Sizeof(bqi)),
(*byte)(unsafe.Pointer(&bi)),
uint32(unsafe.Sizeof(bi)),
&dwOut,
nil,
)
if err != nil {
return nil, err
}
b.Full = float64(bi.FullChargedCapacity)
bws := batteryWaitStatus{BatteryTag: bqi.BatteryTag}
var bs batteryStatus
err = windows.DeviceIoControl(
handle,
2703436, // IOCTL_BATTERY_QUERY_STATUS
(*byte)(unsafe.Pointer(&bws)),
uint32(unsafe.Sizeof(bws)),
(*byte)(unsafe.Pointer(&bs)),
uint32(unsafe.Sizeof(bs)),
&dwOut,
nil,
)
if err != nil {
return nil, err
}
if b.Current, err = uint32ToFloat64(bs.Capacity); err != nil {
return nil, err
}
if b.Voltage, err = uint32ToFloat64(bs.Voltage); err != nil {
return nil, err
}
b.Voltage /= 1000
b.State = readState(bs.PowerState)
return b, nil
}
func systemGetAll() ([]*battery, error) {
var batteries []*battery
var i int
var errs Errors
for i = 0; ; i++ {
b, err := systemGet(i)
if err == ErrNotFound {
break
}
if err != nil {
errs = append(errs, err)
continue
}
batteries = append(batteries, b)
}
if i == 0 {
return nil, &NoBatteryError{}
}
if len(batteries) == 0 {
return nil, errs
}
return batteries, nil
}

View file

@ -0,0 +1,65 @@
//go:build !darwin
package battery
import (
"math"
)
// battery type represents a single battery entry information.
type battery struct {
// Current battery state.
State State
// Current (momentary) capacity (in mWh).
Current float64
// Last known full capacity (in mWh).
Full float64
// Current voltage (in V).
Voltage float64
}
func mapMostLogicalState(currentState, newState State) State {
switch currentState {
case Discharging, NotCharging:
return Discharging
case Empty:
return newState
case Charging:
if newState == Discharging {
return Discharging
}
return Charging
case Unknown:
return newState
case Full:
return newState
}
return newState
}
// GetAll returns information about all batteries in the system.
//
// If error != nil, it will be either ErrFatal or Errors.
// If error is of type Errors, it is guaranteed that length of both returned slices is the same and that i-th error coresponds with i-th battery structure.
func Get() (*Info, error) {
parseBatteryInfo := func(batteries []*battery) *Info {
var info Info
var current, total float64
var state 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 := systemGetAll()
if err != nil {
return nil, err
}
return parseBatteryInfo(batteries), nil
}

View file

@ -0,0 +1,51 @@
//go:build !darwin
// battery
// Copyright (C) 2016-2017 Karol 'Kenji Takahashi' Woźniak
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package battery
import (
"testing"
"github.com/alecthomas/assert"
)
func TestMapBatteriesState(t *testing.T) {
cases := []struct {
Case string
ExpectedState State
CurrentState State
NewState State
}{
{Case: "charging > charged", ExpectedState: Charging, CurrentState: Full, NewState: Charging},
{Case: "charging < discharging", ExpectedState: Discharging, CurrentState: Discharging, NewState: Charging},
{Case: "charging == charging", ExpectedState: Charging, CurrentState: Charging, NewState: Charging},
{Case: "discharging > charged", ExpectedState: Discharging, CurrentState: Full, NewState: Discharging},
{Case: "discharging > unknown", ExpectedState: Discharging, CurrentState: Unknown, NewState: Discharging},
{Case: "discharging > full", ExpectedState: Discharging, CurrentState: Full, NewState: Discharging},
{Case: "discharging > charging 2", ExpectedState: Discharging, CurrentState: Charging, NewState: Discharging},
{Case: "discharging > empty", ExpectedState: Discharging, CurrentState: Empty, NewState: Discharging},
}
for _, tc := range cases {
assert.Equal(t, tc.ExpectedState, mapMostLogicalState(tc.CurrentState, tc.NewState), tc.Case)
}
}

View file

@ -0,0 +1,42 @@
// battery
// Copyright (C) 2016-2017 Karol 'Kenji Takahashi' Woźniak
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package battery
import "fmt"
var ErrNotFound = fmt.Errorf("Not found")
type Errors []error
func (e Errors) Error() string {
var s string
for _, err := range e {
if err != nil {
s += err.Error() + ", "
}
}
// strip trailing colon/space
if len(s) > 1 {
s = s[:len(s)-2]
}
return s
}

View file

@ -0,0 +1,48 @@
// battery
// Copyright (C) 2016-2017 Karol 'Kenji Takahashi' Woźniak
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package battery
import (
"errors"
"testing"
)
func TestErrors(t *testing.T) {
cases := []struct {
in Errors
str string
}{
{Errors{nil}, ""},
{Errors{errors.New("")}, ""},
{Errors{errors.New("t1")}, "t1"},
{Errors{errors.New("t2"), errors.New("t3")}, "t2, t3"},
{Errors{errors.New("t4"), errors.New("t5")}, "t4, t5"},
}
for i, c := range cases {
str := c.in.Error()
if str != c.str {
t.Errorf("%d: %v != %v", i, str, c.str)
}
}
}

View file

@ -0,0 +1,27 @@
package cmd
import (
"bytes"
"os/exec"
"strings"
)
func Run(command string, args ...string) (string, error) {
cmd := exec.Command(command, args...)
var out bytes.Buffer
var err bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &err
cmdErr := cmd.Run()
if cmdErr != nil {
output := err.String()
return output, cmdErr
}
// some silly commands return 0 and the output is in stderr instead of stdout
result := out.String()
if len(result) == 0 {
result = err.String()
}
output := strings.TrimSpace(result)
return output, nil
}

View file

@ -9,6 +9,8 @@ import (
"io/fs"
"log"
"net/http"
"oh-my-posh/environment/battery"
"oh-my-posh/environment/cmd"
"oh-my-posh/regex"
"os"
"os/exec"
@ -19,7 +21,6 @@ import (
"sync"
"time"
"github.com/distatus/battery"
process "github.com/shirou/gopsutil/v3/process"
"golang.org/x/text/cases"
"golang.org/x/text/language"
@ -61,12 +62,6 @@ func (e *CommandError) Error() string {
return e.Err
}
type NoBatteryError struct{}
func (m *NoBatteryError) Error() string {
return "no battery"
}
type FileInfo struct {
ParentFolder string
Path string
@ -140,11 +135,6 @@ func (t *TemplateCache) AddSegmentData(key string, value interface{}) {
t.Segments[key] = value
}
type BatteryInfo struct {
Percentage int
State battery.State
}
type Environment interface {
Getenv(key string) string
Pwd() string
@ -173,7 +163,7 @@ type Environment interface {
RunShellCommand(shell, command string) string
ExecutionTime() float64
Flags() *Flags
BatteryState() (*BatteryInfo, error)
BatteryState() (*battery.Info, error)
QueryWindowTitles(processName, windowTitleRegex string) (string, error)
WindowsRegistryKeyValue(path string) (*WindowsRegistryValue, error)
HTTPRequest(url string, timeout int, requestModifiers ...HTTPRequestModifier) ([]byte, error)
@ -203,11 +193,11 @@ func (c *commandCache) set(command, path string) {
}
func (c *commandCache) get(command string) (string, bool) {
cmd, found := c.commands.get(command)
cacheCommand, found := c.commands.get(command)
if !found {
return "", false
}
command, ok := cmd.(string)
command, ok := cacheCommand.(string)
return command, ok
}
@ -494,29 +484,15 @@ func (env *ShellEnvironment) GOOS() string {
func (env *ShellEnvironment) RunCommand(command string, args ...string) (string, error) {
defer env.Trace(time.Now(), "RunCommand", append([]string{command}, args...)...)
if cmd, ok := env.cmdCache.get(command); ok {
command = cmd
if cacheCommand, ok := env.cmdCache.get(command); ok {
command = cacheCommand
}
cmd := exec.Command(command, args...)
var out bytes.Buffer
var err bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &err
cmdErr := cmd.Run()
if cmdErr != nil {
output := err.String()
errorStr := fmt.Sprintf("cmd.Start() failed with '%s'", output)
env.Log(Error, "RunCommand", errorStr)
return output, cmdErr
output, err := cmd.Run(command, args...)
if err != nil {
env.Log(Error, "RunCommand", "cmd.Run() failed")
}
// some silly commands return 0 and the output is in stderr instead of stdout
result := out.String()
if len(result) == 0 {
result = err.String()
}
output := strings.TrimSpace(result)
env.Log(Debug, "RunCommand", output)
return output, nil
return output, err
}
func (env *ShellEnvironment) RunShellCommand(shell, command string) string {

View file

@ -1,5 +1,3 @@
//go:build darwin
package environment
import (
@ -9,7 +7,7 @@ import (
"strings"
"time"
"github.com/distatus/battery"
"oh-my-posh/environment/battery"
)
func mapMostLogicalState(state string) battery.State {
@ -31,30 +29,30 @@ func mapMostLogicalState(state string) battery.State {
}
}
func (env *ShellEnvironment) parseBatteryOutput(output string) (*BatteryInfo, error) {
func (env *ShellEnvironment) parseBatteryOutput(output string) (*battery.Info, 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)
env.Log(Error, "BatteryState", 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())
env.Log(Error, "BatteryState", err.Error())
return nil, errors.New("Unable to parse battery percentage")
}
return &BatteryInfo{
return &battery.Info{
Percentage: percentage,
State: mapMostLogicalState(matches["STATE"]),
}, nil
}
func (env *ShellEnvironment) BatteryState() (*BatteryInfo, error) {
defer env.Trace(time.Now(), "BatteryInfo")
func (env *ShellEnvironment) BatteryState() (*battery.Info, error) {
defer env.Trace(time.Now(), "BatteryState")
output, err := env.RunCommand("pmset", "-g", "batt")
if err != nil {
env.Log(Error, "BatteryInfo", err.Error())
env.Log(Error, "BatteryState", err.Error())
return nil, err
}
if !strings.Contains(output, "Battery") {

View file

@ -1,5 +1,3 @@
//go:build windows
package environment
import (

View file

@ -0,0 +1,18 @@
//go:build !darwin
package environment
import (
"oh-my-posh/environment/battery"
"time"
)
func (env *ShellEnvironment) BatteryState() (*battery.Info, error) {
defer env.Trace(time.Now(), "BatteryState")
info, err := battery.Get()
if err != nil {
env.Log(Error, "BatteryState", err.Error())
return nil, err
}
return info, nil
}

View file

@ -1,106 +0,0 @@
//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
}

View file

@ -1,31 +0,0 @@
//go:build !darwin
package environment
import (
"testing"
"github.com/distatus/battery"
"github.com/stretchr/testify/assert"
)
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)
}
}

View file

@ -8,7 +8,6 @@ require (
github.com/alecthomas/assert v1.0.0
github.com/alecthomas/colour v0.1.0 // indirect
github.com/alecthomas/repr v0.1.0 // indirect
github.com/distatus/battery v0.10.0
github.com/esimov/stackblur-go v1.1.0
github.com/fogleman/gg v1.3.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
@ -29,7 +28,6 @@ require (
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b
golang.org/x/text v0.3.7
gopkg.in/ini.v1 v1.66.6
howett.net/plist v1.0.0 // indirect
)
require (
@ -90,6 +88,4 @@ require (
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
)
replace github.com/distatus/battery v0.10.0 => github.com/JanDeDobbeleer/battery v0.10.0-5
replace github.com/atotto/clipboard v0.1.4 => github.com/jandedobbeleer/clipboard v0.1.4-1

View file

@ -6,8 +6,6 @@ github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/ConradIrwin/font v0.0.0-20210318200717-ce8d41cc0732 h1:0EDePskeF4vNFCk70ATaFHQzjmwXsk+VImnMJttecNU=
github.com/ConradIrwin/font v0.0.0-20210318200717-ce8d41cc0732/go.mod h1:krTLO7JWu6g8RMxG8sl+T1Hf8W93XQacBKJmqFZ2MFY=
github.com/JanDeDobbeleer/battery v0.10.0-5 h1:RdZlM4ioJRVrt0JFAgLASbECb7xlBifvmgwYFdwp55I=
github.com/JanDeDobbeleer/battery v0.10.0-5/go.mod h1:STnSvFLX//eEpkaN7qWRxCWxrWOcssTDgnG4yqq9BRE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
@ -91,7 +89,6 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jandedobbeleer/clipboard v0.1.4-1 h1:rJehm5W0a3hvjcxyB3snqLBV4yvMBBc12JyMP7ngNQw=
github.com/jandedobbeleer/clipboard v0.1.4-1/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
@ -231,7 +228,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -271,8 +267,6 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -282,6 +276,3 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=

View file

@ -3,6 +3,7 @@ package mock
import (
"io/fs"
"oh-my-posh/environment"
"oh-my-posh/environment/battery"
"time"
mock "github.com/stretchr/testify/mock"
@ -122,9 +123,9 @@ func (env *MockedEnvironment) Flags() *environment.Flags {
return arguments.Get(0).(*environment.Flags)
}
func (env *MockedEnvironment) BatteryState() (*environment.BatteryInfo, error) {
func (env *MockedEnvironment) BatteryState() (*battery.Info, error) {
args := env.Called()
return args.Get(0).(*environment.BatteryInfo), args.Error(1)
return args.Get(0).(*battery.Info), args.Error(1)
}
func (env *MockedEnvironment) Shell() string {

View file

@ -2,16 +2,15 @@ package segments
import (
"oh-my-posh/environment"
"oh-my-posh/environment/battery"
"oh-my-posh/properties"
"github.com/distatus/battery"
)
type Battery struct {
props properties.Properties
env environment.Environment
*environment.BatteryInfo
*battery.Info
Error string
Icon string
}
@ -38,18 +37,18 @@ func (b *Battery) Enabled() bool {
}
var err error
b.BatteryInfo, err = b.env.BatteryState()
b.Info, err = b.env.BatteryState()
if !b.enabledWhileError(err) {
return false
}
// case on computer without batteries(no error, empty array)
if err == nil && b.BatteryInfo == nil {
if err == nil && b.Info == nil {
return false
}
switch b.BatteryInfo.State {
switch b.Info.State {
case battery.Discharging:
b.Icon = b.props.GetString(DischargingIcon, "")
case battery.NotCharging:
@ -68,7 +67,7 @@ func (b *Battery) enabledWhileError(err error) bool {
if err == nil {
return true
}
if _, ok := err.(*environment.NoBatteryError); ok {
if _, ok := err.(*battery.NoBatteryError); ok {
return false
}
displayError := b.props.GetBool(properties.DisplayError, false)