package queue import ( "errors" "os" "path/filepath" "strings" ) // MuteFileName is the mute file's name inside the config directory. const MuteFileName = "mute" // ErrNoMutePath reports a Set attempt without a configured file path (config // directory unknown; the daemon then runs without mute and warns at start). var ErrNoMutePath = errors.New("mute: no file path configured (config directory unknown)") // MuteStore manages the persistent mute state as a file in the config // directory: content "true" = muted, anything else (or no file) = not muted. // // The daemon re-reads the file on every /speak request, so a state written // by the CLI while no daemon runs takes effect immediately, and the state // survives daemon restarts. type MuteStore struct { // Path is the mute file's full path. Empty (no config directory): Muted // is always false and Set an error. Path string } // Muted reports the persistent mute state. A missing or unreadable file // means not muted: a broken setup must never be "accidentally silent". func (s MuteStore) Muted() bool { if s.Path == "" { return false } b, err := os.ReadFile(s.Path) if err != nil { return false } return strings.TrimSpace(string(b)) == "true" } // Set writes the mute state atomically (temp file + rename, so daemon and // CLI never read a half-written file) and creates the config directory if // needed. func (s MuteStore) Set(muted bool) error { if s.Path == "" { return ErrNoMutePath } dir := filepath.Dir(s.Path) if err := os.MkdirAll(dir, 0o755); err != nil { return err } content := "false\n" if muted { content = "true\n" } tmp, err := os.CreateTemp(dir, ".mute-*") if err != nil { return err } name := tmp.Name() if _, err := tmp.WriteString(content); err != nil { tmp.Close() os.Remove(name) return err } if err := tmp.Close(); err != nil { os.Remove(name) return err } if err := os.Chmod(name, 0o644); err != nil { os.Remove(name) return err } if err := os.Rename(name, s.Path); err != nil { os.Remove(name) return err } return nil }