package main import ( "fmt" "net" "syscall" "strings" "strconv" "os" "os/exec" "os/signal" "io/ioutil" "time" "flag" "errors" "path/filepath" "github.com/BurntSushi/toml" "github.com/google/shlex" ) var config Config type MusicPlayer interface { nowPlaying() string status() int } type GenericPlayer struct { NowPlayingCmd string StatusCmd string } type MPD struct{ Host string Port int } type MOCP struct{} type Config struct { Sections []string Delay int Separator string IP IPConfig Mounts MountConfig Music MusicConfig Clock ClockConfig Battery BatteryConfig Hostname HostnameConfig } type IPConfig struct { Label string Interfaces []string IPv4Only bool `toml:"ipv4only"` } type MountConfig struct { Label string MountPoints []string `toml:"mountpoints"` } type MusicConfig struct { LabelPlaying string `toml:"labelplaying"` LabelPaused string `toml:"labelpaused"` LabelStopped string `toml:"labelstopped"` Type string Host string Port int NowPlayingCmd string `toml:"nowplayingcmd"` StatusCmd string `toml:"statuscmd"` } type ClockConfig struct { Label string Format string } type BatteryConfig struct { Label string SubLabel string `toml:"sublabel"` FullLabel string `toml:"fulllabel"` ChargingLabel string `toml:"charginglabel"` DischargingLabel string `toml:"discharginglabel"` UnknownStatusLabel string `toml:"unknownstatuslabel"` Batteries []string `toml:"batteries"` } type HostnameConfig struct { Label string } func diskSizes(config MountConfig) string { var b strings.Builder var statfs syscall.Statfs_t const div = float64(1024 * 1024 * 1024) const unit = "G" sep := "" for _, mount := range config.MountPoints { b.WriteString(sep) err := syscall.Statfs(mount, &statfs) if err != nil { b.WriteString(fmt.Sprintf("No such mount: %v", mount)) } else { availBytes := float64(statfs.Bavail * uint64(statfs.Bsize)) avail := strconv.FormatFloat(availBytes / div, 'f', 2, 64) b.WriteString(fmt.Sprintf("%v : %v %v", mount, avail, unit)) sep = " " } } return b.String() } func ips(config IPConfig) string { var b strings.Builder sep := "" for _, interfaceName := range config.Interfaces { b.WriteString(sep) b.WriteString(interfaceName) b.WriteString(": ") if len(config.Interfaces) > 1 { b.WriteString(" [ ") } b.WriteString(ip(interfaceName, config.IPv4Only)) if len(config.Interfaces) > 1 { b.WriteString(" ]") } sep = " " } return b.String() } func ip(interfaceName string, IPv4Only bool) string { var b strings.Builder sep := "" in, err := net.InterfaceByName(interfaceName) if err != nil { return "No such interface" } addrs, err := in.Addrs() if err != nil { panic(err) } for _, addr := range addrs { var ip net.IP switch v := addr.(type) { case *net.IPAddr: ip = v.IP case *net.IPNet: ip = v.IP default: } if IPv4Only { if ip.To4() != nil { b.WriteString(sep) b.WriteString(ip.String()) sep = " " } } else { b.WriteString(sep) b.WriteString(ip.String()) sep = " " } } return b.String() } func clock(format string) string { t := time.Now() return t.Format(format) } func (player GenericPlayer) nowPlaying() string { split, err := shlex.Split(player.NowPlayingCmd) if err != nil { panic(err) } if len(split) == 0 { panic(errors.New("Empty \"now playing\" command for generic player")) } var args []string if len(split) > 1 { args = append(args, split[1:]...) } cmd := exec.Command(split[0], args...) out, err := cmd.Output() if err != nil { return "" } sOut := string(out) sOut = strings.ReplaceAll(sOut, "\n", "") return sOut } func (player GenericPlayer) status() int { split, err := shlex.Split(player.StatusCmd) if err != nil { panic(err) } if len(split) == 0 { panic(errors.New("Empty \"status\" command for generic player")) } var args []string if len(split) > 1 { args = append(args, split[1:]...) } cmd := exec.Command(split[0], args...) out, err := cmd.Output() if err != nil { return 2 } sOut := string(out) sOut = strings.ReplaceAll(sOut, "\n", "") iOut, err := strconv.Atoi(sOut) if err != nil { panic(err) } return iOut } func (mpd MPD) nowPlaying() string { cmd := exec.Command("mpc", "-h", mpd.Host, "-p", strconv.Itoa(mpd.Port), "current") out, err := cmd.Output() if err != nil { panic(err) } sOut := string(out) sOut = strings.ReplaceAll(sOut, "\n", "") return sOut } // Returns 0 if playing, 1 if paused and 2 otherwise func (mpd MPD) status() int { cmd := exec.Command("mpc", "-h", mpd.Host, "-p", strconv.Itoa(mpd.Port), "status") out, err := cmd.Output() if err != nil { return -1 } sOut := string(out) if strings.Contains(sOut, "playing") { return 0 } else if strings.Contains(sOut, "paused") { return 1 } else { return 2 } } func (mocp MOCP) nowPlaying() string { cmd := exec.Command("mocp", "-Q", "%artist - %song") out, err := cmd.Output() if err != nil { panic(err) } sOut := string(out) sOut = strings.ReplaceAll(sOut, "\n", "") return sOut } // Returns 0 if playing, 1 if paused and 2 otherwise func (mocp MOCP) status() int { cmd := exec.Command("mocp", "-i") out, err := cmd.Output() if err != nil { return -1 } sOut := string(out) if strings.Contains(sOut, "PLAY") { return 0 } else if strings.Contains(sOut, "PAUSE") { return 1 } else { return 2 } } func readWithDefault(fpath string, def string) string { content, err := ioutil.ReadFile(fpath) if err != nil { content = []byte(def) } return strings.TrimSuffix(string(content), "\n") } func labelStatus(config BatteryConfig, status string) string { switch status { case "Full": if config.FullLabel != "" { return config.FullLabel } case "Charging": if config.ChargingLabel != "" { return config.ChargingLabel } case "Discharging": if config.DischargingLabel != "" { return config.DischargingLabel } default: if config.UnknownStatusLabel != "" { return config.UnknownStatusLabel } } return status } func battery(config BatteryConfig) string { var b strings.Builder const batteryPath = "/sys/class/power_supply" type Battery struct { nb int capacity string status string } var batteries []Battery batteryNb := 0 if config.Batteries == nil { files, err := ioutil.ReadDir(batteryPath) if err != nil { panic(err) } for _, file := range files { capPath := filepath.Join(batteryPath, file.Name(), "capacity") statusPath := filepath.Join(batteryPath, file.Name(), "status") batteries = append(batteries, Battery { nb: batteryNb, capacity: readWithDefault(capPath, "0"), status: labelStatus(config, readWithDefault(statusPath, "N/A")), }) batteryNb += 1 } } else { for _, battery := range config.Batteries { capPath := filepath.Join(batteryPath, battery, "capacity") statusPath := filepath.Join(batteryPath, battery, "status") batteries = append(batteries, Battery { nb: batteryNb, capacity: readWithDefault(capPath, "0"), status: labelStatus(config, readWithDefault(statusPath, "N/A")), }) batteryNb += 1 } } separator := "" for _, bat := range batteries { b.WriteString(separator) b.WriteString(fmt.Sprintf("%v%v: %v%% %v", config.SubLabel, bat.nb, bat.capacity, bat.status)) separator = " " } return b.String() } func buildClock(config ClockConfig) string { var b strings.Builder b.WriteString(config.Label) b.WriteString(clock(config.Format)) return b.String() } func buildIP(config IPConfig) string { var b strings.Builder b.WriteString(config.Label) b.WriteString("[ ") b.WriteString(ips(config)) b.WriteString(" ]") return b.String() } func buildMounts(config MountConfig) string { var b strings.Builder b.WriteString(config.Label) b.WriteString("[ ") b.WriteString(diskSizes(config)) b.WriteString(" ]") return b.String() } func buildMusic(config MusicConfig) string { var b strings.Builder var player MusicPlayer switch config.Type { case "mpd": host := config.Host if host == "" { host = "127.0.0.1" } port := config.Port if port == 0 { port = 6600 } player = MPD{Host: host, Port: port} case "mocp": player = MOCP{} case "generic": player = GenericPlayer{NowPlayingCmd: config.NowPlayingCmd, StatusCmd: config.StatusCmd} default: } exists := player != nil if !exists { return "Unknown music player type" } switch player.status() { case 0: b.WriteString(config.LabelPlaying) case 1: b.WriteString(config.LabelPaused) case 2: b.WriteString(config.LabelStopped) default: exists = false } if exists { b.WriteString(player.nowPlaying()) } return b.String() } func buildBattery(config BatteryConfig) string { var b strings.Builder b.WriteString(config.Label) b.WriteString("[ ") b.WriteString(battery(config)) b.WriteString(" ]") return b.String() } func buildHostname(config HostnameConfig) string { var b strings.Builder b.WriteString(config.Label) hostname, err := os.Hostname() if err != nil { panic(err) } b.WriteString(hostname) return b.String() } func getDefaultConfigPath() string { return "/etc/statusbar.conf" } func readConfig(configPath *string) ([]byte, error) { var content []byte var err error if configPath == nil { content, err = ioutil.ReadAll(os.Stdin) } else { if _, err := os.Stat(*configPath); err != nil { fmt.Fprintf(os.Stderr, "Config not found") content, err = ioutil.ReadAll(os.Stdin) } else { content, err = ioutil.ReadFile(*configPath) } } return content, err } func awaitSighup(configPath *string) { for { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGHUP) <-c fmt.Fprintf(os.Stderr, "Reloading config...") content, err := readConfig(configPath) if err != nil { panic(err) } _, err = toml.Decode(string(content), &config) if err != nil { panic(err) } } } func main() { configPath := flag.String("c", getDefaultConfigPath(), "The config path.") daemon := flag.Bool("d", false, "Whether to launch statusbar as a daemon process or not.") flag.Parse() if *configPath == "-" { configPath = nil } content, err := readConfig(configPath) if err != nil { panic(err) } //var config Config _, err = toml.Decode(string(content), &config) if err != nil { panic(err) } if *daemon { go awaitSighup(configPath) } for true { var b strings.Builder for i, section := range config.Sections { switch section { case "clock": b.WriteString(buildClock(config.Clock)) case "ip": b.WriteString(buildIP(config.IP)) case "mounts": b.WriteString(buildMounts(config.Mounts)) case "music": b.WriteString(buildMusic(config.Music)) case "battery": b.WriteString(buildBattery(config.Battery)) case "hostname": b.WriteString(buildHostname(config.Hostname)) default: } if i < len(config.Sections) - 1 { b.WriteString(config.Separator) } } fmt.Println(b.String()) time.Sleep(time.Duration(config.Delay) * time.Second) } }