power.go (3103B)
1 package power 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "math" 8 "os" 9 "sort" 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 sort.Slice(supplies, func(i, j int) bool { 49 return supplies[i] < supplies[j] 50 }) 51 52 var sections = []string{} 53 for _, s := range supplies { 54 path := fmt.Sprintf("%s/%s/uevent", b.path, s) 55 f, err := os.Open(path) 56 if err != nil { 57 return "", fmt.Errorf( 58 "failed to open %s: %v", path, err, 59 ) 60 } 61 defer f.Close() 62 63 switch { 64 case s == "AC": 65 sections = append(sections, outputAC(f)) 66 case strings.Contains(s, "BAT"): 67 sections = append(sections, outputBAT(f)) 68 } 69 } 70 71 var output strings.Builder 72 for i, s := range sections { 73 if i > 0 { 74 output.WriteRune(' ') 75 output.WriteRune(' ') 76 } 77 output.WriteString(s) 78 } 79 80 return output.String(), nil 81 } 82 83 func outputAC(f *os.File) string { 84 var scanner = bufio.NewScanner(f) 85 for scanner.Scan() { 86 k, v, found := strings.Cut(scanner.Text(), "=") 87 if !found { 88 continue 89 } 90 if k == "POWER_SUPPLY_ONLINE" { 91 online, err := strconv.ParseBool(v) 92 if err != nil { 93 break 94 } 95 if online { 96 return "" 97 } else { 98 return "" 99 } 100 } 101 } 102 103 return "" 104 } 105 106 func outputBAT(f *os.File) string { 107 // Compile the stats. 108 var ( 109 stats = map[string]string{} 110 scanner = bufio.NewScanner(f) 111 ) 112 for scanner.Scan() { 113 k, v, found := strings.Cut(scanner.Text(), "=") 114 if !found { 115 continue 116 } 117 // Exit early if the battery is not present. 118 if k == "POWER_SUPPLY_PRESENT" && v == "0" { 119 return "" 120 } 121 122 stats[k] = v 123 } 124 125 // Assemble output. 126 var icon rune 127 switch stats["POWER_SUPPLY_STATUS"] { 128 case "Charging": 129 icon = '' 130 case "Discharging": 131 icon = '' 132 case "Full": 133 icon = '' 134 default: 135 icon = '' 136 } 137 138 // Get capacity. 139 var capacity string 140 if s, exists := stats["POWER_SUPPLY_CAPACITY"]; exists { 141 capacity = s + "%" 142 } 143 144 // Get remaining. 145 pn, _ := strconv.ParseFloat(stats["POWER_SUPPLY_POWER_NOW"], 64) 146 en, _ := strconv.ParseFloat(stats["POWER_SUPPLY_ENERGY_NOW"], 64) 147 if r := remaining(en, pn); r != "" { 148 return fmt.Sprintf("%c %s %s", icon, capacity, r) 149 } 150 151 return fmt.Sprintf("%c %s", icon, capacity) 152 } 153 154 func remaining(energy, power float64) string { 155 if energy == 0 || power == 0 { 156 return "" 157 } 158 159 // Calculate the remaining hours. 160 var hours = energy / power 161 if hours == 0 { 162 return "" 163 } 164 165 return fmt.Sprintf( 166 "%d:%02d", 167 int(hours), 168 int((hours-math.Floor(hours))*60), 169 ) 170 }