// Package config loads lectio's TOML configuration file. // // Resolution order: LECTIO_CONFIG env var -> ~/.config/lectio/config.toml // (auto-seeded from the embedded default on first run, honoring // XDG_CONFIG_HOME via os.UserConfigDir()) -> built-in defaults. TOML that // fails to parse falls back to defaults with a warning on stderr. package config import ( _ "embed" "fmt" "os" "path/filepath" "strings" "github.com/pelletier/go-toml/v2" ) // seedTOML is the embedded default config.toml, written to disk on first // run and parsed as the fallback default values. // //go:embed config.toml var seedTOML []byte // validVersions are the five scripture versions lectio understands. var validVersions = map[string]bool{ "pl": true, "wuj": true, "vul": true, "grb": true, "drb": true, } // validDisplays are the lectio-web reading-pane layouts. var validDisplays = map[string]bool{ "horizontal": true, "vertical": true, "interlinear": true, } // NormalizeDisplay lower-cases display and falls back to "horizontal" when // it is not one of validDisplays -- the same lenient style as the rest of // lectio-web's config fields (no error, just a safe default). Exported so // internal/web can apply the identical normalization to an explicit // ?display= query override, keeping the valid set in this one place. func NormalizeDisplay(display string) string { d := strings.ToLower(display) if !validDisplays[d] { return "horizontal" } return d } // Config holds lectio's user-configurable settings. type Config struct { SchemaVersion int `toml:"schema_version"` Lectionary string `toml:"lectionary"` TraditionalLang string `toml:"traditional_lang"` Versions []string `toml:"versions"` DefaultVersion string `toml:"default_version"` Width int `toml:"width"` All bool `toml:"all"` Offline bool `toml:"offline"` WebTheme string `toml:"web_theme"` WebPort int `toml:"web_port"` WebDisplay string `toml:"web_display"` Parts map[string]map[string]bool `toml:"parts"` } // PartShown reports whether a part renders: true unless explicitly set false. func (c Config) PartShown(lectionary, partID string) bool { if m, ok := c.Parts[lectionary]; ok { if v, ok := m[partID]; ok { return v } } return true } // Default returns lectio's built-in configuration, used when no config file // is found and as the base that a partial config file overrides. func Default() Config { return Config{ SchemaVersion: 1, Lectionary: "new", TraditionalLang: "pl", Versions: []string{"pl", "wuj", "vul", "grb", "drb"}, DefaultVersion: "pl", Width: 0, All: false, Offline: false, WebTheme: "transfiguration", WebPort: 0, WebDisplay: "horizontal", Parts: nil, } } // configPath resolves the config file location: LECTIO_CONFIG if set, // otherwise the default XDG-aware path under os.UserConfigDir(). The bool // reports whether this is the default (seedable) location. func configPath() (path string, isDefault bool, err error) { if p := os.Getenv("LECTIO_CONFIG"); p != "" { return p, false, nil } dir, err := os.UserConfigDir() if err != nil { return "", false, err } return filepath.Join(dir, "lectio", "config.toml"), true, nil } // seedIfMissing writes the embedded default config to path if nothing is // there yet. func seedIfMissing(path string) error { if _, err := os.Stat(path); err == nil { return nil } else if !os.IsNotExist(err) { return err } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } return os.WriteFile(path, seedTOML, 0o644) } // Load reads lectio's configuration, following the package's resolution // order (see the package doc comment). It always returns a usable Config: // on a missing config dir or a TOML parse error it falls back to Default() // (warning on stderr in the parse-error case). It returns a non-nil error // only when the loaded config contains a semantically invalid value. func Load() (Config, error) { def := Default() path, isDefault, err := configPath() if err != nil { fmt.Fprintf(os.Stderr, "lectio: warning: could not resolve config directory: %v; using defaults\n", err) return def, nil } if isDefault { if err := seedIfMissing(path); err != nil { return def, err } } data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return def, nil } return def, err } cfg := def if err := toml.Unmarshal(data, &cfg); err != nil { fmt.Fprintf(os.Stderr, "lectio: warning: invalid config at %s: %v; using defaults\n", path, err) return def, nil } cfg.WebDisplay = NormalizeDisplay(cfg.WebDisplay) if err := validate(cfg); err != nil { return Config{}, err } return cfg, nil } // validate checks the semantic constraints Load enforces beyond what TOML // unmarshaling itself catches. func validate(cfg Config) error { if cfg.Lectionary != "new" && cfg.Lectionary != "traditional" { return fmt.Errorf("config: invalid lectionary %q (must be \"new\" or \"traditional\")", cfg.Lectionary) } if cfg.TraditionalLang != "pl" && cfg.TraditionalLang != "en" { return fmt.Errorf("config: invalid traditional_lang %q (must be \"pl\" or \"en\")", cfg.TraditionalLang) } for _, v := range cfg.Versions { if !validVersions[v] { return fmt.Errorf("config: invalid version %q in versions (must be one of pl, wuj, vul, grb, drb)", v) } } if !validVersions[cfg.DefaultVersion] { return fmt.Errorf("config: invalid default_version %q (must be one of pl, wuj, vul, grb, drb)", cfg.DefaultVersion) } return nil }