rpcfetch: add template program

Since documentation does not yet exist a template program is added showcasing the intended usage of this library. A configuration interface will be added in a future commit.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
Ophestra Umiker 2024-06-20 22:19:08 +09:00
parent 802ad250ca
commit 2c859c1a46
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
4 changed files with 275 additions and 0 deletions

112
fetch/apply.go Normal file
View File

@ -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
}

74
fetch/apply_linux.go Normal file
View File

@ -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])
}
}
}
}

26
fetch/config.go Normal file
View File

@ -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",
},
},
}

63
fetch/main.go Normal file
View File

@ -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()
}
}