From 2453b598def94649cc7ce9af7155bfaad40cce12 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Fri, 24 Jul 2026 15:01:03 +0200 Subject: web: /settings page to edit config + books.toml, persisted and applied live; v0.14.0 --- internal/config/config.go | 74 +++++++++++++++++++++++++++++++++++++++++- internal/config/config_test.go | 25 ++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) (limited to 'internal/config') diff --git a/internal/config/config.go b/internal/config/config.go index 3428ae8..a4d2c3e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,6 +11,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "strings" "github.com/pelletier/go-toml/v2" @@ -24,7 +25,7 @@ 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.13.1" +const Version = "0.14.0" // validVersions are the five scripture versions lectio understands. var validVersions = map[string]bool{ @@ -300,3 +301,74 @@ func validate(cfg Config) error { } return nil } + +// Validate exposes the semantic checks Load applies, so callers that build a +// Config (the web /settings form) can reject an invalid one before saving. +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). +func Save(cfg Config) error { + path, _, err := configPath() + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + return os.WriteFile(path, renderConfigTOML(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 { + 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)) + + // Preserve the parts map (not edited by the form) so [parts.*] survives Save. + lects := make([]string, 0, len(cfg.Parts)) + for l := range cfg.Parts { + lects = append(lects, l) + } + sort.Strings(lects) + for _, l := range lects { + parts := cfg.Parts[l] + keys := make([]string, 0, len(parts)) + for k := range parts { + keys = append(keys, k) + } + sort.Strings(keys) + fmt.Fprintf(&b, "\n[parts.%s]\n", l) + for _, k := range keys { + fmt.Fprintf(&b, "%s = %t\n", k, parts[k]) + } + } + return []byte(b.String()) +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index cc0c941..53a3980 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -199,6 +199,31 @@ func TestSiglaStyleDefaultsAndDialect(t *testing.T) { } } +func TestSaveRoundTrip(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "config.toml") + t.Setenv("LECTIO_CONFIG", path) + + cfg := Default() + cfg.UILanguage = "pl" + cfg.SiglaStyle = "english" + cfg.WebMono = true + cfg.WebVersions = []string{"wuj", "vul"} + cfg.Pager = "less -R" + cfg.Parts = map[string]map[string]bool{"new": {"psalm": false}} + if err := Save(cfg); err != nil { + t.Fatal(err) + } + got, err := Load() + if err != nil { + t.Fatalf("reload: %v", err) + } + if got.UILanguage != "pl" || got.SiglaStyle != "english" || !got.WebMono || + len(got.WebVersions) != 2 || got.Pager != "less -R" || got.PartShown("new", "psalm") { + t.Errorf("round-trip mismatch: %+v", got) + } +} + func TestPartShown(t *testing.T) { var empty Config if !empty.PartShown("new", "psalm") { -- cgit v1.3