From 12f462d70d9a0ae90fe69212d316c0ac878c71f5 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Sat, 18 Aug 2018 18:34:53 +0200 Subject: [PATCH] Initial version --- .gitignore | 1 + config.yml | 42 +++++++++++++++++ entry.go | 94 +++++++++++++++++++++++++++++++++++++ main.go | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 272 insertions(+) create mode 100644 .gitignore create mode 100644 config.yml create mode 100644 entry.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51e6cf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tmux-collector diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..53fb7ba --- /dev/null +++ b/config.yml @@ -0,0 +1,42 @@ +--- + +# Must match background color of bar or will look weird +base_bg_color: colour235 + +# Segments to display (first = left, last = right) +segments: + + # Arch Linux: Check for Updates + - background_success: colour88 + foreground_success: colour251 + command: [bash, -c, 'echo "$(checkupdates | wc -l) Updates" | grep -v "^0 Updates" || true'] + interval: 10m + + # Dropbox CLI: Check daemon status + - background_success: colour19 + foreground_success: colour251 + command: [bash, -c, 'dropbox.py status | head -n1 | grep -v "Up to date" || true'] + interval: 30s + prefix: "" + + # System: If present display battery status + - background_success: colour195 + foreground_success: colour237 + command: [tmux-battery] + + # System: Display a stable subset of the uptime + - background_success: colour34 + foreground_success: colour232 + command: [bash, -c, 'uptime | cut -f 4-5 -d " " | cut -f 1 -d ","'] + + # Time + - background_success: colour235 + foreground_success: colour250 + command: ['date', '+%a, %H:%M:%S'] + + # Date + - background_success: colour235 + foreground_success: colour69 + command: ['date', '+%Y-%m-%d (KW %V)'] + +... diff --git a/entry.go b/entry.go new file mode 100644 index 0000000..bac9a1b --- /dev/null +++ b/entry.go @@ -0,0 +1,94 @@ +package main + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "os" + "path" + "strings" + "time" + + homedir "github.com/mitchellh/go-homedir" +) + +type config struct { + BaseBgColor string `yaml:"base_bg_color" json:"base_bg_color"` + Segments []*segment `yaml:"segments" json:"segments"` +} + +type segment struct { + BackgroundError string `yaml:"background_error" json:"background_error"` + BackgroundSuccess string `yaml:"background_success" json:"background_success"` + ForegroundError string `yaml:"foreground_error" json:"foreground_error"` + ForegroundSuccess string `yaml:"foreground_success" json:"foreground_success"` + Command []string `yaml:"command" json:"command"` + Prefix string `yaml:"prefix" json:"prefix"` + Interval time.Duration `yaml:"interval" json:"interval"` + + Output string + Err error +} + +func (s segment) cacheKey() string { + return fmt.Sprintf("%x.json", sha256.Sum256([]byte(strings.Join(s.Command, " ")))) +} + +func (s segment) storeCache() error { + if s.Interval == 0 { + // No interval defined = execute all the time, no caching + return nil + } + + p, err := homedir.Expand(path.Join("~", ".cache", "tmux-collector", s.cacheKey())) + if err != nil { + return err + } + + if err = os.MkdirAll(path.Dir(p), 0700); err != nil { + return err + } + + fh, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer fh.Close() + + return json.NewEncoder(fh).Encode(s) +} + +func (s *segment) loadCache() (bool, error) { + if s.Interval == 0 { + // No interval defined = execute all the time, no caching + return false, nil + } + + p, err := homedir.Expand(path.Join("~", ".cache", "tmux-collector", s.cacheKey())) + if err != nil { + return false, err + } + + stat, err := os.Stat(p) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + if time.Since(stat.ModTime()) > s.Interval { + return false, nil + } + + fh, err := os.Open(p) + if err != nil { + return false, err + } + defer fh.Close() + + if err = json.NewDecoder(fh).Decode(s); err != nil { + return false, err + } + + return true, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..1cdc40f --- /dev/null +++ b/main.go @@ -0,0 +1,135 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "strings" + "sync" + + "github.com/Luzifer/rconfig" + log "github.com/sirupsen/logrus" + yaml "gopkg.in/yaml.v2" +) + +var ( + cfg = struct { + Config string `flag:"config,c" default:"config.yml" description:"Configuration file"` + LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"` + VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"` + }{} + + version = "dev" +) + +func init() { + if err := rconfig.ParseAndValidate(&cfg); err != nil { + log.Fatalf("Unable to parse commandline options: %s", err) + } + + if cfg.VersionAndExit { + fmt.Printf("tmux-collector %s\n", version) + os.Exit(0) + } + + if l, err := log.ParseLevel(cfg.LogLevel); err != nil { + log.WithError(err).Fatal("Unable to parse log level") + } else { + log.SetLevel(l) + } +} + +func main() { + conf, err := loadConfig() + if err != nil { + log.WithError(err).Fatal("Unable to load config") + } + + wg := &sync.WaitGroup{} + wg.Add(len(conf.Segments)) + for _, seg := range conf.Segments { + go executeSegment(seg, wg) + } + wg.Wait() + + lastBgColor := conf.BaseBgColor + output := []string{} + + for _, seg := range conf.Segments { + bg := seg.BackgroundSuccess + fg := seg.ForegroundSuccess + + if seg.Err != nil { + if seg.BackgroundError != "" { + bg = seg.BackgroundError + } + if seg.ForegroundError != "" { + fg = seg.ForegroundError + } + + if strings.TrimSpace(seg.Output) == "" { + seg.Output = seg.Err.Error() + } + } + + if strings.TrimSpace(seg.Output) == "" { + continue + } + + if seg.Prefix != "" { + seg.Output = strings.Join([]string{seg.Prefix, seg.Output}, " ") + } + + if lastBgColor != bg { + output = append(output, fmt.Sprintf(" #[fg=%s,bg=%s]#[fg=%s,bg=%s] ", + bg, lastBgColor, fg, bg, + )) + } else { + output = append(output, fmt.Sprintf("#[fg=%s,bg=%s] ", + fg, bg, + )) + } + output = append(output, seg.Output) + lastBgColor = bg + } + + fmt.Print(strings.Join(output, "")) +} + +func executeSegment(seg *segment, wg *sync.WaitGroup) { + defer wg.Done() + + loaded, err := seg.loadCache() + if err != nil { + log.WithError(err).Error("Unable to load cache") + } + if loaded { + log.WithField("command", strings.Join(seg.Command, " ")).Debug("Loaded from cache") + return + } + + buf := new(bytes.Buffer) + + cmd := exec.Command(seg.Command[0], seg.Command[1:]...) + cmd.Stdout = buf + + seg.Err = cmd.Run() + seg.Output = strings.Split(buf.String(), "\n")[0] + log.WithField("command", strings.Join(seg.Command, " ")).Debug("Freshly loaded") + + if err = seg.storeCache(); err != nil { + log.WithError(err).Error("Unable to store cache") + } +} + +func loadConfig() (*config, error) { + f, err := os.Open(cfg.Config) + if err != nil { + return nil, err + } + defer f.Close() + + out := &config{} + return out, yaml.NewDecoder(f).Decode(out) +}