diff --git a/fetch/apply.go b/fetch/apply.go new file mode 100644 index 0000000..b6aa689 --- /dev/null +++ b/fetch/apply.go @@ -0,0 +1,112 @@ +package main + +import ( + "encoding/gob" + "errors" + "fmt" + "log" + "os" + "path" + "strconv" + "strings" + "time" + + "git.ophivana.moe/cat/rpcfetch" +) + +var ( + pidS = strconv.Itoa(os.Getpid()) + launchTime = time.Now().Unix() +) + +var substitute = map[string]func(s *applyState) string{ + "pid": func(_ *applyState) string { + return pidS + }, + "1min": func(s *applyState) string { + s.populateLoadavg() + return s.loadavg[0] + }, + "5min": func(s *applyState) string { + s.populateLoadavg() + return s.loadavg[1] + }, + "15min": func(s *applyState) string { + s.populateLoadavg() + return s.loadavg[2] + }, + "used": func(s *applyState) string { + s.populateMem() + return fmt.Sprintf("%.1f GiB", float64(s.mem[1]-s.mem[0])/(1<<20)) + }, + + "total": func(s *applyState) string { + s.populateMem() + return fmt.Sprintf("%.1f GiB", float64(s.mem[1])/(1<<20)) + }, +} + +type applyState struct { + loadavg *[3]string + mem *[2]int +} + +func (s *applyState) replace(t string) string { + for k, f := range substitute { + t = strings.ReplaceAll(t, "%"+k, f(s)) + } + return t +} + +func apply() { + act := *conf.Activity + s := &applyState{} + + act.State = s.replace(act.State) + act.Details = s.replace(act.Details) + act.Assets = &rpcfetch.ActivityAssets{ + LargeImage: act.Assets.LargeImage, + LargeText: s.replace(act.Assets.LargeText), + SmallImage: act.Assets.SmallImage, + SmallText: s.replace(act.Assets.SmallText), + } + + if nonce, err := retry(&act); err != nil { + log.Fatalf("error setting activity: %s", err) + } else { + log.Printf("activity updated with nonce %s", nonce) + } +} + +func save() { + nf, err := os.CreateTemp(path.Dir(confPath), ".rpcfetch.conf.*") + if err != nil { + log.Fatalf("error creating temporary configuration file: %s", err) + } + + if err = gob.NewEncoder(nf).Encode(defaultConfig); err != nil { + fmt.Printf("error writing configuration file: %s\n", err) + os.Exit(1) + } + + if err = nf.Close(); err != nil { + log.Fatalf("error closing temporary configuration file: %s", err) + } + + if err = os.Rename(nf.Name(), confPath); err != nil { + log.Fatalf("error renaming configuration file: %s", err) + } + log.Printf("saved configuration to %s", confPath) +} + +func retry(act *rpcfetch.Activity) (string, error) { +try: + nonce, err := d.SetActivity(act) + if errors.Is(err, rpcfetch.ErrAgain) { + log.Println("retrying in 5 seconds...") + time.Sleep(5 * time.Second) + goto try + } + + return nonce, err +} diff --git a/fetch/apply_linux.go b/fetch/apply_linux.go new file mode 100644 index 0000000..c1c3d29 --- /dev/null +++ b/fetch/apply_linux.go @@ -0,0 +1,74 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "strconv" + "strings" +) + +func (s *applyState) populateLoadavg() { + if s.loadavg != nil { + return + } + + if b, err := os.ReadFile("/proc/loadavg"); err != nil { + log.Printf("error reading loadavg: %s", err) + } else { + res := strings.SplitN(string(b), " ", 4) + if len(res) >= 3 { + s.loadavg = (*[3]string)(res[:3]) + return + } + log.Printf("unexpected loadavg raw: %s", string(b)) + } + + s.loadavg = &[3]string{"?", "?", "?"} +} + +func (s *applyState) populateMem() { + if s.mem != nil { + return + } + + // failure defaults + s.mem = &[2]int{-1, -1} + + if f, err := os.Open("/proc/meminfo"); err != nil { + log.Printf("error reading meminfo: %s", err) + } else { + defer func() { + if err = f.Close(); err != nil { + log.Fatalf("error closing meminfo: %s", err) + } + }() + + p := bufio.NewScanner(f) + for p.Scan() { + res := strings.SplitN(strings.ReplaceAll(p.Text(), " ", ""), ":", 2) + if len(res) != 2 { + fmt.Printf("unexpected meminfo line: %s", p.Text()) + return + } + + format := func(r string) int { + var v int + if v, err = strconv.Atoi(r[:len(r)-2]); err != nil { + log.Printf("error parsing meminfo value: %s", err) + v = -1 + } + + return v + } + + switch res[0] { + case "MemFree": + s.mem[0] = format(res[1]) + case "MemTotal": + s.mem[1] = format(res[1]) + } + } + } +} diff --git a/fetch/config.go b/fetch/config.go new file mode 100644 index 0000000..d3237ed --- /dev/null +++ b/fetch/config.go @@ -0,0 +1,26 @@ +package main + +import ( + "git.ophivana.moe/cat/rpcfetch" +) + +type config struct { + ID string + Activity *rpcfetch.Activity +} + +// sample config so the program works out of the box +var defaultConfig = config{ + ID: "1252927154480611351", + Activity: &rpcfetch.Activity{ + State: "%used / %total", + Details: "%1min %5min %15min", + Timestamps: &rpcfetch.ActivityTimestamps{Start: &launchTime}, + Assets: &rpcfetch.ActivityAssets{ + LargeImage: "yorha", + LargeText: "1252927154480611351", + SmallImage: "flan", + SmallText: "PID: %pid", + }, + }, +} diff --git a/fetch/main.go b/fetch/main.go new file mode 100644 index 0000000..24e0d53 --- /dev/null +++ b/fetch/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "encoding/gob" + "flag" + "fmt" + "log" + "os" + "time" + + "git.ophivana.moe/cat/rpcfetch" +) + +var ( + d *rpcfetch.Client + + conf config + confPath string +) + +func init() { + flag.StringVar(&confPath, "conf", "rpcfetch.conf", "path to rpcfetch configuration file") +} + +func main() { + flag.Parse() + + if cf, err := os.Open(confPath); err != nil { + if !os.IsNotExist(err) { + fmt.Printf("error opening configuration file: %s\n", err) + os.Exit(1) + } + + // use defaults + log.Print("configuration file does not exist, using defaults") + conf = defaultConfig + } else { + // decode from existing file + if err = gob.NewDecoder(cf).Decode(&conf); err != nil { + fmt.Printf("error reading configuration: %s\n", err) + os.Exit(1) + } + if err = cf.Close(); err != nil { + log.Fatalf("error closing configuration file: %s", err) + } + } + + d = rpcfetch.New(conf.ID) + defer func() { + if err := d.Close(); err != nil { + log.Printf("error closing client: %s", err) + } + }() + + // restore activity from configuration + apply() + + // update periodically + for { + time.Sleep(5 * time.Second) + apply() + } +}