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