// Package cli is lectio's flag-driven command dispatcher: it wires config, // the offline readings engine and render together into the `lectio` binary's // Run entry point. package cli import ( "flag" "fmt" "io" "math/rand" "os" "regexp" "strconv" "strings" "time" "github.com/lukaszkasprzak/lectio/internal/bible" "github.com/lukaszkasprzak/lectio/internal/config" "github.com/lukaszkasprzak/lectio/internal/export" "github.com/lukaszkasprzak/lectio/internal/i18n" "github.com/lukaszkasprzak/lectio/internal/liturgy" "github.com/lukaszkasprzak/lectio/internal/naming" "github.com/lukaszkasprzak/lectio/internal/readings" "github.com/lukaszkasprzak/lectio/internal/render" ) const helpText = `lectio — daily Catholic liturgy readings (Polish + 4 versions) Usage: lectio [DATE] [flags] DATE = YYYY-MM-DD (default: today), any position Flags: -a, --all all readings, not just the gospel -b, --bible VER one version's text: wuj,vul,grb,drb -c, --compare LIST versions side by side (comma-separated) -p, --ref REF look up a passage (e.g. "J 3:16") with -b/-c -r, --raw text only, no banner/headings (for piping) -w, --width N wrap width; 0 = detect terminal -l, --lectionary WHICH new|trad (trad -> traditional) --ui-lang LANG override interface/export language: pl|en -P, --pager page reading output (like git); default from config --no-pager never page, even if config sets one --list list all books + abbreviations (dialect from sigla_style) --citation print the day's gospel reference (scripts/cron) and exit --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-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 .tsv) and exit; errors fail, coverage gaps warn --json with --corpus-check, print the report as JSON --rand, --rand-v print a random verse and exit (uses default_version or -b VER) --rand-ch print a random chapter and exit --md export the readings as Markdown (to --out or stdout) --pdf export the readings as PDF (to --out or stdout) --out FILE write --md/--pdf/--calendar output to FILE --calendar YYYY-MM export the month as a printable A4 PDF calendar --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) --form WHICH override calendar form old|new (needs --format) -v, --version print the version and exit -h, --help this help Versions: 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 wuj,drb 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 -p "Jn 3:16" -b vul look up a passage (English sigla) lectio -p "J 3,16" -c wuj,drb Polish sigla when sigla_style=polish/auto+pl UI lectio --list list every book + abbreviations lectio --citation today's gospel reference lectio --week 2026-07-22 a week of gospel references from a date lectio --rand-v a random verse (for cron/prompt) Flags override config. Exit codes: 0 ok, 1 runtime error, 2 usage error (bad flag, bad date, bad version, bad --lectionary). ` const ( defaultWidth = 80 compareDefaultWidth = 120 ) var dateRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) func today() string { return time.Now().Format("2006-01-02") } // Run is the CLI entry point; returns a process exit code. func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { if wantsHelp(args) { fmt.Fprint(stdout, helpText) return 0 } if wantsVersion(args) { fmt.Fprintln(stdout, "lectio "+config.Version) return 0 } if len(args) > 0 && args[0] == "help" { fmt.Fprint(stdout, helpText) return 0 } date, rest, err := extractDate(args) if err != nil { fmt.Fprintln(stderr, "lectio:", err) return 2 } var all, raw, pagerFlag, noPager, citation, week bool var randV, randCh bool var expMD, expPDF bool var output, calendar, uiLang string var bibleVer, compareList, lectionary string var width int var list bool var ref string var liturgy bool var calNew, calCheck string var corpusCheck string var jsonOut bool var format, fromFlag, toFlag, yearFlag, formFlag string 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: wuj,vul,grb,drb") fs.StringVar(&bibleVer, "bible", "", "one version's text: 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.StringVar(&lectionary, "l", "", "new|trad") fs.StringVar(&lectionary, "lectionary", "", "new|trad") fs.StringVar(&uiLang, "ui-lang", "", "override interface/export language: pl|en") fs.BoolVar(&pagerFlag, "P", false, "page reading output (like git)") fs.BoolVar(&pagerFlag, "pager", false, "page reading output (like git)") fs.BoolVar(&noPager, "no-pager", false, "never page, even if config sets one") fs.StringVar(&ref, "p", "", "look up a passage, e.g. \"J 3:16\" (with -b/-c)") fs.StringVar(&ref, "ref", "", "look up a passage, e.g. \"J 3:16\" (with -b/-c)") fs.BoolVar(&list, "list", false, "list all books + abbreviations (in your sigla_style)") fs.BoolVar(&citation, "citation", false, "print the day's gospel reference and exit") fs.BoolVar(&week, "week", false, "list the coming week's gospel references and exit") fs.BoolVar(&randV, "rand", false, "print a random verse from the corpus and exit") fs.BoolVar(&randV, "rand-v", false, "print a random verse from the corpus and exit") fs.BoolVar(&randCh, "rand-ch", false, "print a random chapter from the corpus and exit") fs.BoolVar(&expMD, "md", false, "export the readings as Markdown") fs.BoolVar(&expPDF, "pdf", false, "export the readings as PDF") fs.StringVar(&output, "out", "", "write --md/--pdf output to FILE (default: stdout)") fs.StringVar(&output, "output", "", "write --md/--pdf output to FILE (default: stdout)") fs.StringVar(&calendar, "calendar", "", "export a month (YYYY-MM) as a printable A4 PDF calendar") fs.BoolVar(&liturgy, "L", false, "print the computed liturgical day (offline calendar engine) and exit") 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(&corpusCheck, "corpus-check", "", "validate a bible corpus (code or path to .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") fs.StringVar(&fromFlag, "from", "", "range start YYYY-MM-DD (with --to; needs --format)") fs.StringVar(&toFlag, "to", "", "range end YYYY-MM-DD (with --from; needs --format)") fs.StringVar(&yearFlag, "year", "", "emit the whole year YYYY (needs --format)") fs.StringVar(&formFlag, "form", "", "override calendar form: old|new (needs --format)") 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 calNew != "" { return runCalNew(calNew, stdout, stderr) } if calCheck != "" { return runCalCheck(calCheck, stdout, stderr) } if corpusCheck != "" { return runCorpusCheck(corpusCheck, jsonOut, stdout, stderr) } cfg, err := config.Load() if err != nil { fmt.Fprintln(stderr, "lectio:", err) return 1 } if dir, err := config.CorporaDir(); err == nil { bible.SetUserCorporaDir(dir) } if dir, err := config.NamesDir(); err == nil { naming.SetUserDir(dir) } if dir, err := config.UIDir(); err == nil { i18n.SetUserDir(dir) } if lectionary != "" { cfg.Lectionary = lectionary } if uiLang != "" { cfg.UILanguage = config.NormalizeUILanguage(uiLang) } if format != "" || fromFlag != "" || toFlag != "" || yearFlag != "" || formFlag != "" { return runFeed(cfg, format, date, fromFlag, toFlag, yearFlag, formFlag, stdout, stderr) } if liturgy { return runLiturgy(cfg, date, stdout, stderr) } if citation || week || randV || randCh { tbl, _ := bible.LoadBookTable(userBooksINI()) switch { case randV || randCh: ver, verr := randVersion(cfg, bibleVer) if verr != nil { fmt.Fprintln(stderr, "lectio:", verr) return 2 } return runRand(cfg, tbl, ver, randCh, raw, width, stdout, stderr) case citation: return runCitation(cfg, tbl, date, stdout, stderr) default: return runWeek(cfg, tbl, date, stdout, stderr) } } effAll := all || cfg.All if expMD || expPDF { ver := bibleVer if ver == "" { ver = cfg.DefaultVersion } if !config.ValidVersion(ver) { fmt.Fprintf(stderr, "lectio: unknown version %q (want one of wuj, vul, grb, drb)\n", ver) return 2 } return runExport(cfg, date, ver, effAll, expPDF, output, stdout, stderr) } if calendar != "" { return runCalendar(cfg, calendar, output, stdout, stderr) } effWidth := width if effWidth == 0 { effWidth = cfg.Width } if effWidth == 0 { // Detect the real terminal width now, from stdout, before it may be // replaced by the pager's pipe below -- otherwise paging would wrap at // the default width instead of the terminal's. if tw := termWidth(stdout); tw > 0 { effWidth = tw } } // Validate versions before any pager starts -- don't page an error. var refVersions []string if ref != "" { rv, verr := refLookupVersions(cfg, bibleVer, compareList) if verr != nil { fmt.Fprintln(stderr, "lectio:", verr) return 2 } refVersions = rv } else { // Day-reading paths: bt is a valid version here (compareList wins over // bibleVer, as before, so bibleVer is left unvalidated when a list is given). if bibleVer != "" && compareList == "" && !config.ValidVersion(bibleVer) { fmt.Fprintf(stderr, "lectio: unknown version %q (want one of wuj, vul, grb, drb)\n", bibleVer) return 2 } if compareList != "" { for _, v := range strings.Split(compareList, ",") { if v = strings.TrimSpace(v); v != "" && !config.ValidVersion(v) { fmt.Fprintf(stderr, "lectio: unknown version %q (want one of wuj, vul, grb, drb)\n", v) return 2 } } } } var bookTbl *bible.BookTable if list || ref != "" { tbl, terr := bible.LoadBookTable(userBooksINI()) if terr != nil { fmt.Fprintln(stderr, "lectio:", terr) // warn; tbl is still a usable defaults table } bookTbl = tbl } out := stdout finish := func() {} if pagerRequested(pagerFlag, noPager, cfg) && isTerminalWriter(stdout) { if o, f, ok := startPager(pagerCommand(cfg), stdout, stderr); ok { out = o finish = f } } var code int switch { case list: code = runList(bookTbl, cfg.SiglaLang(), out) case ref != "": code = lookupRef(cfg, bookTbl, ref, refVersions, raw, effWidth, out, stderr) case compareList != "": code = renderCompare(cfg, compareList, date, effAll, raw, effWidth, out, stderr) case bibleVer != "": code = fetchAndPrint(cfg, bibleVer, date, effAll, raw, effWidth, out, stderr) default: code = fetchAndPrint(cfg, cfg.DefaultVersion, date, effAll, raw, effWidth, out, stderr) } finish() return code } // gospelSection returns the gospel section from a (gospel-only, All=false) // load: the section tagged "ewangelia" (modern) or "evangelium" (traditional), // else the first section present. ok is false only when secs is empty. func gospelSection(secs []liturgy.Section) (liturgy.Section, bool) { for _, s := range secs { if s.PartID == "ewangelia" || s.PartID == "evangelium" { return s, true } } if len(secs) > 0 { return secs[0], true } return liturgy.Section{}, false } // gospelCitation returns a section's scripture reference: its Citation field if // set, else the reference parsed out of its Heading (e.g. "Ewangelia (Mt 7, // 1-5)" -> "Mt 7, 1-5"), else "" (source-form, never translated). func gospelCitation(sec liturgy.Section) string { if sec.Citation != "" { return sec.Citation } if c, err := liturgy.ExtractCitation(sec.Heading); err == nil { return c } return "" } // dialectCitation returns a gospel section's reference for display. The offline // loader already renders Section.Citation in the reader's configured sigla // dialect (see readings.citationForms), so this just returns it (cfg and tbl are // kept for signature compatibility with the callers). func dialectCitation(cfg config.Config, tbl *bible.BookTable, sec liturgy.Section) string { return gospelCitation(sec) } // runExport handles --md/--pdf: compute the day's readings offline and write // them as Markdown or PDF to --out FILE (or stdout). Honors date/lectionary/all // and the version. func runExport(cfg config.Config, date, version string, all, asPDF bool, output string, stdout, stderr io.Writer) int { secs, info, err := readings.Load(cfg, readings.Options{Date: date, 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) return 1 } if version == "" || version == "bt" { version = vernacularVersion(cfg) } var data []byte if asPDF { data, err = export.ReadingsPDF(date, info, secs, version, cfg.Lectionary, cfg.UILanguage) if err != nil { fmt.Fprintln(stderr, "lectio:", err) return 1 } } else { data = []byte(export.Markdown(date, info, secs, version, cfg.Lectionary, cfg.UILanguage)) } if output != "" { if err := os.WriteFile(output, data, 0o644); err != nil { fmt.Fprintln(stderr, "lectio:", err) return 1 } return 0 } _, _ = stdout.Write(data) return 0 } // runCalendar handles --calendar YYYY-MM: gather each day's celebration name + // gospel reference + liturgical colour for the month (gospel-only loads, offline // -aware) and render a printable A4 PDF calendar to --out FILE (or stdout). func runCalendar(cfg config.Config, ym, output string, stdout, stderr io.Writer) int { t, err := time.Parse("2006-01", ym) if err != nil { fmt.Fprintf(stderr, "lectio: invalid --calendar %q (want YYYY-MM)\n", ym) return 2 } year, month := t.Year(), int(t.Month()) first := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) var days []export.CalendarDay for d := first; int(d.Month()) == month; d = d.AddDate(0, 0, 1) { cd := export.CalendarDay{Day: d.Day()} if secs, info, lerr := readings.Load(cfg, readings.Options{Date: d.Format("2006-01-02"), All: false}); lerr == nil { cd.Name = info.Name cd.Colour = info.Colour cd.Citation = readings.GospelCitation(secs) } days = append(days, cd) } data, err := export.CalendarPDF(year, month, days, cfg.Lectionary, cfg.UILanguage) if err != nil { fmt.Fprintln(stderr, "lectio:", err) return 1 } if output != "" { if err := os.WriteFile(output, data, 0o644); err != nil { fmt.Fprintln(stderr, "lectio:", err) return 1 } return 0 } _, _ = stdout.Write(data) return 0 } // runCitation handles --citation: fetch the day's gospel (gospel-only) and // print just its scripture reference in the configured sigla dialect (see // dialectCitation), for scripts/cron/prompt use. Honors the resolved cfg // (lectionary/lang/offline) and date. Exit 1 if the day has no gospel // reference (unpublished date, no network while offline, ...). func runCitation(cfg config.Config, tbl *bible.BookTable, date string, stdout, stderr io.Writer) int { secs, _, err := readings.Load(cfg, readings.Options{Date: date, All: false}) if err != nil { fmt.Fprintln(stderr, "lectio:", err) return 1 } sec, ok := gospelSection(secs) if !ok { fmt.Fprintln(stderr, "lectio: no gospel for", date) return 1 } cit := dialectCitation(cfg, tbl, sec) if cit == "" { fmt.Fprintln(stderr, "lectio: no gospel reference for", date) return 1 } fmt.Fprintln(stdout, cit) return 0 } // runWeek handles --week: print the gospel reference for each of the seven days // starting at date, one "YYYY-MM-DD " line per day. A day that // can't be loaded (unpublished, offline gap, no gospel) shows "—" rather than // aborting the run, so the list is always seven lines. Honors cfg // (lectionary/lang/offline). func runWeek(cfg config.Config, tbl *bible.BookTable, date string, stdout, stderr io.Writer) int { start, err := time.Parse("2006-01-02", date) if err != nil { fmt.Fprintln(stderr, "lectio:", err) return 2 } for i := 0; i < 7; i++ { d := start.AddDate(0, 0, i).Format("2006-01-02") cit := "—" secs, _, err := readings.Load(cfg, readings.Options{Date: d, All: false}) if err == nil { if sec, ok := gospelSection(secs); ok { if c := dialectCitation(cfg, tbl, sec); c != "" { cit = c } } } fmt.Fprintf(stdout, "%s %s\n", d, cit) } return 0 } // userBooksTOML returns the bytes of the optional user books.toml, or nil if // it is absent/unreadable (built-in defaults are used). func userBooksINI() []byte { return config.UserBooksINI() } // wantsHelp reports whether -h/--help appears anywhere in args. func wantsHelp(args []string) bool { for _, a := range args { if a == "-h" || a == "--help" { return true } } return false } // wantsVersion reports whether -v/--version appears anywhere in args (like // wantsHelp), so `lectio DATE -v` prints the version rather than erroring on // an "undefined flag". func wantsVersion(args []string) bool { for _, a := range args { if a == "-v" || a == "--version" { return true } } return false } // rangeDateFlags are the flags whose own value is itself a YYYY-MM-DD date // (--from/--to, in either one- or two-dash spelling -- Go's flag package // treats them the same). extractDate must not mistake such a value for the // positional DATE token. var rangeDateFlags = map[string]bool{"--from": true, "-from": true, "--to": true, "-to": true} // 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 other flag's own value can // match dateRe's shape (width is an int, versions/lists/lang/lectionary are // short codes) except --from/--to's, which are skipped explicitly via // rangeDateFlags, 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 skipNext := false for _, a := range args { if skipNext { rest = append(rest, a) skipNext = false continue } if rangeDateFlags[a] { rest = append(rest, a) skipNext = true continue } 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) } if !found { date = today() } return date, rest, nil } // normalizeLectionary maps -l/--lectionary's accepted spellings ("new", // "trad", "traditional") onto the canonical config.Config.Lectionary values // ("new", "traditional") via config.NormalizeLectionary; "" (flag not given) // passes through unchanged so the caller knows to leave config's own // setting alone. func normalizeLectionary(lectionary string) (string, error) { if lectionary == "" { return "", nil } v, ok := config.NormalizeLectionary(lectionary) if !ok { return "", fmt.Errorf("invalid --lectionary %q (want new|trad)", lectionary) } return v, nil } // fetchAndPrint is the shared single-version render path (default version or // -b/--bible): compute the day's readings offline, resolve the reading corpus, // then render each section's heading and render.GatherVersion blocks. func fetchAndPrint(cfg config.Config, version, date string, all, raw bool, width int, stdout, stderr io.Writer) int { secs, dayInfo, err := readings.Load(cfg, readings.Options{Date: date, 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) return 1 } // The single-version daily view renders lectio's own reading corpus (an // explicit reading_version, else the vernacular that matches the UI // language, else the complete Latin Vulgate -- the same choice -L makes), // so the former "bt" niedziela default now maps to a real, offline corpus. if version == "" || version == "bt" { version = vernacularVersion(cfg) } w := resolveWidth(width, false, stdout) if !raw { printDayInfo(stdout, dayInfo) banner := bannerFor(cfg.UILanguage, all, date) fmt.Fprintln(stdout, banner) fmt.Fprintln(stdout, strings.Repeat("=", minInt(len(banner), w))) fmt.Fprintln(stdout) } pieces := make([]string, 0, len(secs)) for _, sec := range secs { pieces = append(pieces, renderSection(sec, version, cfg.Lectionary, cfg.UILanguage, w, raw)) } fmt.Fprintln(stdout, strings.Join(pieces, "\n\n")) return 0 } // printDayInfo prints the day's celebration name -- and, if the source // carries one, its temporal Season on its own line -- above the banner. // Name/Season are never translated (source-language, like the readings/ // citations themselves; see liturgy.DayInfo). A source that yielded no // DayInfo (info.Name == "") prints nothing: the header is a nice-to-have, // never an error condition. Callers only reach this when !raw; --raw skips // it entirely, keeping piped output text-only. // // The CLI has no ANSI styling of its own (unlike the TUI's Faint/dim // styles), so "dim" here is expressed structurally: the Season, if any, // gets its own line under Name rather than sharing emphasis with it. func printDayInfo(stdout io.Writer, info liturgy.DayInfo) { if info.Name == "" { return } name := info.Name if info.Colour != "" { name += " · " + info.Colour } fmt.Fprintln(stdout, name) if info.Season != "" { fmt.Fprintln(stdout, info.Season) } } // bannerFor builds the " DATE" banner: the // "readings" word when every part is shown, "gospel" for the gospel-only // default, and the connective between word and date, all localised via // i18n.Get(lang) -- lang="pl" reproduces ewangelia.py's original Polish // wording exactly ("Ewangelia na DATE" / "Czytania na DATE"); lang="en" // gives "Gospel for DATE" / "Readings for DATE". func bannerFor(lang string, all bool, date string) string { ui := i18n.Get(lang) word := ui.BannerGospel if all { word = ui.BannerReadings } return word + " " + ui.BannerConnective + " " + date } // renderSection formats one section as its heading (unless raw) followed by // render.GatherVersion's blocks, each wrapped to width. The heading's part // label is localised via render.LocalizeHeading (lang); the citation/verse // text is never touched. func renderSection(sec liturgy.Section, version, lectionary, lang string, width int, raw bool) string { var lines []string if !raw { lines = append(lines, render.HeadingWithRef(sec, lang)) lines = append(lines, "") } _, blocks := render.GatherVersion(version, sec, lectionary, lang) for _, b := range blocks { lines = append(lines, wrapText(b, width)) } return strings.TrimRight(strings.Join(lines, "\n"), "\n") } // 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, stdout, stderr io.Writer) int { var versions []string if list == "" { versions = append(versions, cfg.Versions...) } else { for _, v := range strings.Split(list, ",") { v = strings.TrimSpace(v) if v == "" { continue } versions = append(versions, v) } } for _, v := range versions { if !config.ValidVersion(v) { fmt.Fprintf(stderr, "lectio: unknown version %q (want one of wuj, vul, grb, drb)\n", v) return 2 } } versions = render.EffectiveVersions(versions, cfg.Lectionary, cfg.Offline) secs, dayInfo, err := readings.Load(cfg, readings.Options{Date: date, 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) return 1 } w := resolveWidth(width, true, stdout) if !raw { printDayInfo(stdout, dayInfo) banner := bannerFor(cfg.UILanguage, all, date) fmt.Fprintln(stdout, banner) fmt.Fprintln(stdout, strings.Repeat("=", minInt(len(banner), w))) fmt.Fprintln(stdout) } fmt.Fprintln(stdout, render.Compare(secs, versions, w, cfg.Lectionary, cfg.UILanguage)) return 0 } // randVersion picks the corpus version for --rand: an explicit -b VER (any of // the four is accepted), else the configured default_version. An unset default // transparently falls back to the first corpus version in cfg.Versions (else // "wuj"), so --rand always yields a passage. Only an unknown version code is an // error. func randVersion(cfg config.Config, bibleVer string) (string, error) { v := bibleVer if v == "" { v = cfg.DefaultVersion } if v != "" && !config.ValidVersion(v) { return "", fmt.Errorf("unknown version %q (want one of wuj, vul, grb, drb)", v) } if isCorpusVersion(v) { return v, nil } for _, c := range cfg.Versions { if isCorpusVersion(c) { return c, nil } } return "wuj", nil } // runRand prints a random verse (chapter=false) or whole chapter (chapter=true) // from version's embedded corpus, for a "verse of the day" cron/pipe. It picks a // random book -> chapter -> verse (not perfectly uniform across all verses, but // fine for the purpose). Non-raw prints the reference (in cfg's sigla dialect) // then the text; raw prints just the text (verse-numbered for a chapter). func runRand(cfg config.Config, tbl *bible.BookTable, version string, chapter, raw bool, width int, stdout, stderr io.Writer) int { books := bible.CorpusBooks(version) if len(books) == 0 { fmt.Fprintf(stderr, "lectio: no corpus for version %q\n", version) return 1 } rng := rand.New(rand.NewSource(time.Now().UnixNano())) var book string var chap int var verses []bible.Verse for tries := 0; tries < 20 && len(verses) == 0; tries++ { book = books[rng.Intn(len(books))] chaps := bible.Chapters(version, book) if len(chaps) == 0 { continue } chap = chaps[rng.Intn(len(chaps))] verses = bible.Verses(version, book, chap) } if len(verses) == 0 { fmt.Fprintln(stderr, "lectio: could not pick a random passage") return 1 } w := resolveWidth(width, false, stdout) dialect := cfg.SiglaLang() if chapter { if !raw { fmt.Fprintln(stdout, fmt.Sprintf("%s %d", tbl.Abbrev(dialect, book), chap)) fmt.Fprintln(stdout) } lines := make([]string, 0, len(verses)) for _, v := range verses { lines = append(lines, wrapText(fmt.Sprintf("%d %s", v.Verse, v.Text), w)) } fmt.Fprintln(stdout, strings.Join(lines, "\n")) return 0 } v := verses[rng.Intn(len(verses))] if !raw { fmt.Fprintln(stdout, tbl.FormatRef(dialect, fmt.Sprintf("%s %d:%d", book, chap, v.Verse))) fmt.Fprintln(stdout) } fmt.Fprintln(stdout, wrapText(v.Text, w)) return 0 } // isCorpusVersion reports whether v is a scripture-text version with an // embedded corpus (wuj/vul/grb/drb) a passage can be looked up in. Every valid // version now has a corpus, so this is exactly config.ValidVersion. func isCorpusVersion(v string) bool { return config.ValidVersion(v) } // refLookupVersions resolves which corpus version(s) `--ref` should look a // passage up in. Precedence: an explicit -c LIST, // then a single -b VER, then a sensible default (the configured default_version // if it has a corpus, else the first corpus version in cfg.Versions). It errors // (exit 2 in the caller) on any non-corpus version or if nothing usable is // configured. func refLookupVersions(cfg config.Config, bibleVer, compareList string) ([]string, error) { if compareList != "" { var versions []string for _, v := range strings.Split(compareList, ",") { if v = strings.TrimSpace(v); v == "" { continue } if !isCorpusVersion(v) { return nil, fmt.Errorf("--ref cannot use version %q; pass a corpus version (wuj, vul, grb, drb)", v) } versions = append(versions, v) } if len(versions) == 0 { return nil, fmt.Errorf("--ref needs at least one corpus version in -c") } return versions, nil } if bibleVer != "" { if !isCorpusVersion(bibleVer) { return nil, fmt.Errorf("--ref cannot use version %q; pass a corpus version (wuj, vul, grb, drb)", bibleVer) } return []string{bibleVer}, nil } if isCorpusVersion(cfg.DefaultVersion) { return []string{cfg.DefaultVersion}, nil } for _, v := range cfg.Versions { if isCorpusVersion(v) { return []string{v}, nil } } return nil, fmt.Errorf("--ref needs a corpus version; pass -b wuj|vul|grb|drb") } // lookupRef renders a passage lookup (-p/--ref). It parses the typed reference // in the resolved sigla dialect (bookTbl.ParseRef -- dialect-scoped book names // and number syntax) into an English colon-style reference, then reuses the // same render path as the readings with lectionary="traditional" (the ref is // already in target form, looked up literally per version -- no cross-version // psalm renumbering). versions is the validated corpus set. raw omits the // header. Exit 2 on an unparseable ref, exit 1 if no requested version has the // passage. func lookupRef(cfg config.Config, tbl *bible.BookTable, ref string, versions []string, raw bool, width int, stdout, stderr io.Writer) int { ref = strings.TrimSpace(ref) engRef, ok := tbl.ParseRef(cfg.SiglaLang(), ref) if !ok { fmt.Fprintf(stderr, "lectio: could not read reference %q in the %s dialect; see 'lectio --list'\n", ref, cfg.SiglaLang()) return 2 } sec := liturgy.Section{Citation: engRef, Heading: engRef} found := false for _, v := range versions { if vs, _ := bible.Lookup(v, engRef); len(vs) > 0 { found = true break } } if !found { fmt.Fprintf(stderr, "lectio: no text found for %q in %s\n", ref, strings.Join(versions, ", ")) return 1 } w := width if w <= 0 { if len(versions) > 1 { w = compareDefaultWidth } else { w = defaultWidth } } if !raw { fmt.Fprintln(stdout, ref) fmt.Fprintln(stdout, strings.Repeat("=", minInt(len([]rune(ref)), w))) fmt.Fprintln(stdout) } if len(versions) == 1 { label, blocks := render.GatherVersion(versions[0], sec, "traditional", cfg.UILanguage) if !raw { fmt.Fprintln(stdout, label) fmt.Fprintln(stdout) } lines := make([]string, 0, len(blocks)) for _, b := range blocks { lines = append(lines, render.Wrap(b, w)) } fmt.Fprintln(stdout, strings.Join(lines, "\n")) return 0 } fmt.Fprintln(stdout, render.Compare([]liturgy.Section{sec}, versions, w, "traditional", cfg.UILanguage)) return 0 } // runList handles --list: print every book of the sigla dialect (scriptural // order) as " ", followed by a "Versions:" block listing // every known bible corpus -- built-in and user/drop-in alike (see // bible.Corpora, which SetUserCorporaDir keeps current) -- as " //