talk is going to be published. Code comments, CLI output, error messages,
daemon log lines, MCP descriptions and results, test names and assertions,
README, spec, skill, build scripts and flake are now English. Only the
spoken announcement texts stay bilingual: the wrap result and the test
sentence are German or English depending on the detected language.
Language fallback is now configurable: `lang = "en" | "de"` (env
TALK_LANG, default en) is used when detection cannot tell (numbers, single
words, ties) and for talk's own announcements. langdetect.Detect takes the
fallback as an argument instead of hard-coding German. `talk setup` asks for
it; `talk doctor` and `talk status` show it.
Vocabulary pinned across packages so the process-boundary tests observe
stable strings: chain log lines ("chain: tier X skipped (not available)",
"falling back to the next tier", "spoken via X"), queue lines ("discarded:
quiet hours ... active", "replaced by toast", "max_age"), CLI notes
("speaking directly", "daemon started at", "no daemon", "nothing to cancel"),
status block with aligned English labels, MCP messages, origin guard 403.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
81 lines
2.0 KiB
Go
81 lines
2.0 KiB
Go
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
|
|
}
|