aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--internal/config/config.go159
-rw-r--r--internal/config/config.toml30
-rw-r--r--internal/config/config_test.go57
5 files changed, 250 insertions, 0 deletions
diff --git a/go.mod b/go.mod
index 7d5a27b..9852557 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,5 @@
module github.com/lukaszkasprzak/lectio
go 1.24.0
+
+require github.com/pelletier/go-toml/v2 v2.4.2
diff --git a/go.sum b/go.sum
index e69de29..df426f0 100644
--- a/go.sum
+++ b/go.sum
@@ -0,0 +1,2 @@
+github.com/pelletier/go-toml/v2 v2.4.2 h1:M2fKKbmyvI+hGId/D0W64qDBMVhJnNR10O5gIbMc//Q=
+github.com/pelletier/go-toml/v2 v2.4.2/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..4e11c56
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,159 @@
+// 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"
+
+ "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,
+}
+
+// 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"`
+ 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,
+ 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
+ }
+
+ 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
+}
diff --git a/internal/config/config.toml b/internal/config/config.toml
new file mode 100644
index 0000000..d31e2c4
--- /dev/null
+++ b/internal/config/config.toml
@@ -0,0 +1,30 @@
+schema_version = 1
+lectionary = "new" # "new" (niedziela.pl) or "traditional" (missalemeum, 1962)
+traditional_lang = "pl" # vernacular for traditional propers: "pl" or "en"
+versions = ["pl", "wuj", "vul", "grb", "drb"] # compare set + TUI cycle order
+default_version = "pl" # TUI start / `lectio show` default
+width = 0 # CLI wrap width; 0 = detect terminal
+all = false # default to all parts (true) or just the gospel (false)
+offline = false # true = never fetch; read only harvested sigla + cache
+
+# Which parts to show. Both tables are commented out -> every part is shown.
+# Uncomment a table and set a part to false to hide it; parts you don't list
+# stay shown. (Parsed as a map: a part is hidden only if explicitly false.)
+#
+# [parts.new]
+# pierwsze_czytanie = true # 1. czytanie (1st reading)
+# psalm = true # Psalm
+# drugie_czytanie = true # 2. czytanie (2nd reading, on feasts)
+# aklamacja = true # Aklamacja (acclamation)
+# ewangelia = true # Ewangelia (gospel)
+#
+# [parts.traditional]
+# introitus = true # Introit
+# oratio = true # Collect
+# lectio = true # Epistle
+# graduale = true # Gradual / Alleluia / Tract
+# evangelium = true # Gospel
+# offertorium = true # Offertory
+# secreta = true # Secret
+# communio = true # Communion
+# postcommunio = true # Postcommunion
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
new file mode 100644
index 0000000..94c33cd
--- /dev/null
+++ b/internal/config/config_test.go
@@ -0,0 +1,57 @@
+package config
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestLoadSeeds(t *testing.T) {
+ dir := t.TempDir()
+ t.Setenv("XDG_CONFIG_HOME", dir)
+ cfg, err := Load()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cfg.DefaultVersion != "pl" || cfg.Offline {
+ t.Errorf("defaults wrong: %+v", cfg)
+ }
+ if cfg.Lectionary != "new" {
+ t.Errorf("lectionary default wrong: %+v", cfg)
+ }
+ if _, err := os.Stat(filepath.Join(dir, "lectio", "config.toml")); err != nil {
+ t.Error("config not seeded")
+ }
+}
+
+func TestLoadOverride(t *testing.T) {
+ dir := t.TempDir()
+ t.Setenv("XDG_CONFIG_HOME", dir)
+ os.MkdirAll(filepath.Join(dir, "lectio"), 0o755)
+ os.WriteFile(filepath.Join(dir, "lectio", "config.toml"),
+ []byte("offline = true\ndefault_version = \"wuj\"\n"), 0o644)
+ cfg, _ := Load()
+ if !cfg.Offline || cfg.DefaultVersion != "wuj" {
+ t.Errorf("override not applied: %+v", cfg)
+ }
+}
+
+func TestPartShown(t *testing.T) {
+ var empty Config
+ if !empty.PartShown("new", "psalm") {
+ t.Error("nil Parts: psalm should be shown")
+ }
+ if !empty.PartShown("new", "ewangelia") {
+ t.Error("nil Parts: ewangelia should be shown")
+ }
+
+ hidden := Config{Parts: map[string]map[string]bool{
+ "new": {"psalm": false},
+ }}
+ if hidden.PartShown("new", "psalm") {
+ t.Error("psalm explicitly false should be hidden")
+ }
+ if !hidden.PartShown("new", "ewangelia") {
+ t.Error("ewangelia not mentioned should still be shown")
+ }
+}