summaryrefslogtreecommitdiff
path: root/internal
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 /internal
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).
Diffstat (limited to 'internal')
-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
+}