aboutsummaryrefslogtreecommitdiff
path: root/internal/cli
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-29 08:24:34 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-29 08:24:34 +0200
commit4ef5570c348e2d8d83c67e049b06e7b5d1a7542d (patch)
tree1da35c3bd67f4235f8ed58af6f3f6e27eaa5a270 /internal/cli
parent802cedfa3b92a07247bf513b90e0fdff71201294 (diff)
downloadlectio-4ef5570c348e2d8d83c67e049b06e7b5d1a7542d.tar.gz
lectio-4ef5570c348e2d8d83c67e049b06e7b5d1a7542d.zip
feat(cli): --cal-dump NAME to generate a commented calendar template
Replace the static examples/sanctorale-of.ini with a `--cal-dump NAME` flag that writes the running universal calendar to ~/.config/lectio/calendars/NAME.ini as a fully-commented layer template (every celebration present, all commented). Uncomment one day and edit a field to override it; enable with `use = NAME`. - Form-aware: respects -l/--lectionary, so `-l trad --cal-dump x` dumps the 1962 calendar (322 entries) and the default dumps the OF calendar (212 entries). - Entries sorted by date then slug; refuses to overwrite an existing file. - Always in sync with the embedded (or sanctorale-overridden) calendar, so no stale checked-in template to maintain. - Update help text, man page (OPTIONS/FILES/EXAMPLES) and README to match; drop examples/sanctorale-of.ini.
Diffstat (limited to 'internal/cli')
-rw-r--r--internal/cli/callayer.go102
-rw-r--r--internal/cli/cli.go9
2 files changed, 110 insertions, 1 deletions
diff --git a/internal/cli/callayer.go b/internal/cli/callayer.go
index 3900eb1..db59b13 100644
--- a/internal/cli/callayer.go
+++ b/internal/cli/callayer.go
@@ -43,8 +43,110 @@ name.en = Saint Example, Patron
reading.gospel = Jn 10:11-16
`
+// calDumpHeader precedes a --cal-dump file. %[1]s = the layer name, %[2]s = a
+// human label for the form (e.g. "Ordinary Form").
+const calDumpHeader = `# lectio calendar layer "%[1]s" -- the %[2]s universal calendar, DUMPED and
+# fully COMMENTED OUT. As shipped it changes nothing. To alter one day, uncomment
+# its block, edit a field (rank, colour, name, ...), then enable the layer by
+# adding its name to 'use' in config.ini: use = %[1]s
+#
+# A layer OVERLAYS the base calendar field by field, so every day you leave
+# commented keeps its default. You can also:
+# * remove a celebration: uncomment its block, add suppress = true
+# * add one: write a new [your-slug] with date/rank/colour/name.*
+# * change its readings: add reading.first / reading.psalm / reading.gospel
+#
+# Fields: date = MM-DD (or easter+-N / christmas+-N / sunday-after MM-DD) ;
+# rank = solemnity|feast|memorial|optional ; class = lord|bvm|saint ;
+# colour = white|red|green|violet|rose|black ; name.<lang> = display name.
+# (reading.* lines are omitted here; add them only to change a day's readings.)
+#
+# Re-dump anytime with: lectio --cal-dump %[1]s (add -l trad for the 1962 form).
+
+[layer]
+id = %[1]s
+name = %[1]s
+type = diocesan
+`
+
var validRanks = map[string]bool{"solemnity": true, "feast": true, "memorial": true, "optional": true}
+// runCalDump writes the running universal calendar for a form to
+// <CalendarsDir>/<name>.ini as a fully-commented layer template: every
+// celebration present, all commented, so uncommenting and editing one day
+// overrides it. It refuses to overwrite an existing file. formFlag ("old"/"new",
+// optional) overrides the form implied by cfg.
+func runCalDump(cfg config.Config, name, formFlag string, stdout, stderr io.Writer) int {
+ sel := cfg.Selection()
+ if formFlag != "" {
+ if formFlag != "old" && formFlag != "new" {
+ fmt.Fprintf(stderr, "lectio: invalid --form %q (want old|new)\n", formFlag)
+ return 2
+ }
+ sel.Form = formFlag
+ }
+ path, err := layerPath(name)
+ if err != nil {
+ fmt.Fprintln(stderr, "lectio:", err)
+ return 2
+ }
+ if _, err := os.Stat(path); err == nil {
+ fmt.Fprintf(stderr, "lectio: %s already exists (not overwriting)\n", path)
+ return 1
+ }
+
+ base := caldata.Base(sel.Form)
+ formLabel := "Ordinary Form"
+ if sel.Form == "old" {
+ formLabel = "Extraordinary Form (1962)"
+ }
+
+ // Sort by date then slug. MM-DD dates sort chronologically and ahead of the
+ // movable specs (easter+N, christmas-N), which then group alphabetically.
+ slugs := make([]string, 0, len(base.Cels))
+ for slug := range base.Cels {
+ slugs = append(slugs, slug)
+ }
+ sort.Slice(slugs, func(i, j int) bool {
+ di, dj := base.Cels[slugs[i]].Fields["date"], base.Cels[slugs[j]].Fields["date"]
+ if di != dj {
+ return di < dj
+ }
+ return slugs[i] < slugs[j]
+ })
+
+ var b strings.Builder
+ fmt.Fprintf(&b, calDumpHeader, name, formLabel)
+ order := []string{"date", "rank", "class", "colour", "name.en", "name.pl", "name.la"}
+ n := 0
+ for _, slug := range slugs {
+ f := base.Cels[slug].Fields
+ if f["date"] == "" {
+ continue // skip anything without a date (defensive)
+ }
+ b.WriteByte('\n')
+ fmt.Fprintf(&b, "# [%s]\n", slug)
+ for _, k := range order {
+ if v := f[k]; v != "" {
+ fmt.Fprintf(&b, "# %s = %s\n", k, v)
+ }
+ }
+ n++
+ }
+
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ fmt.Fprintln(stderr, "lectio:", err)
+ return 1
+ }
+ if err := os.WriteFile(path, []byte(b.String()), 0o644); err != nil {
+ fmt.Fprintln(stderr, "lectio:", err)
+ return 1
+ }
+ fmt.Fprintf(stdout, "dumped %d %s celebrations to %s\n", n, formLabel, path)
+ fmt.Fprintf(stdout, "uncomment a day, edit it, then set use = %s in config.ini\n", name)
+ return 0
+}
+
// layerPath resolves <CalendarsDir>/<name>.ini, rejecting a name with path separators.
func layerPath(name string) (string, error) {
if name == "" || strings.ContainsAny(name, `/\`) {
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index 078d0c2..ffefd54 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -46,6 +46,8 @@ Flags:
--week list the coming week's gospel references and exit
-L, --liturgy print the computed liturgical day (offline, no network) and exit
--cal-new NAME scaffold a custom calendar layer ~/.config/lectio/calendars/NAME.ini
+ --cal-dump NAME dump the whole universal calendar to NAME.ini, commented,
+ to uncomment+edit one day (add -l trad for the 1962 form)
--cal-check NAME validate a custom calendar layer NAME.ini and exit
--corpus-check X validate a bible corpus (code, e.g. drb, or a path to
a <code>.tsv) and exit; errors fail, coverage gaps warn
@@ -132,7 +134,7 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
var list bool
var ref string
var liturgy bool
- var calNew, calCheck string
+ var calNew, calCheck, calDump string
var corpusCheck string
var jsonOut bool
var format, fromFlag, toFlag, yearFlag, formFlag string
@@ -174,6 +176,7 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
fs.BoolVar(&liturgy, "liturgy", false, "print the computed liturgical day (offline calendar engine) and exit")
fs.StringVar(&calNew, "cal-new", "", "scaffold a new calendar-layer file NAME.ini and exit")
fs.StringVar(&calCheck, "cal-check", "", "validate the calendar-layer file NAME.ini and exit")
+ fs.StringVar(&calDump, "cal-dump", "", "dump the universal calendar to NAME.ini as a commented layer template and exit")
fs.StringVar(&corpusCheck, "corpus-check", "", "validate a bible corpus (code or path to <code>.tsv) and exit")
fs.BoolVar(&jsonOut, "json", false, "with --corpus-check, print the report as JSON")
fs.StringVar(&format, "format", "", "json|ical: emit the computed calendar and exit")
@@ -229,6 +232,10 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
cfg.UILanguage = config.NormalizeUILanguage(uiLang)
}
+ if calDump != "" {
+ return runCalDump(cfg, calDump, formFlag, stdout, stderr)
+ }
+
// `--year N` on its own prints the human-readable key-dates overview;
// with --format (or --from/--to) it emits the machine calendar via runFeed.
if yearFlag != "" && format == "" && fromFlag == "" && toFlag == "" {