Carry the power draw to the bar over the bus, not through a file (hq ADR 0260)
Modules talk over the bus. The power module's bundle puts each fresh reading of its own sampler in its state draw, keyed by the machine; the bar's block shows that state, which the bar's bundle watches under the read the mesh grants it. A new catalogue check refuses a module naming another module's path under /run or /var/lib.
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
// The values other modules keep on the bus for a block they contributed (novox/hq ADR 0260).
|
||||
//
|
||||
// A block that `shows` a state is rendered as a custom block running `i3status-value <state> <key>`.
|
||||
// i3status-rs cannot reach the bus, and this bundle can: the node's runtime launched it, and the mesh
|
||||
// granted this module a read of every state the modules on its machine offer the bar. So it watches each
|
||||
// state its bars name and leaves the value's text in this module's own directory, where the block's
|
||||
// command reads it. No file of another module is read, and nothing but the bus passes between them.
|
||||
|
||||
// valueCall is how a bar names a value: the command the template renders.
|
||||
var valueCall = regexp.MustCompile(`i3status-value ([a-z0-9][a-z0-9-]*\.[a-z0-9][a-z0-9-]*) ([^\s"]+)`)
|
||||
|
||||
// Wanted is one state and key a bar shows.
|
||||
type Wanted struct{ State, Key string }
|
||||
|
||||
func (w Wanted) file() string { return w.State + "@" + w.Key }
|
||||
|
||||
// wantedIn is every value the bars' files name, each once, in order.
|
||||
func wantedIn(files []string) []Wanted {
|
||||
seen := map[Wanted]bool{}
|
||||
for _, f := range files {
|
||||
raw, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, m := range valueCall.FindAllStringSubmatch(string(raw), -1) {
|
||||
seen[Wanted{State: m[1], Key: m[2]}] = true
|
||||
}
|
||||
}
|
||||
out := make([]Wanted, 0, len(seen))
|
||||
for w := range seen {
|
||||
out = append(out, w)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].file() < out[j].file() })
|
||||
return out
|
||||
}
|
||||
|
||||
// valuesDir is where the values are left: the account's runtime directory, gone at logout.
|
||||
func valuesDir() string {
|
||||
return filepath.Join(runUserDir, strconv.Itoa(os.Getuid()), "i3status-rust", "values")
|
||||
}
|
||||
|
||||
// keep writes one change of a value to its file: the value's `text`, or no file once it is deleted.
|
||||
func keep(dir string, w Wanted, c stdio.StateChange) error {
|
||||
path := filepath.Join(dir, w.file())
|
||||
if c.Op == "delete" || c.Op == "purge" {
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var v struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Value, &v); err != nil {
|
||||
return fmt.Errorf("%s's value for %s has no text: %w", w.State, w.Key, err)
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := path + ".new"
|
||||
if err := os.WriteFile(tmp, []byte(v.Text+"\n"), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
// Watcher is a function that watches one key of one state; the SDK's in the bundle, a fake in a test.
|
||||
type Watcher func(state, key string, handle func(stdio.StateChange) error) error
|
||||
|
||||
// followValues watches every value the bars name, starting the ones not yet watched each time it is
|
||||
// called; one the runtime refuses — a read not granted yet — is tried again the next time.
|
||||
func followValues(files []string, dir string, watching map[Wanted]bool, watch Watcher) []string {
|
||||
var said []string
|
||||
for _, w := range wantedIn(files) {
|
||||
if watching[w] {
|
||||
continue
|
||||
}
|
||||
w := w
|
||||
err := watch(w.State, w.Key, func(c stdio.StateChange) error { return keep(dir, w, c) })
|
||||
if err != nil {
|
||||
said = append(said, fmt.Sprintf("cannot watch %s %s: %v", w.State, w.Key, err))
|
||||
continue
|
||||
}
|
||||
watching[w] = true
|
||||
}
|
||||
return said
|
||||
}
|
||||
|
||||
// FollowValues runs followValues every half minute for as long as the bundle runs, so a block a push
|
||||
// adds is followed without a restart.
|
||||
func FollowValues() {
|
||||
watching := map[Wanted]bool{}
|
||||
watch := func(state, key string, handle func(stdio.StateChange) error) error {
|
||||
return stdio.State(state).Watch(key, handle)
|
||||
}
|
||||
for {
|
||||
files, _ := filepath.Glob(filepath.Join(configDir(), "*.toml"))
|
||||
for _, s := range followValues(files, valuesDir(), watching, watch) {
|
||||
fmt.Fprintln(os.Stderr, "i3status-rust:", s)
|
||||
}
|
||||
time.Sleep(30 * time.Second)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user