aboutsummaryrefslogtreecommitdiff
path: root/internal/config/config.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/config/config.go')
-rw-r--r--internal/config/config.go264
1 files changed, 204 insertions, 60 deletions
diff --git a/internal/config/config.go b/internal/config/config.go
index a7365bb..d514d33 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -1,9 +1,11 @@
-// Package config loads lectio's TOML configuration file.
+// Package config loads lectio's INI configuration file.
//
-// Resolution order: LECTIO_CONFIG env var -> ~/.config/lectio/config.toml
+// Resolution order: LECTIO_CONFIG env var -> ~/.config/lectio/config.ini
// (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.
+// XDG_CONFIG_HOME via os.UserConfigDir()) -> built-in defaults. A pre-existing
+// config.toml (lectio's former format) at the default location is converted to
+// config.ini once, on first run, and left on disk. INI that fails to parse
+// falls back to defaults with a warning on stderr.
package config
import (
@@ -12,16 +14,20 @@ import (
"os"
"path/filepath"
"sort"
+ "strconv"
"strings"
"github.com/pelletier/go-toml/v2"
+
+ "github.com/lukaszkasprzak/lectio/internal/calendar"
+ "github.com/lukaszkasprzak/lectio/internal/ini"
)
-// seedTOML is the embedded default config.toml, written to disk on first
-// run and parsed as the fallback default values.
+// seedINI is the embedded default config.ini, written to disk on first run and
+// parsed as the fallback default values.
//
-//go:embed config.toml
-var seedTOML []byte
+//go:embed config.ini
+var seedINI []byte
// Version is lectio's release version, shared by every binary's
// -v/--version output (lectio, lectio-ui, lectio-web).
@@ -107,7 +113,8 @@ func NormalizeSiglaStyle(s string) string {
}
}
-// Config holds lectio's user-configurable settings.
+// Config holds lectio's user-configurable settings. The toml tags are used only
+// by the one-shot config.toml -> config.ini migration; the live format is INI.
type Config struct {
SchemaVersion int `toml:"schema_version"`
Lectionary string `toml:"lectionary"`
@@ -126,6 +133,11 @@ type Config struct {
WebVersions []string `toml:"web_versions"`
Pager string `toml:"pager"`
Parts map[string]map[string]bool `toml:"parts"`
+
+ // Computed-calendar placement options (INI [calendar] section; INI-only).
+ CalEpiphany string `toml:"-"`
+ CalAscension string `toml:"-"`
+ CalCorpusChristi string `toml:"-"`
}
// PartShown reports whether a part renders: true unless explicitly set false.
@@ -155,6 +167,28 @@ func (c Config) SiglaLang() string {
}
}
+// Selection maps the config onto the calendar engine's Selection: the form
+// (from Lectionary: "new" -> OF, "traditional" -> EF) plus the national
+// placement options, each falling back to the Universal Roman default.
+func (c Config) Selection() calendar.Selection {
+ sel := calendar.DefaultSelection()
+ if c.Lectionary == "traditional" {
+ sel.Form = "old"
+ } else {
+ sel.Form = "new"
+ }
+ if c.CalEpiphany != "" {
+ sel.Epiphany = c.CalEpiphany
+ }
+ if c.CalAscension != "" {
+ sel.Ascension = c.CalAscension
+ }
+ if c.CalCorpusChristi != "" {
+ sel.CorpusChristi = c.CalCorpusChristi
+ }
+ return sel
+}
+
// 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 {
@@ -181,7 +215,7 @@ func Default() Config {
// 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.
+// reports whether this is the default (seedable/migratable) location.
func configPath() (path string, isDefault bool, err error) {
if p := os.Getenv("LECTIO_CONFIG"); p != "" {
return p, false, nil
@@ -190,7 +224,7 @@ func configPath() (path string, isDefault bool, err error) {
if err != nil {
return "", false, err
}
- return filepath.Join(dir, "lectio", "config.toml"), true, nil
+ return filepath.Join(dir, "lectio", "config.ini"), true, nil
}
// BooksPath returns the path to the optional user books.toml (in the same
@@ -230,14 +264,32 @@ func seedIfMissing(path string) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
- return os.WriteFile(path, seedTOML, 0o644)
+ return os.WriteFile(path, seedINI, 0o644)
+}
+
+// migrateFromTOML converts a former config.toml sitting next to iniPath into
+// config.ini, once. The old file is left in place (so the change is
+// reversible). A missing or unreadable config.toml is not an error.
+func migrateFromTOML(iniPath string) {
+ tomlPath := filepath.Join(filepath.Dir(iniPath), "config.toml")
+ data, err := os.ReadFile(tomlPath)
+ if err != nil {
+ return
+ }
+ cfg := Default()
+ if err := toml.Unmarshal(data, &cfg); err != nil {
+ return
+ }
+ normalize(&cfg)
+ _ = Save(cfg) // writes config.ini
+ fmt.Fprintf(os.Stderr, "lectio: migrated %s -> config.ini (old file kept)\n", tomlPath)
}
-// 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.
+// 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 an INI 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()
@@ -248,8 +300,11 @@ func Load() (Config, error) {
}
if isDefault {
- if err := seedIfMissing(path); err != nil {
- return def, err
+ if _, statErr := os.Stat(path); os.IsNotExist(statErr) {
+ migrateFromTOML(path) // convert a former config.toml if present
+ if err := seedIfMissing(path); err != nil {
+ return def, err
+ }
}
}
@@ -261,24 +316,112 @@ func Load() (Config, error) {
return def, err
}
- cfg := def
- if err := toml.Unmarshal(data, &cfg); err != nil {
+ cfg, err := readINI(data)
+ if 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)
+ normalize(&cfg)
if err := validate(cfg); err != nil {
return Config{}, err
}
+ return cfg, nil
+}
+
+// normalize applies the lenient field fix-ups Load enforces after parsing.
+func normalize(cfg *Config) {
+ cfg.WebDisplay = NormalizeDisplay(cfg.WebDisplay)
+ cfg.UILanguage = NormalizeUILanguage(cfg.UILanguage)
+ cfg.SiglaStyle = NormalizeSiglaStyle(cfg.SiglaStyle)
+}
+// readINI parses INI bytes into a Config over Default(). Absent keys keep the
+// default; present keys override (an empty list value clears the list).
+func readINI(data []byte) (Config, error) {
+ cfg := Default()
+ secs, err := ini.Parse(data)
+ if err != nil {
+ return cfg, err
+ }
+ for _, s := range secs {
+ switch {
+ case s.Name == "":
+ for _, p := range s.Pairs {
+ applyScalar(&cfg, p.Key, p.Val)
+ }
+ case s.Name == "calendar":
+ for _, p := range s.Pairs {
+ switch p.Key {
+ case "epiphany":
+ cfg.CalEpiphany = p.Val
+ case "ascension":
+ cfg.CalAscension = p.Val
+ case "corpus_christi":
+ cfg.CalCorpusChristi = p.Val
+ }
+ }
+ case strings.HasPrefix(s.Name, "parts."):
+ lect := strings.TrimPrefix(s.Name, "parts.")
+ if cfg.Parts == nil {
+ cfg.Parts = map[string]map[string]bool{}
+ }
+ if cfg.Parts[lect] == nil {
+ cfg.Parts[lect] = map[string]bool{}
+ }
+ for _, p := range s.Pairs {
+ cfg.Parts[lect][p.Key] = p.Val == "true"
+ }
+ }
+ }
return cfg, nil
}
-// validate checks the semantic constraints Load enforces beyond what TOML
-// unmarshaling itself catches.
+func applyScalar(cfg *Config, key, val string) {
+ switch key {
+ case "schema_version":
+ cfg.SchemaVersion = atoiOr(val, cfg.SchemaVersion)
+ case "lectionary":
+ cfg.Lectionary = val
+ case "traditional_lang":
+ cfg.TraditionalLang = val
+ case "versions":
+ cfg.Versions = ini.List(val)
+ case "default_version":
+ cfg.DefaultVersion = val
+ case "width":
+ cfg.Width = atoiOr(val, cfg.Width)
+ case "all":
+ cfg.All = val == "true"
+ case "offline":
+ cfg.Offline = val == "true"
+ case "ui_language":
+ cfg.UILanguage = val
+ case "sigla_style":
+ cfg.SiglaStyle = val
+ case "web_theme":
+ cfg.WebTheme = val
+ case "web_port":
+ cfg.WebPort = atoiOr(val, cfg.WebPort)
+ case "web_display":
+ cfg.WebDisplay = val
+ case "web_mono":
+ cfg.WebMono = val == "true"
+ case "web_versions":
+ cfg.WebVersions = ini.List(val)
+ case "pager":
+ cfg.Pager = val
+ }
+}
+
+func atoiOr(s string, def int) int {
+ if n, err := strconv.Atoi(strings.TrimSpace(s)); err == nil {
+ return n
+ }
+ return def
+}
+
+// validate checks the semantic constraints Load enforces beyond parsing.
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)
@@ -307,8 +450,7 @@ func validate(cfg Config) error {
func Validate(cfg Config) error { return validate(cfg) }
// Save writes cfg to the config file (configPath, honoring LECTIO_CONFIG),
-// creating the directory if needed, as commented TOML (regenerated -- the
-// explanatory comments are kept, the values are cfg's).
+// creating the directory if needed, as commented INI.
func Save(cfg Config) error {
path, _, err := configPath()
if err != nil {
@@ -317,42 +459,37 @@ func Save(cfg Config) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
- return os.WriteFile(path, renderConfigTOML(cfg), 0o644)
+ return os.WriteFile(path, renderConfigINI(cfg), 0o644)
}
-func tomlStr(s string) string { return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"` }
-
-func tomlStrSlice(ss []string) string {
- q := make([]string, len(ss))
- for i, s := range ss {
- q[i] = tomlStr(s)
- }
- return "[" + strings.Join(q, ", ") + "]"
-}
-
-// renderConfigTOML regenerates config.toml from cfg, preserving the seed's
-// field comments. Parts (the per-lectionary hide map) is written back verbatim
-// so a config that had [parts.*] sections keeps them across a Save.
-func renderConfigTOML(cfg Config) []byte {
+// renderConfigINI regenerates config.ini from cfg. Comments are on their own
+// lines so no value ever shares a line with a "#"/";" comment character.
+func renderConfigINI(cfg Config) []byte {
var b strings.Builder
- fmt.Fprintf(&b, "schema_version = %d\n", cfg.SchemaVersion)
- fmt.Fprintf(&b, "lectionary = %s # \"new\" (niedziela.pl) or \"traditional\" (missalemeum, 1962)\n", tomlStr(cfg.Lectionary))
- fmt.Fprintf(&b, "traditional_lang = %s # vernacular for traditional propers: \"pl\" or \"en\"\n", tomlStr(cfg.TraditionalLang))
- fmt.Fprintf(&b, "versions = %s # compare set + TUI cycle order (bt = Biblia TysiÄ…clecia)\n", tomlStrSlice(cfg.Versions))
- fmt.Fprintf(&b, "default_version = %s # TUI start / `lectio -b` default\n", tomlStr(cfg.DefaultVersion))
- fmt.Fprintf(&b, "width = %d # CLI wrap width; 0 = detect terminal\n", cfg.Width)
- fmt.Fprintf(&b, "all = %t # all parts (true) or just the gospel (false)\n", cfg.All)
- fmt.Fprintf(&b, "offline = %t # true = never fetch; read only cache + harvested sigla\n", cfg.Offline)
- fmt.Fprintf(&b, "ui_language = %s # interface language: \"en\" or \"pl\" (readings stay source-language)\n", tomlStr(cfg.UILanguage))
- fmt.Fprintf(&b, "sigla_style = %s # citation dialect for --ref/--list/--citation: \"auto\", \"polish\", \"english\"\n", tomlStr(cfg.SiglaStyle))
- fmt.Fprintf(&b, "web_theme = %s # default lectio-web theme\n", tomlStr(cfg.WebTheme))
- fmt.Fprintf(&b, "web_port = %d # lectio-web port; 0 = try 1099, then any free port\n", cfg.WebPort)
- fmt.Fprintf(&b, "web_display = %s # lectio-web layout: \"horizontal\", \"vertical\", \"interlinear\"\n", tomlStr(cfg.WebDisplay))
- fmt.Fprintf(&b, "web_mono = %t # lectio-web: monospace reading face\n", cfg.WebMono)
- fmt.Fprintf(&b, "web_versions = %s # which versions start toggled on; empty = just default_version\n", tomlStrSlice(cfg.WebVersions))
- fmt.Fprintf(&b, "pager = %s # pager for `lectio` output; \"\" = off\n", tomlStr(cfg.Pager))
+ b.WriteString("# lectio configuration (INI). Lists are comma-separated; booleans true/false.\n\n")
+ fmt.Fprintf(&b, "schema_version = %d\n", cfg.SchemaVersion)
+ fmt.Fprintf(&b, "lectionary = %s\n", cfg.Lectionary)
+ fmt.Fprintf(&b, "traditional_lang = %s\n", cfg.TraditionalLang)
+ fmt.Fprintf(&b, "versions = %s\n", strings.Join(cfg.Versions, ", "))
+ fmt.Fprintf(&b, "default_version = %s\n", cfg.DefaultVersion)
+ fmt.Fprintf(&b, "width = %d\n", cfg.Width)
+ fmt.Fprintf(&b, "all = %t\n", cfg.All)
+ fmt.Fprintf(&b, "offline = %t\n", cfg.Offline)
+ fmt.Fprintf(&b, "ui_language = %s\n", cfg.UILanguage)
+ fmt.Fprintf(&b, "sigla_style = %s\n", cfg.SiglaStyle)
+ fmt.Fprintf(&b, "web_theme = %s\n", cfg.WebTheme)
+ fmt.Fprintf(&b, "web_port = %d\n", cfg.WebPort)
+ fmt.Fprintf(&b, "web_display = %s\n", cfg.WebDisplay)
+ fmt.Fprintf(&b, "web_mono = %t\n", cfg.WebMono)
+ fmt.Fprintf(&b, "web_versions = %s\n", strings.Join(cfg.WebVersions, ", "))
+ fmt.Fprintf(&b, "pager = %s\n", cfg.Pager)
+
+ b.WriteString("\n# Computed-calendar national placement options.\n[calendar]\n")
+ fmt.Fprintf(&b, "epiphany = %s\n", orDefault(cfg.CalEpiphany, "fixed"))
+ fmt.Fprintf(&b, "ascension = %s\n", orDefault(cfg.CalAscension, "thursday"))
+ fmt.Fprintf(&b, "corpus_christi = %s\n", orDefault(cfg.CalCorpusChristi, "thursday"))
- // Preserve the parts map (not edited by the form) so [parts.*] survives Save.
+ // Preserve the parts map (per-lectionary hide flags) across a Save.
lects := make([]string, 0, len(cfg.Parts))
for l := range cfg.Parts {
lects = append(lects, l)
@@ -372,3 +509,10 @@ func renderConfigTOML(cfg Config) []byte {
}
return []byte(b.String())
}
+
+func orDefault(v, def string) string {
+ if v == "" {
+ return def
+ }
+ return v
+}