aboutsummaryrefslogtreecommitdiff
path: root/internal/cli/cli.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/cli/cli.go')
-rw-r--r--internal/cli/cli.go374
1 files changed, 162 insertions, 212 deletions
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index 66f45a7..e7a2439 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -1,6 +1,6 @@
-// Package cli is lectio's subcommand dispatcher: it wires config, the
-// readings router, liturgy.Harvest and render together into the `lectio`
-// binary's Run entry point.
+// Package cli is lectio's flag-driven command dispatcher: it wires config,
+// the readings router, liturgy.Harvest and render together into the
+// `lectio` binary's Run entry point.
package cli
import (
@@ -25,36 +25,35 @@ const versionString = "0.1.0"
const helpText = `lectio — daily Catholic liturgy readings (Polish + 4 versions)
Usage:
- lectio [today] [--all] [--raw] [--width N] [--refresh]
- lectio date D [--all] [--raw] [--width N] [--refresh] D = YYYY-MM-DD
- lectio show VERSION [--date D] [--all] [--raw] [--width N] [--refresh]
- one version's text
- lectio compare LIST [--date D] [--all] [--width N] [--refresh] [--raw]
- LIST = comma-separated versions (default: config "versions")
- lectio update [--days N] [--from D] harvest future sigla to the TSV
- lectio --version | lectio -v
- lectio help | lectio -h | lectio <cmd> -h
+ lectio [DATE] [flags] DATE = YYYY-MM-DD (default: today), any position
-Global flags (any subcommand, any position):
- --offline skip the network, use cached/harvested data only
- --lectionary new|traditional override config "lectionary" for this run
- --lang pl|en override config "traditional_lang" for this run
-
-Per-command flags:
- --all show every reading part (1st/2nd, psalm, acclamation,
- gospel); default is the gospel only, unless config
- all = true
- --raw omit the banner/headings (for piping)
- --width N wrap width; 0 = detect terminal, else a sane default
- (80 normally, 120 for compare)
- --refresh bypass the cache and re-fetch
- --date D date as YYYY-MM-DD (show/compare; default: today)
+Flags:
+ -a, --all all readings, not just the gospel
+ -b, --bible VER one version's text: pl,wuj,vul,grb,drb
+ -c, --compare LIST versions side by side (comma-separated)
+ -r, --raw text only, no banner/headings (for piping)
+ -w, --width N wrap width; 0 = detect terminal
+ -R, --refresh ignore cache, re-download
+ -o, --offline cache/sigla only, no network
+ -l, --lectionary WHICH new|trad (trad -> traditional)
+ -g, --lang LANG traditional lectionary language: pl|en
+ -u, --update harvest sigla maximally to the horizon (idempotent)
+ -v, --version print the version and exit
+ -h, --help this help
Versions: pl (Polski/niedziela.pl) wuj (Wujek) vul (Wulgata) grb (Grecki)
drb (Douay-Rheims)
+Examples:
+ lectio today's gospel
+ lectio 2026-07-22 -a all readings for a date
+ lectio -c pl,wuj 2026-07-22 compare two versions for a date
+ lectio -b vul -a Vulgate text, all readings
+ lectio -l trad -a traditional lectionary, all readings
+ lectio -u harvest sigla to the horizon
+
Flags override config. Exit codes: 0 ok, 1 runtime error (fetch/parse),
-2 usage error (unknown command/version, bad date, missing argument).
+2 usage error (bad flag, bad date, bad version, bad --lectionary/--lang).
`
const (
@@ -91,9 +90,52 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
return 0
}
- offline, lectionary, lang, rest := extractGlobal(args)
- if lectionary != "" && lectionary != "new" && lectionary != "traditional" {
- fmt.Fprintf(stderr, "lectio: invalid --lectionary %q (want new|traditional)\n", lectionary)
+ date, rest, err := extractDate(args)
+ if err != nil {
+ fmt.Fprintln(stderr, "lectio:", err)
+ return 2
+ }
+
+ var all, raw, refresh, offline, update bool
+ var bibleVer, compareList, lectionary, lang string
+ var width int
+
+ fs := flag.NewFlagSet("lectio", flag.ContinueOnError)
+ fs.SetOutput(stderr)
+ fs.Usage = func() { fmt.Fprint(stderr, helpText) }
+
+ fs.BoolVar(&all, "a", false, "all readings, not just the gospel")
+ fs.BoolVar(&all, "all", false, "all readings, not just the gospel")
+ fs.StringVar(&bibleVer, "b", "", "one version's text: pl,wuj,vul,grb,drb")
+ fs.StringVar(&bibleVer, "bible", "", "one version's text: pl,wuj,vul,grb,drb")
+ fs.StringVar(&compareList, "c", "", "versions side by side (comma list)")
+ fs.StringVar(&compareList, "compare", "", "versions side by side (comma list)")
+ fs.BoolVar(&raw, "r", false, "text only, no banner/headings")
+ fs.BoolVar(&raw, "raw", false, "text only, no banner/headings")
+ fs.IntVar(&width, "w", 0, "wrap width (0 = detect terminal)")
+ fs.IntVar(&width, "width", 0, "wrap width (0 = detect terminal)")
+ fs.BoolVar(&refresh, "R", false, "ignore cache, re-download")
+ fs.BoolVar(&refresh, "refresh", false, "ignore cache, re-download")
+ fs.BoolVar(&offline, "o", false, "cache/sigla only, no network")
+ fs.BoolVar(&offline, "offline", false, "cache/sigla only, no network")
+ fs.StringVar(&lectionary, "l", "", "new|trad")
+ fs.StringVar(&lectionary, "lectionary", "", "new|trad")
+ fs.StringVar(&lang, "g", "", "pl|en")
+ fs.StringVar(&lang, "lang", "", "pl|en")
+ fs.BoolVar(&update, "u", false, "harvest sigla maximally to the horizon")
+ fs.BoolVar(&update, "update", false, "harvest sigla maximally to the horizon")
+
+ if err := fs.Parse(rest); err != nil {
+ return 2
+ }
+ if fs.NArg() > 0 {
+ fmt.Fprintf(stderr, "lectio: unexpected argument %q; see 'lectio -h'\n", fs.Arg(0))
+ return 2
+ }
+
+ lectionary, err = normalizeLectionary(lectionary)
+ if err != nil {
+ fmt.Fprintln(stderr, "lectio:", err)
return 2
}
if lang != "" && lang != "pl" && lang != "en" {
@@ -101,46 +143,42 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
return 2
}
- cmd := "today"
- cmdArgs := rest
- if len(rest) > 0 && !strings.HasPrefix(rest[0], "-") {
- cmd = rest[0]
- cmdArgs = rest[1:]
+ if update {
+ return runHarvest(date, stdout, stderr)
}
- switch cmd {
- case "today", "date", "show", "compare":
- cfg, err := config.Load()
- if err != nil {
- fmt.Fprintln(stderr, "lectio:", err)
- return 1
- }
- if offline {
- cfg.Offline = true
- }
- if lectionary != "" {
- cfg.Lectionary = lectionary
- }
- if lang != "" {
- cfg.TraditionalLang = lang
- }
- switch cmd {
- case "today":
- return runToday(cmdArgs, cfg, stdout, stderr)
- case "date":
- return runDate(cmdArgs, cfg, stdout, stderr)
- case "show":
- return runShow(cmdArgs, cfg, stdout, stderr)
- case "compare":
- return runCompare(cmdArgs, cfg, stdout, stderr)
+ cfg, err := config.Load()
+ if err != nil {
+ fmt.Fprintln(stderr, "lectio:", err)
+ return 1
+ }
+ if offline {
+ cfg.Offline = true
+ }
+ if lectionary != "" {
+ cfg.Lectionary = lectionary
+ }
+ if lang != "" {
+ cfg.TraditionalLang = lang
+ }
+
+ effAll := all || cfg.All
+ effWidth := width
+ if effWidth == 0 {
+ effWidth = cfg.Width
+ }
+
+ if compareList != "" {
+ return renderCompare(cfg, compareList, date, effAll, raw, effWidth, refresh, stdout, stderr)
+ }
+ if bibleVer != "" {
+ if !validVersions[bibleVer] {
+ fmt.Fprintf(stderr, "lectio: unknown version %q (want one of pl, wuj, vul, grb, drb)\n", bibleVer)
+ return 2
}
- return 1 // unreachable
- case "update":
- return runUpdate(cmdArgs, stdout, stderr)
- default:
- fmt.Fprintf(stderr, "lectio: unknown command %q; see 'lectio help'\n", cmd)
- return 2
+ return fetchAndPrint(cfg, bibleVer, date, effAll, raw, effWidth, refresh, stdout, stderr)
}
+ return fetchAndPrint(cfg, cfg.DefaultVersion, date, effAll, raw, effWidth, refresh, stdout, stderr)
}
// wantsHelp reports whether -h/--help appears anywhere in args.
@@ -153,107 +191,64 @@ func wantsHelp(args []string) bool {
return false
}
-// extractGlobal pulls the global --offline/--lectionary/--lang flags out of
-// args (they may appear anywhere, before or after the subcommand), leaving
-// rest for subcommand dispatch and per-command flag parsing.
-func extractGlobal(args []string) (offline bool, lectionary, lang string, rest []string) {
- for i := 0; i < len(args); i++ {
- a := args[i]
- switch {
- case a == "--offline":
- offline = true
- case a == "--lectionary" && i+1 < len(args):
- lectionary = args[i+1]
- i++
- case strings.HasPrefix(a, "--lectionary="):
- lectionary = strings.TrimPrefix(a, "--lectionary=")
- case a == "--lang" && i+1 < len(args):
- lang = args[i+1]
- i++
- case strings.HasPrefix(a, "--lang="):
- lang = strings.TrimPrefix(a, "--lang=")
- default:
- rest = append(rest, a)
+// extractDate pulls the single positional DATE token (YYYY-MM-DD, matching
+// dateRe) out of args, wherever it appears, and returns it along with the
+// remaining tokens for flag.FlagSet to parse. No flag's own value can match
+// dateRe's shape (width is an int, versions/lists/lang/lectionary are short
+// codes), so scanning raw tokens this way is unambiguous. Defaults to
+// today() when no date token is present; errors if more than one is found.
+func extractDate(args []string) (date string, rest []string, err error) {
+ found := false
+ for _, a := range args {
+ if dateRe.MatchString(a) {
+ if found {
+ return "", nil, fmt.Errorf("multiple dates given (%q and %q)", date, a)
+ }
+ date = a
+ found = true
+ continue
}
+ rest = append(rest, a)
}
- return offline, lectionary, lang, rest
-}
-
-// runToday handles the (default) "today" subcommand: --all --raw --width
-// --refresh, date = today.
-func runToday(args []string, cfg config.Config, stdout, stderr io.Writer) int {
- fs := flag.NewFlagSet("today", flag.ContinueOnError)
- fs.SetOutput(stderr)
- all := fs.Bool("all", cfg.All, "show every reading part, not just the gospel")
- raw := fs.Bool("raw", false, "omit banner/headings (for piping)")
- width := fs.Int("width", cfg.Width, "wrap width (0 = detect terminal)")
- refresh := fs.Bool("refresh", false, "bypass cache, re-fetch")
- if err := fs.Parse(args); err != nil {
- return 2
- }
- if fs.NArg() > 0 {
- fmt.Fprintf(stderr, "lectio: today takes no date argument (use: lectio date %s)\n", fs.Arg(0))
- return 2
+ if !found {
+ date = today()
}
- return fetchAndPrint(cfg, cfg.DefaultVersion, today(), *all, *raw, *width, *refresh, stdout, stderr)
+ return date, rest, nil
}
-// runDate handles "date D": D is the required first positional argument,
-// flags as in runToday.
-func runDate(args []string, cfg config.Config, stdout, stderr io.Writer) int {
- if len(args) == 0 {
- fmt.Fprintln(stderr, "lectio: date requires an argument (YYYY-MM-DD)")
- return 2
- }
- date := args[0]
- if !dateRe.MatchString(date) {
- fmt.Fprintf(stderr, "lectio: invalid date %q (want YYYY-MM-DD)\n", date)
- return 2
- }
- fs := flag.NewFlagSet("date", flag.ContinueOnError)
- fs.SetOutput(stderr)
- all := fs.Bool("all", cfg.All, "show every reading part, not just the gospel")
- raw := fs.Bool("raw", false, "omit banner/headings (for piping)")
- width := fs.Int("width", cfg.Width, "wrap width (0 = detect terminal)")
- refresh := fs.Bool("refresh", false, "bypass cache, re-fetch")
- if err := fs.Parse(args[1:]); err != nil {
- return 2
+// normalizeLectionary maps -l/--lectionary's accepted spellings ("new",
+// "trad", "traditional") onto the canonical config.Config.Lectionary values
+// ("new", "traditional"); "" (flag not given) passes through unchanged so
+// the caller knows to leave config's own setting alone.
+func normalizeLectionary(lectionary string) (string, error) {
+ switch lectionary {
+ case "":
+ return "", nil
+ case "trad":
+ return "traditional", nil
+ case "new", "traditional":
+ return lectionary, nil
+ default:
+ return "", fmt.Errorf("invalid --lectionary %q (want new|trad)", lectionary)
}
- return fetchAndPrint(cfg, cfg.DefaultVersion, date, *all, *raw, *width, *refresh, stdout, stderr)
}
-// runShow handles "show VERSION": same as today/date but the version is
-// fixed by the caller instead of cfg.DefaultVersion.
-func runShow(args []string, cfg config.Config, stdout, stderr io.Writer) int {
- if len(args) == 0 {
- fmt.Fprintln(stderr, "lectio: show requires a version argument (pl|wuj|vul|grb|drb)")
- return 2
- }
- ver := args[0]
- if !validVersions[ver] {
- fmt.Fprintf(stderr, "lectio: unknown version %q (want one of pl, wuj, vul, grb, drb)\n", ver)
- return 2
- }
- fs := flag.NewFlagSet("show", flag.ContinueOnError)
- fs.SetOutput(stderr)
- all := fs.Bool("all", cfg.All, "show every reading part, not just the gospel")
- raw := fs.Bool("raw", false, "omit banner/headings (for piping)")
- width := fs.Int("width", cfg.Width, "wrap width (0 = detect terminal)")
- refresh := fs.Bool("refresh", false, "bypass cache, re-fetch")
- date := fs.String("date", today(), "date as YYYY-MM-DD")
- if err := fs.Parse(args[1:]); err != nil {
- return 2
- }
- if !dateRe.MatchString(*date) {
- fmt.Fprintf(stderr, "lectio: invalid date %q (want YYYY-MM-DD)\n", *date)
- return 2
+// runHarvest handles -u/--update: harvest sigla maximally (to the
+// unpublished horizon) from date, printing the outcome or -- on a genuine
+// interruption (see liturgy.Harvest) -- the error.
+func runHarvest(date string, stdout, stderr io.Writer) int {
+ added, furthest, err := liturgy.Harvest(date, 0)
+ if err != nil {
+ fmt.Fprintln(stderr, "lectio:", err)
+ return 1
}
- return fetchAndPrint(cfg, ver, *date, *all, *raw, *width, *refresh, stdout, stderr)
+ fmt.Fprintf(stdout, "harvested %d day(s), furthest %s\n", added, furthest)
+ return 0
}
-// fetchAndPrint is the shared today/date/show path: fetch via the readings
-// router, apply the offline version swap, then render each section's
-// heading and render.GatherVersion blocks.
+// fetchAndPrint is the shared single-version render path (default version or
+// -b/--bible): fetch via the readings router, apply the offline version
+// swap, then render each section's heading and render.GatherVersion blocks.
func fetchAndPrint(cfg config.Config, version, date string, all, raw bool, width int, refresh bool, stdout, stderr io.Writer) int {
secs, err := readings.Load(cfg, readings.Options{
Date: date,
@@ -320,31 +315,10 @@ func renderSection(sec liturgy.Section, version, lectionary string, width int, r
return strings.TrimRight(strings.Join(lines, "\n"), "\n")
}
-// runCompare handles "compare LIST": LIST is the required first positional
-// argument (falling back to cfg.Versions when omitted entirely).
-func runCompare(args []string, cfg config.Config, stdout, stderr io.Writer) int {
- var list string
- rest := args
- if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
- list = args[0]
- rest = args[1:]
- }
-
- fs := flag.NewFlagSet("compare", flag.ContinueOnError)
- fs.SetOutput(stderr)
- all := fs.Bool("all", cfg.All, "show every reading part, not just the gospel")
- raw := fs.Bool("raw", false, "omit banner/headings (for piping)")
- width := fs.Int("width", cfg.Width, "wrap width (0 = detect terminal; compare default 120)")
- refresh := fs.Bool("refresh", false, "bypass cache, re-fetch")
- date := fs.String("date", today(), "date as YYYY-MM-DD")
- if err := fs.Parse(rest); err != nil {
- return 2
- }
- if !dateRe.MatchString(*date) {
- fmt.Fprintf(stderr, "lectio: invalid date %q (want YYYY-MM-DD)\n", *date)
- return 2
- }
-
+// renderCompare handles -c/--compare LIST: LIST is a comma-separated list of
+// version codes (falling back to cfg.Versions when empty), rendered side by
+// side via render.Compare.
+func renderCompare(cfg config.Config, list, date string, all, raw bool, width int, refresh bool, stdout, stderr io.Writer) int {
var versions []string
if list == "" {
versions = append(versions, cfg.Versions...)
@@ -368,24 +342,24 @@ func runCompare(args []string, cfg config.Config, stdout, stderr io.Writer) int
}
secs, err := readings.Load(cfg, readings.Options{
- Date: *date,
- Refresh: *refresh,
+ Date: date,
+ Refresh: refresh,
Offline: cfg.Offline,
- All: *all,
+ All: all,
})
if err != nil {
fmt.Fprintln(stderr, "lectio:", err)
return 1
}
if len(secs) == 0 {
- fmt.Fprintln(stderr, "lectio: no readings found for", *date)
+ fmt.Fprintln(stderr, "lectio: no readings found for", date)
return 1
}
- w := resolveWidth(*width, true, stdout)
+ w := resolveWidth(width, true, stdout)
- if !*raw {
- banner := bannerFor(*all, *date)
+ if !raw {
+ banner := bannerFor(all, date)
fmt.Fprintln(stdout, banner)
fmt.Fprintln(stdout, strings.Repeat("=", minInt(len(banner), w)))
fmt.Fprintln(stdout)
@@ -394,30 +368,6 @@ func runCompare(args []string, cfg config.Config, stdout, stderr io.Writer) int
return 0
}
-// runUpdate handles "update [--days N] [--from D]": it does not touch
-// config -- liturgy.Harvest owns its own cache/sigla-store paths.
-func runUpdate(args []string, stdout, stderr io.Writer) int {
- fs := flag.NewFlagSet("update", flag.ContinueOnError)
- fs.SetOutput(stderr)
- days := fs.Int("days", 0, "max days to harvest (0 = until the unpublished horizon)")
- from := fs.String("from", today(), "start date as YYYY-MM-DD (default: today)")
- if err := fs.Parse(args); err != nil {
- return 2
- }
- if !dateRe.MatchString(*from) {
- fmt.Fprintf(stderr, "lectio: invalid date %q (want YYYY-MM-DD)\n", *from)
- return 2
- }
-
- added, furthest, err := liturgy.Harvest(*from, *days)
- if err != nil {
- fmt.Fprintln(stderr, "lectio:", err)
- return 1
- }
- fmt.Fprintf(stdout, "harvested %d day(s), furthest %s\n", added, furthest)
- return 0
-}
-
// resolveWidth applies the --width 0 = detect-terminal rule: an explicit
// positive width wins, then a real terminal width if stdout is a tty, then
// a sane per-mode default.