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 `. // 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) } }