From 1610b0bf8b0df108b4eeda870e9cf2e83f9c5390 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Wed, 29 Jul 2026 01:05:08 +0200 Subject: cli: --year N prints the year's key liturgical dates On its own, `--year N` now prints a human-readable overview of the year's key dates -- season boundaries, solemnities and feasts of the Lord -- computed from the engine for the configured form (and custom-calendar layers), paged when a pager is configured. With --format json|ical it still emits the machine calendar (runFeed) as before. Numbered seasonal Sundays and Octave weekdays are omitted from the solemnity list (the season boundary already marks them). --- internal/cli/cli.go | 8 ++- internal/cli/yearview.go | 124 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 internal/cli/yearview.go diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 56de12b..078d0c2 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -59,7 +59,8 @@ Flags: --format WHICH json|ical: emit the computed calendar and exit --from DATE range start YYYY-MM-DD (with --to; needs --format) --to DATE range end YYYY-MM-DD (with --from; needs --format) - --year YYYY emit the whole year YYYY (needs --format) + --year YYYY key dates of the year -- seasons & solemnities (add + --format json|ical to emit the machine calendar instead) --form WHICH override calendar form old|new (needs --format) -v, --version print the version and exit -h, --help this help @@ -228,6 +229,11 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { cfg.UILanguage = config.NormalizeUILanguage(uiLang) } + // `--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 == "" { + return runYearOverview(cfg, yearFlag, formFlag, pagerFlag, noPager, stdout, stderr) + } if format != "" || fromFlag != "" || toFlag != "" || yearFlag != "" || formFlag != "" { return runFeed(cfg, format, date, fromFlag, toFlag, yearFlag, formFlag, stdout, stderr) } diff --git a/internal/cli/yearview.go b/internal/cli/yearview.go new file mode 100644 index 0000000..6b8375c --- /dev/null +++ b/internal/cli/yearview.go @@ -0,0 +1,124 @@ +package cli + +import ( + "fmt" + "io" + "regexp" + "strconv" + "strings" + "time" + + "github.com/lukaszkasprzak/lectio/internal/caldata" + "github.com/lukaszkasprzak/lectio/internal/calendar" + "github.com/lukaszkasprzak/lectio/internal/config" +) + +// reNumberedSunday matches the generic seasonal Sundays ("ordinary-sunday-11", +// "lent-sunday-1"), which carry solemnity rank but are not what a "key dates" +// list wants -- the season boundary already marks them. +var reNumberedSunday = regexp.MustCompile(`-sunday-\d+$`) + +// seasonalSunday reports whether a slug is a generic numbered Sunday or an +// Octave-of-Easter weekday, which the overview omits from the solemnity list. +func seasonalSunday(slug string) bool { + s := strings.TrimPrefix(slug, "ef-") + return reNumberedSunday.MatchString(s) || strings.HasPrefix(s, "easter-octave-") +} + +// seasonName renders a season slug as a display phrase for the year overview. +func seasonName(s calendar.Season) string { + switch s { + case calendar.Advent: + return "Advent" + case calendar.Christmas: + return "Christmas" + case calendar.Lent: + return "Lent" + case calendar.Triduum: + return "the Paschal Triduum" + case calendar.Easter_: + return "Eastertide" + case calendar.Ordinary: + return "Ordinary Time" + } + switch string(s) { + case "septuagesima": + return "Septuagesima" + case "passiontide": + return "Passiontide" + case "time-after-epiphany": + return "Time after Epiphany" + case "time-after-pentecost": + return "Time after Pentecost" + } + return string(s) +} + +// topRank reports whether a rank is the year's headline tier: a solemnity +// (Ordinary Form) or a class-1 feast (Extraordinary Form). +func topRank(r calendar.Rank) bool { + return r == calendar.RankSolemnity || r == calendar.RankClass1 +} + +// runYearOverview prints the key liturgical dates of a civil year -- season +// boundaries, solemnities and feasts of the Lord -- computed from the engine +// for the configured form (and custom-calendar layers). It is what `--year N` +// does on its own (with --format it emits the machine calendar instead). Output +// goes through the pager when one is configured and stdout is a terminal. +func runYearOverview(cfg config.Config, yearStr, formFlag string, pagerFlag, noPager bool, stdout, stderr io.Writer) int { + year, err := strconv.Atoi(strings.TrimSpace(yearStr)) + if err != nil || year < 1583 || year > 9999 { + fmt.Fprintf(stderr, "lectio: invalid --year %q (want 1583-9999)\n", yearStr) + return 2 + } + 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 + } + dir, _ := config.CalendarsDir() + layers, errs := caldata.Stack(sel.Form, dir, cfg.Use) + for _, e := range errs { + fmt.Fprintln(stderr, "lectio: warning:", e) + } + + out := stdout + if pagerRequested(pagerFlag, noPager, cfg) && isTerminalWriter(stdout) { + if o, finish, ok := startPager(pagerCommand(cfg), stdout, stderr); ok { + out = o + defer finish() + } + } + + formName := "Ordinary Form" + if sel.Form == "old" { + formName = "Extraordinary Form (1962)" + } + title := fmt.Sprintf("Key liturgical dates %d — %s", year, formName) + fmt.Fprintf(out, "%s\n%s\n\n", title, strings.Repeat("=", len([]rune(title)))) + + prev := calendar.Season("") + for d := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC); d.Year() == year; d = d.AddDate(0, 0, 1) { + day := calendar.Compute(d, sel, layers) + var tags []string + if day.Season != prev && prev != "" { + tags = append(tags, seasonName(day.Season)+" begins") + } + prev = day.Season + if topRank(day.Observed.Rank) && !seasonalSunday(day.Observed.Slug) { + tags = append(tags, "solemnity") + } else if day.Observed.Class == calendar.ClassLord && day.Observed.Rank == calendar.RankFeast { + tags = append(tags, "feast of the Lord") + } + if len(tags) == 0 { + continue + } + name := celebrationName(cfg, day.Observed) + fmt.Fprintf(out, " %s %s %-46s %s\n", + d.Format("2006-01-02"), d.Weekday().String()[:3], name, strings.Join(tags, " · ")) + } + return 0 +} -- cgit v1.3