package providers import ( "fmt" "io/ioutil" "os" "path" "strconv" "strings" "os/exec" "path/filepath" "github.com/codemicro/bar/internal/i3bar" ) const ( batteryStateFull = "FUL" // "full" batteryStateDischarging = "BAT" batteryStateCharging = "CHR" batteryStateNotCharging = "HLD" // for "hold" - present when charge threshold set batteryStateUnknown = "UNK" ) type Battery struct { FullThreshold float32 OkThreshold float32 WarningThreshold float32 DeviceName string UseDesignMaxEnergy bool name string previousWasBackgroundWarning bool isAlert bool previousValue string } func NewBattery(deviceName string, fullThreshold, okThreshold, warningThreshold float32) i3bar.BlockGenerator { return &Battery{ DeviceName: deviceName, FullThreshold: fullThreshold, OkThreshold: okThreshold, WarningThreshold: warningThreshold, name: "battery", } } func (g *Battery) Frequency() uint8 { if g.isAlert { return 1 } return 5 } func (g *Battery) infoPath() string { return path.Join("/sys/class/power_supply", g.DeviceName) } func (g *Battery) getPercentage() (float32, error) { // TODO: Cache the read of energy_full var maxEnergy, energyNow int32 { maxFile := "energy_full" if g.UseDesignMaxEnergy { maxFile = "energy_full_design" } me, err := ioutil.ReadFile(path.Join(g.infoPath(), maxFile)) if err != nil { return 0, err } en, err := ioutil.ReadFile(path.Join(g.infoPath(), "energy_now")) if err != nil { return 0, err } mei, err := strconv.ParseInt(strings.TrimSpace(string(me)), 10, 32) if err != nil { return 0, err } eni, err := strconv.ParseInt(strings.TrimSpace(string(en)), 10, 32) if err != nil { return 0, err } maxEnergy = int32(mei) energyNow = int32(eni) } return (float32(energyNow) / float32(maxEnergy)) * 100, nil } func (g *Battery) getState() (string, error) { sa, err := ioutil.ReadFile(path.Join(g.infoPath(), "status")) if err != nil { return "", err } var x string switch strings.TrimSpace(string(sa)) { case "Full": x = batteryStateFull case "Discharging": x = batteryStateDischarging case "Charging": x = batteryStateCharging case "Not charging": x = batteryStateNotCharging case "Unknown": fallthrough default: x = batteryStateUnknown } return x, nil } func (g *Battery) Block(colors *i3bar.ColorSet) (*i3bar.Block, error) { percentage, err := g.getPercentage() if err != nil { return nil, err } state, err := g.getState() if err != nil { return nil, err } percentageString := fmt.Sprintf("%.1f", percentage) shortPercentageString := strings.Split(percentageString, ".")[0] if g.previousValue != shortPercentageString && state == batteryStateDischarging { switch shortPercentageString { case "20": _ = g.SendNotification("Battery is getting low", "At 20% and discharging. Consider plugging in.", "low") case "12": _ = g.SendNotification("Battery is getting lower", "At 12% and discharging. Plug in now.", "normal") case "5": _ = g.SendNotification("Battery is really low", "At 5% and discharging. PLUG IN NOW.", "critical") } } g.previousValue = shortPercentageString block := &i3bar.Block{ Name: g.name, Instance: g.DeviceName, FullText: fmt.Sprintf("%s %s%%", state, percentageString), ShortText: percentageString, } if percentage < g.WarningThreshold && g.WarningThreshold != 0 { g.isAlert = true if g.previousWasBackgroundWarning || state == batteryStateCharging { // disable flashing when on charge block.TextColor = colors.Bad } else { block.BackgroundColor = colors.Bad } g.previousWasBackgroundWarning = !g.previousWasBackgroundWarning } else if percentage < g.OkThreshold && g.OkThreshold != 0 { block.TextColor = colors.Warning } else { g.isAlert = false } switch state { case batteryStateCharging: if percentage > g.FullThreshold && g.FullThreshold != 0 { block.TextColor = colors.Good } else { // Set text/background color to white block.BackgroundColor = nil block.TextColor = nil } case batteryStateFull: block.BackgroundColor = colors.Warning block.TextColor = colors.Background case batteryStateUnknown: block.TextColor = colors.Warning } return block, nil } func (g *Battery) GetNameAndInstance() (string, string) { return g.name, g.DeviceName } func (g *Battery) SendNotification(summary, message, priority string) error { fname, err := exec.LookPath("notify-send") if err == nil { fname, err = filepath.Abs(fname) } if err != nil { return err } process, err := os.StartProcess(fname, []string{fname, "--urgency", priority, summary, message}, &os.ProcAttr{Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}}) if err != nil { return err } _ = process.Release() return nil }