summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-29 01:05:08 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-29 01:05:08 +0200
commit1610b0bf8b0df108b4eeda870e9cf2e83f9c5390 (patch)
tree18c2ff0f0c97b33426c985911b05e768beadd03d
parentfd950f027300d3185c7b66b06981eddcfcfead4f (diff)
downloadlectio-1610b0bf8b0df108b4eeda870e9cf2e83f9c5390.tar.gz
lectio-1610b0bf8b0df108b4eeda870e9cf2e83f9c5390.zip
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).
-rw-r--r--internal/cli/cli.go8
-rw-r--r--internal/cli/yearview.go124
2 files changed, 131 insertions, 1 deletions
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
+}