package config import ( "errors" "fmt" "os" "path/filepath" "github.com/BurntSushi/toml" ) // Erlaubte Werte für audio.sink (Teil des öffentlichen Vertrags; Spec: // „Audio-Sink wählbar inkl. null"). const ( SinkOto = "oto" SinkNull = "null" ) // Config ist der effektiv geladene Konfigurations-Stand von talk. Task 02 // führt nur audio.sink; spätere Tasks ergänzen Felder (Provider-Endpunkte, // Voice-Mapping, server, queue …), ohne die Auflösungsreihenfolge zu ändern. type Config struct { Audio Audio `toml:"audio"` } // Audio steuert die Audio-Ausgabe. type Audio struct { // Sink wählt die Wiedergabe: "oto" (embedded Playback, Default) oder // "null" (verwirft — CI, Server, Tests). Sink string `toml:"sink"` } // Defaults liefert die eingebaute Default-Konfiguration (unterste Schicht // der Auflösung). func Defaults() Config { return Config{Audio: Audio{Sink: SinkOto}} } // Options steuert Load. Alle Felder sind injizierbar, damit die Unit-Tests // ohne echte HOME-/Env-Umgebung und ohne echte Dateien auskommen. type Options struct { // File überschreibt den platform-nativen Pfad der config.toml (Tests // injizieren hier eine Temp-Datei). Leer: Dir() + "config.toml". File string // Getenv liefert Umgebungsvariablen (Default: os.Getenv). Über TALK_* // laufen die Env-Overrides sowie die Pfadauflösung (HOME, // XDG_CONFIG_HOME, APPDATA). Getenv func(string) string } // Load löst die Konfiguration schichtweise auf: Defaults → config.toml // (falls vorhanden) → Env-Overrides (TALK_AUDIO_SINK schlägt die Datei). // Env- und Datei-Validierungsfehler werden als klarer Fehler gemeldet, nie // still ignoriert. Fehlt das Config-Verzeichnis (z. B. keine HOME-Variable), // läuft talk mit Defaults + Env weiter — die Datei ist optional, kaputte // Umgebungen dürfen talk nicht unbrauchbar machen. func Load(opts Options) (Config, error) { if opts.Getenv == nil { opts.Getenv = os.Getenv } cfg := Defaults() path := opts.File if path == "" { dir, err := Dir(opts.Getenv) if err != nil { // Kein auffindbares Config-Verzeichnis: optionale Datei entfällt. return applyEnv(cfg, opts.Getenv) } path = filepath.Join(dir, fileName) } if _, err := toml.DecodeFile(path, &cfg); err != nil { if !errors.Is(err, os.ErrNotExist) { return cfg, fmt.Errorf("config %s: %w", path, err) } } return applyEnv(cfg, opts.Getenv) } // applyEnv wendet die Env-Overrides an und validiert das Ergebnis. func applyEnv(cfg Config, getenv func(string) string) (Config, error) { if v := getenv("TALK_AUDIO_SINK"); v != "" { cfg.Audio.Sink = v } switch cfg.Audio.Sink { case SinkOto, SinkNull: return cfg, nil default: return cfg, fmt.Errorf("audio.sink: ungültiger Wert %q (erlaubt: %q, %q)", cfg.Audio.Sink, SinkOto, SinkNull) } }