2026-10-18 17:10:26 +02:00
|
|
|
package queue
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// MuteFileName is the mute file's name inside the config directory.
|
2026-10-18 17:10:26 +02:00
|
|
|
const MuteFileName = "mute"
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// 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)")
|
2026-10-18 17:10:26 +02:00
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// MuteStore manages the persistent mute state as a file in the config
|
|
|
|
|
// directory: content "true" = muted, anything else (or no file) = not muted.
|
2026-10-18 17:10:26 +02:00
|
|
|
//
|
2026-10-24 11:15:35 +02:00
|
|
|
// 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.
|
2026-10-18 17:10:26 +02:00
|
|
|
type MuteStore struct {
|
2026-10-24 11:15:35 +02:00
|
|
|
// Path is the mute file's full path. Empty (no config directory): Muted
|
|
|
|
|
// is always false and Set an error.
|
2026-10-18 17:10:26 +02:00
|
|
|
Path string
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Muted reports the persistent mute state. A missing or unreadable file
|
|
|
|
|
// means not muted: a broken setup must never be "accidentally silent".
|
2026-10-18 17:10:26 +02:00
|
|
|
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"
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// 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.
|
2026-10-18 17:10:26 +02:00
|
|
|
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
|
|
|
|
|
}
|