power.go (3045B)
1 package power 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "math" 8 "os" 9 "slices" 10 "strconv" 11 "strings" 12 ) 13 14 type Config struct { 15 Path string `json:"path"` 16 } 17 18 // https://www.kernel.org/doc/html/latest/power/power_supply_class.html 19 const Path = "/sys/class/power_supply" 20 21 type Block struct { 22 path string 23 } 24 25 func New(path string) *Block { 26 return &Block{path: path} 27 } 28 29 func (b *Block) Name() string { 30 return "power" 31 } 32 33 func (b *Block) Render(ctx context.Context) (string, error) { 34 // Get the power supplies. 35 var supplies []string 36 37 files, err := os.ReadDir(b.path) 38 if err != nil { 39 return "", fmt.Errorf("failed to read dir %s: %v", b.path, err) 40 } 41 for _, f := range files { 42 name := f.Name() 43 44 if name == "AC" || strings.Contains(name, "BAT") { 45 supplies = append(supplies, f.Name()) 46 } 47 } 48 slices.Sort(supplies) 49 50 var sections = []string{} 51 for _, s := range supplies { 52 path := fmt.Sprintf("%s/%s/uevent", b.path, s) 53 f, err := os.Open(path) 54 if err != nil { 55 return "", fmt.Errorf( 56 "failed to open %s: %v", path, err, 57 ) 58 } 59 defer f.Close() 60 61 switch { 62 case s == "AC": 63 sections = append(sections, outputAC(f)) 64 case strings.Contains(s, "BAT"): 65 sections = append(sections, outputBAT(f)) 66 } 67 } 68 69 var output strings.Builder 70 for i, s := range sections { 71 if i > 0 { 72 output.WriteRune(' ') 73 output.WriteRune(' ') 74 } 75 output.WriteString(s) 76 } 77 78 return output.String(), nil 79 } 80 81 func outputAC(f *os.File) string { 82 var scanner = bufio.NewScanner(f) 83 for scanner.Scan() { 84 k, v, found := strings.Cut(scanner.Text(), "=") 85 if !found { 86 continue 87 } 88 if k == "POWER_SUPPLY_ONLINE" { 89 online, err := strconv.ParseBool(v) 90 if err != nil { 91 break 92 } 93 if online { 94 return "" 95 } else { 96 return "" 97 } 98 } 99 } 100 101 return "" 102 } 103 104 func outputBAT(f *os.File) string { 105 // Compile the stats. 106 var ( 107 stats = map[string]string{} 108 scanner = bufio.NewScanner(f) 109 ) 110 for scanner.Scan() { 111 k, v, found := strings.Cut(scanner.Text(), "=") 112 if !found { 113 continue 114 } 115 // Exit early if the battery is not present. 116 if k == "POWER_SUPPLY_PRESENT" && v == "0" { 117 return "" 118 } 119 120 stats[k] = v 121 } 122 123 // Assemble output. 124 var icon rune 125 switch stats["POWER_SUPPLY_STATUS"] { 126 case "Charging": 127 icon = '' 128 case "Discharging": 129 icon = '' 130 case "Full": 131 icon = '' 132 default: 133 icon = '' 134 } 135 136 // Get capacity. 137 var capacity string 138 if s, exists := stats["POWER_SUPPLY_CAPACITY"]; exists { 139 capacity = s + "%" 140 } 141 142 // Get remaining. 143 pn, _ := strconv.ParseFloat(stats["POWER_SUPPLY_POWER_NOW"], 64) 144 en, _ := strconv.ParseFloat(stats["POWER_SUPPLY_ENERGY_NOW"], 64) 145 if r := remaining(en, pn); r != "" { 146 return fmt.Sprintf("%c %s %s", icon, capacity, r) 147 } 148 149 return fmt.Sprintf("%c %s", icon, capacity) 150 } 151 152 func remaining(energy, power float64) string { 153 if energy == 0 || power == 0 { 154 return "" 155 } 156 157 // Calculate the remaining hours. 158 var hours = energy / power 159 if hours == 0 { 160 return "" 161 } 162 163 return fmt.Sprintf( 164 "%d:%02d", 165 int(hours), 166 int((hours-math.Floor(hours))*60), 167 ) 168 }