├── LICENSE ├── .gitignore ├── README.md └── gods.go /LICENSE: -------------------------------------------------------------------------------- 1 | "THE BEER-WARE LICENSE" (Revision 42): 2 | wrote this file. As long as you retain this notice you 3 | can do whatever you want with this stuff. If we meet some day, and you think 4 | this stuff is worth it, you can buy me a beer in return Markus Teich 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | *~ 6 | *.swp 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gods 2 | 3 | ## Summary 4 | 5 | „gods“ stands for „Go dwm status“. It displays time, sysload, memory 6 | consumption, battery level and network transfer speeds. 7 | 8 | ## Dependencies 9 | 10 | Only a working Go environment and the xsetroot binary is needed. Per default you 11 | should use the [Material Design 12 | Webfont](https://github.com/Templarian/MaterialDesign-Webfont) (I'm using the 13 | version from Debian `testing`) within dwm, so you have the nice little icons. 14 | Otherwise you need to exchange some characters in the source (see gods.go 15 | header). For dwm the [statuscolor 16 | patch](http://dwm.suckless.org/patches/statuscolors) is recommended. 17 | 18 | ## Usage 19 | 20 | To install, run 21 | 22 | go get github.com/schachmat/gods 23 | 24 | Then add the following line to your `.xinitrc` or whereever you start dwm, but 25 | before actually starting dwm: 26 | 27 | $GOPATH/bin/gods & 28 | 29 | ## Configuration 30 | 31 | The Gods status bar can be easily modified, just by patching the source. You can 32 | add new informational panels, remove others, change the ordering or formating. 33 | With a custom font you can use own icons and separators and through the 34 | statuscolors patch config in dwm you can change the colors. 35 | 36 | ## Contributing 37 | 38 | This repository is meant as an example of how to draw your dwm status bar with 39 | go, as I use it. No additional features will be merged from pull-requests, but 40 | you can tell me about your fork and changes and then I can link them here as 41 | further examples for other people to look at. 42 | 43 | - [phacops forked](https://github.com/phacops/gods) gods to not use a custom 44 | font, but rather replace the icons with plain text. 45 | 46 | ## License 47 | 48 | "THE BEER-WARE LICENSE" (Revision 42): 49 | wrote this file. As long as you retain this notice you 50 | can do whatever you want with this stuff. If we meet some day, and you think 51 | this stuff is worth it, you can buy me a beer in return Markus Teich 52 | -------------------------------------------------------------------------------- /gods.go: -------------------------------------------------------------------------------- 1 | // Command gods collects some system information, formats it nicely and sets 2 | // the X root windows name so it can be displayed in the dwm status bar. 3 | // 4 | // The low value runes in the output are used by dwm to colorize the output 5 | // (\u0001 to \u0006, needs the http://dwm.suckless.org/patches/statuscolors 6 | // patch) and as Icons or separators (e.g. "\uf246"). This setup is recommended 7 | // for using the following fonts in dwm config.h: primary: dejavu sans mono, 8 | // fallback: material design icons. 9 | // 10 | // For license information see the file LICENSE 11 | package main 12 | 13 | import ( 14 | "bufio" 15 | "bytes" 16 | "fmt" 17 | "io/ioutil" 18 | "os" 19 | "os/exec" 20 | "regexp" 21 | "runtime" 22 | "strconv" 23 | "strings" 24 | "time" 25 | ) 26 | 27 | const ( 28 | iconCPU = "\uf35b" 29 | iconDateTime = "\uf150" 30 | iconMemory = "\uf193" 31 | iconNetRX = "\uf046" 32 | iconNetTX = "\uf05e" 33 | iconPowerBattery = "\uf080" 34 | iconPowerCharging = "\uf084" 35 | iconVolume = "\uf57e" 36 | iconVolumeMuted = "\uf581" 37 | 38 | fieldSeparator = " " 39 | ) 40 | 41 | const ( 42 | reset = 0 43 | green = 1 44 | yellow = 2 45 | red = 3 46 | ) 47 | 48 | var ( 49 | color = []string{"\u0001", "\u0002", "\u0003", "\u0006"} 50 | ignoreNetDevPrefix = []string{"lo", "tun", "tap"} 51 | cores = runtime.NumCPU() // count of cores to scale cpu usage 52 | rxOld = 0 53 | txOld = 0 54 | unmutedLine = regexp.MustCompile("^[[:blank:]]*Mute: no$") 55 | volumeLine = regexp.MustCompile("^[[:blank:]]*Volume: ") 56 | channelVolume = regexp.MustCompile("[[:digit:]]+%") 57 | ) 58 | 59 | // fixed builds a fixed width string with given icon and fitting suffix 60 | func fixed(icon string, rate int) string { 61 | if rate < 0 { 62 | return color[red] + " ERR" + color[reset] + icon 63 | } 64 | 65 | var decDigit = 0 66 | var suf = "B" // default: display as B/s 67 | 68 | switch { 69 | case rate >= (1000 * 1024 * 1024): // > 999 MiB/s 70 | return color[red] + " ERR" + color[reset] + icon 71 | case rate >= (1000 * 1024): // display as MiB/s 72 | decDigit = (rate / 1024 / 102) % 10 73 | rate /= (1024 * 1024) 74 | suf = "M" 75 | icon = color[green] + icon + color[reset] 76 | case rate >= 1000: // display as KiB/s 77 | decDigit = (rate / 102) % 10 78 | rate /= 1024 79 | suf = "K" 80 | } 81 | 82 | if rate >= 100 { 83 | return fmt.Sprintf("%s%3d%s%s", color[reset], rate, suf, icon) 84 | } else if rate >= 10 { 85 | return fmt.Sprintf("%s %2d%s%s", color[reset], rate, suf, icon) 86 | } else { 87 | return fmt.Sprintf("%s%1d.%1d%s%s", color[reset], rate, decDigit, suf, icon) 88 | } 89 | } 90 | 91 | // updateNetUse reads current transfer rates of certain network interfaces 92 | func updateNetUse() string { 93 | file, err := os.Open("/proc/net/dev") 94 | if err != nil { 95 | e := " " + color[red] + "ERR" + color[reset] 96 | return e + iconNetRX + " " + e + iconNetTX 97 | } 98 | defer file.Close() 99 | 100 | var void = 0 // target for unused values 101 | var dev, rx, tx, rxNow, txNow = "", 0, 0, 0, 0 102 | var scanner = bufio.NewScanner(file) 103 | // Skip first two lines (table header) 104 | scanner.Scan() 105 | scanner.Scan() 106 | for scanner.Scan() { 107 | _, err = fmt.Sscanf(scanner.Text(), "%s %d %d %d %d %d %d %d %d %d", 108 | &dev, &rx, &void, &void, &void, &void, &void, &void, &void, &tx) 109 | for _, ignorePrefix := range ignoreNetDevPrefix { 110 | if strings.HasPrefix(dev, ignorePrefix) { 111 | continue 112 | } 113 | } 114 | rxNow += rx 115 | txNow += tx 116 | } 117 | 118 | defer func() { rxOld, txOld = rxNow, txNow }() 119 | return fmt.Sprintf( 120 | "%s %s", 121 | fixed(iconNetRX, rxNow-rxOld), 122 | fixed(iconNetTX, txNow-txOld), 123 | ) 124 | } 125 | 126 | // colored surrounds the percentage with color escapes if it is outside of a 127 | // formatable range or urgent is true or warn is true. 128 | func colored(icon string, percentage int, urgent, warn bool) string { 129 | if percentage >= 1000 { 130 | return fmt.Sprintf(" %sHI%s%s", color[red], color[reset], icon) 131 | } else if percentage < 0 { 132 | return fmt.Sprintf("%sNEG%s%s", color[red], color[reset], icon) 133 | } else if urgent { 134 | return fmt.Sprintf("%3d%s%s", percentage, color[red], icon) 135 | } else if warn { 136 | return fmt.Sprintf("%3d%s%s", percentage, color[yellow], icon) 137 | } 138 | return fmt.Sprintf("%3d%s", percentage, icon) 139 | } 140 | 141 | // updatePower reads the current battery and power plug status 142 | func updatePower() string { 143 | const powerSupply = "/sys/class/power_supply/" 144 | var enFull, enNow int = 0, 0 145 | var plugged, err = ioutil.ReadFile(powerSupply + "AC/online") 146 | if err != nil { 147 | return color[red] + "ERR" + color[reset] + iconPowerBattery 148 | } 149 | batts, err := ioutil.ReadDir(powerSupply) 150 | if err != nil { 151 | return color[red] + "ERR" + color[reset] + iconPowerBattery 152 | } 153 | 154 | readval := func(name, field string) int { 155 | var path = powerSupply + name + "/" 156 | var file []byte 157 | if tmp, err := ioutil.ReadFile(path + "energy_" + field); err == nil { 158 | file = tmp 159 | } else if tmp, err := ioutil.ReadFile(path + "charge_" + field); err == nil { 160 | file = tmp 161 | } else { 162 | return 0 163 | } 164 | 165 | if ret, err := strconv.Atoi(strings.TrimSpace(string(file))); err == nil { 166 | return ret 167 | } 168 | return 0 169 | } 170 | 171 | for _, batt := range batts { 172 | name := batt.Name() 173 | if !strings.HasPrefix(name, "BAT") { 174 | continue 175 | } 176 | 177 | enFull += readval(name, "full") 178 | enNow += readval(name, "now") 179 | } 180 | 181 | if enFull == 0 { // Battery found but no readable full file. 182 | return color[red] + "ERR" + color[reset] + iconPowerBattery 183 | } 184 | 185 | p := enNow * 100 / enFull 186 | var icon = iconPowerBattery 187 | if string(plugged) == "1\n" { 188 | icon = iconPowerCharging 189 | } 190 | 191 | return colored(icon, p, p<=10, p<=20) 192 | } 193 | 194 | // updateVolume reads the volume from pulseaudio 195 | func updateVolume() string { 196 | cmd := exec.Command("pactl", "list", "sinks") 197 | cmd.Env = append(os.Environ(), "LC_ALL=C") 198 | out, err := cmd.Output() 199 | if err != nil { 200 | fmt.Println(err) 201 | return color[red] + "ERR" + color[reset] + iconVolume 202 | } 203 | scanner := bufio.NewScanner(bytes.NewBuffer(out)) 204 | chanCount := 0 205 | volSum := 0 206 | icon := iconVolumeMuted 207 | for scanner.Scan() { 208 | line := scanner.Text() 209 | if unmutedLine.MatchString(line) { 210 | icon = iconVolume 211 | } 212 | if !volumeLine.MatchString(line) { 213 | continue 214 | } 215 | m := channelVolume.FindAllString(line, -1) 216 | for _, c := range m { 217 | var v int 218 | if _, err := fmt.Sscanf(c, "%d%%", &v); err == nil { 219 | chanCount++ 220 | volSum += v 221 | } 222 | } 223 | } 224 | if err := scanner.Err(); err != nil || chanCount == 0 { 225 | return color[red] + "ERR" + color[reset] + iconVolume 226 | } 227 | 228 | p := volSum/chanCount 229 | return colored(icon, p, p>100, p>=90) 230 | } 231 | 232 | // updateCPUUse reads the last minute sysload and scales it to the core count 233 | func updateCPUUse() string { 234 | var load float32 235 | var loadavg, err = ioutil.ReadFile("/proc/loadavg") 236 | if err != nil { 237 | return color[red] + "ERR" + color[reset] + iconCPU 238 | } 239 | _, err = fmt.Sscanf(string(loadavg), "%f", &load) 240 | if err != nil { 241 | return color[red] + "ERR" + color[reset] + iconCPU 242 | } 243 | p := int(load*100.0/float32(cores)) 244 | return colored(iconCPU, p, p>=100, p>=70) 245 | } 246 | 247 | // updateMemUse reads the memory used by applications and scales to [0, 100] 248 | func updateMemUse() string { 249 | var file, err = os.Open("/proc/meminfo") 250 | if err != nil { 251 | return color[red] + "ERR" + color[reset] + iconMemory 252 | } 253 | defer file.Close() 254 | 255 | // done must equal the flag combination (0001 | 0010 | 0100 | 1000) = 15 256 | var total, used, done = 0, 0, 0 257 | for info := bufio.NewScanner(file); done != 15 && info.Scan(); { 258 | var prop, val = "", 0 259 | if _, err = fmt.Sscanf(info.Text(), "%s %d", &prop, &val); err != nil { 260 | return color[red] + "ERR" + color[reset] + iconMemory 261 | } 262 | switch prop { 263 | case "MemTotal:": 264 | total = val 265 | used += val 266 | done |= 1 267 | case "MemFree:": 268 | used -= val 269 | done |= 2 270 | case "Buffers:": 271 | used -= val 272 | done |= 4 273 | case "Cached:": 274 | used -= val 275 | done |= 8 276 | } 277 | } 278 | p := used*100/total 279 | return colored(iconMemory, p, p>=95, p>=70) 280 | } 281 | 282 | // main updates the dwm statusbar every second 283 | func main() { 284 | for { 285 | status := []string{ 286 | "", 287 | updateNetUse(), 288 | updateCPUUse(), 289 | updateMemUse(), 290 | updatePower(), 291 | updateVolume(), 292 | time.Now().Local().Format("Mon 02 " + iconDateTime + " 15:04:05"), 293 | } 294 | s := strings.Join(status, color[reset]+fieldSeparator) 295 | exec.Command("xsetroot", "-name", s).Run() 296 | 297 | // sleep until beginning of next second 298 | var now = time.Now() 299 | time.Sleep(now.Truncate(time.Second).Add(time.Second).Sub(now)) 300 | } 301 | } 302 | --------------------------------------------------------------------------------