// 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 // Version is lectio's release version, shared by every binary's // -v/--version output (lectio, lectio-ui, lectio-web). const Version = "0.7.0" // validVersions are the five scripture versions lectio understands. var validVersions = map[string]bool{ "bt": true, "wuj": true, "vul": true, "grb": true, "drb": true, } // ValidVersion reports whether v is one of the five scripture versions // lectio understands (bt, wuj, vul, grb, drb). func ValidVersion(v string) bool { return validVersions[v] } // NormalizeLectionary maps -l/--lectionary's accepted spellings ("new", // "trad", "traditional") onto the canonical Config.Lectionary values ("new", // "traditional"); anything else reports ok=false. func NormalizeLectionary(s string) (string, bool) { switch strings.ToLower(s) { case "new": return "new", true case "trad", "traditional": return "traditional", true default: return "", false } } // 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 } // validUILanguages are the two UI chrome languages lectio understands. var validUILanguages = map[string]bool{ "en": true, "pl": true, } // NormalizeUILanguage lower-cases lang and falls back to "en" when it is not // one of validUILanguages -- the same lenient style as NormalizeDisplay (no // error, just a safe default). func NormalizeUILanguage(lang string) string { l := strings.ToLower(lang) if !validUILanguages[l] { return "en" } return l } // NormalizeSiglaStyle maps a sigla_style setting to one of "auto", "polish", // "english"; anything unrecognised (incl. "") falls back to "auto". Accepts the // short aliases "pl"/"en" for convenience. func NormalizeSiglaStyle(s string) string { switch strings.ToLower(strings.TrimSpace(s)) { case "polish", "pl": return "polish" case "english", "en": return "english" default: return "auto" } } // 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"` UILanguage string `toml:"ui_language"` SiglaStyle string `toml:"sigla_style"` WebTheme string `toml:"web_theme"` WebPort int `toml:"web_port"` WebDisplay string `toml:"web_display"` WebMono bool `toml:"web_mono"` WebVersions []string `toml:"web_versions"` Pager string `toml:"pager"` 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 } // SiglaLang resolves the book dialect (a books.toml section code, "en"/"pl") // that `lectio --ref`/`--list` use: the explicit sigla_style, or -- when "auto" // -- the UI language. func (c Config) SiglaLang() string { switch c.SiglaStyle { case "polish": return "pl" case "english": return "en" default: if c.UILanguage == "pl" { return "pl" } return "en" } } // 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{"bt", "wuj", "vul", "grb", "drb"}, DefaultVersion: "bt", Width: 0, All: false, Offline: false, UILanguage: "en", SiglaStyle: "auto", WebTheme: "transfiguration", WebPort: 0, WebDisplay: "horizontal", WebMono: false, WebVersions: nil, Pager: "", 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 } // BooksPath returns the path to the optional user books.toml (in the same // directory as the config file). There is no embedded seed on disk -- absence // means "use the built-in defaults". func BooksPath() (string, error) { p, _, err := configPath() if err != nil { return "", err } return filepath.Join(filepath.Dir(p), "books.toml"), 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) cfg.UILanguage = NormalizeUILanguage(cfg.UILanguage) cfg.SiglaStyle = NormalizeSiglaStyle(cfg.SiglaStyle) 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 bt, wuj, vul, grb, drb)", v) } } if !validVersions[cfg.DefaultVersion] { return fmt.Errorf("config: invalid default_version %q (must be one of bt, wuj, vul, grb, drb)", cfg.DefaultVersion) } for _, v := range cfg.WebVersions { if !validVersions[v] { return fmt.Errorf("config: invalid version %q in web_versions (must be one of bt, wuj, vul, grb, drb)", v) } } return nil }