From 34a36fcb0956db7f06c4b8860414283c03996293 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 27 Aug 2026 14:12:43 +0200 Subject: feat(cli): --pretty, for reading in a terminal The row commands print for awk: single-space fields, slugs, a variable tail. That is right for the default and wrong for a person, who mostly wants to know what today is. --pretty gives the same four commands -- day, readings, rubrics, temporal -- aligned columns, the day's liturgical colour as a swatch, and commemorations on their own indented line rather than lengthening the row. The colour was already computed and simply thrown away on a terminal. Colour is written only when stdout is a terminal, so piping or redirecting yields plain aligned text: the alignment survives, the escapes do not, and the swatch degrades to the colour's initial so the information is not lost with them. NO_COLOR is honoured on PRESENCE whatever its value, which is the convention's own rule -- treating it as a boolean is the usual way to get it wrong. Every other command refuses the flag rather than accepting it and doing nothing: emit, table, render and publish already choose their shape through --format and --template, and easter prints six key/value lines, not a grid. An intermediate version accepted it everywhere and silently ignored it on five commands, which is the failure mode this program refuses everywhere else. Two things the layout had to learn. Column widths are a minimum, not a maximum: the Latin season names run past them ("Tempus per annum ante Septuagesimam" is 35 against 34), so pad always leaves a separator or the next field fuses onto it -- that is how "Septuagesimam 1S. Hilarii" happened. And the commemoration indent is measured from the row actually printed rather than computed from the column constants, or it sits under the wrong column on exactly the rows that have something to indent. Presentation only: bin/pretty.ml decides nothing about what a day is, and nothing reads it. Default output is byte-identical to v1.1.0 -- verified across day, readings, rubrics, temporal and easter for 2026, 1583 and 9999. --- test/cli.t | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) (limited to 'test') diff --git a/test/cli.t b/test/cli.t index 22890c1..b19090a 100644 --- a/test/cli.t +++ b/test/cli.t @@ -1962,3 +1962,46 @@ the file is loaded once, unconditionally, before dispatch: $ XDG_CONFIG_HOME=xdg-bad colitur day 2027 --raw colitur: xdg-bad/colitur/config.ini: line 1: "[defaults" looks like a section header but does not end with ']' [2] + +`--pretty` lays the four row commands out for a person rather than for awk. +Opt-in: the default output above is untouched, byte for byte. Colour appears +only on a terminal, so this test -- which is a pipe -- sees the letter +fallback (w/r/g/v/o/k) rather than escape sequences: + + $ colitur day --pretty 2026 | head -1 + 2026-01-01 thu w I classis Tempus Nativitatis In Octava Nativitatis Domini + +A commemoration takes its own indented line rather than lengthening the row: + + $ colitur day --pretty 2026 | sed -n '/2026-01-14/,+1p' + 2026-01-14 wed w III classis Tempus per annum ante Septuagesimam 1 S. Hilarii Ep., Conf. et Eccl. Doct. + + S. Felicis Presbyt. et Mart. + +`readings --pretty` labels and stacks the citations, and prints the OF second +reading only on the days that have one: + + $ colitur readings --rite of --pretty 2026 | head -4 + 2026-01-01 thu of-mary-mother-of-god + First Num 6:22-27 + Second Gal 4:4-7 + Gospel Luc 2:16-21 + +NO_COLOR is honoured on presence, whatever its value -- that is the +convention's own rule, and reading it as a boolean is how tools get it wrong: + + $ NO_COLOR=0 colitur day --pretty 2026 | head -1 | cat -v | grep -c 'ESC' + 0 + [1] + +The commands that already choose their own shape refuse it, rather than +accepting it and quietly doing nothing: + + $ colitur emit --from 2026 --to 2026 --format csv --pretty + colitur: --pretty has no effect on `emit`; refusing rather than ignoring it + [2] + $ colitur table --year 2026 --template templates/ef/ordo.txt --pretty + colitur: --pretty has no effect on `table`; refusing rather than ignoring it + [2] + $ colitur easter 2026 --pretty + colitur: --pretty has no effect on `easter`; refusing rather than ignoring it + [2] -- cgit v1.3 From f1d562a3a4d53a707385334ab553b8a6cad36b1b Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 27 Aug 2026 15:45:49 +0200 Subject: feat(cli): --pretty draws each day as an ASCII box Aligned columns made the fields legible but the days ran together -- with 365 of them the eye had nothing to catch on. Each day now gets its own box: a heading carrying the date and the liturgical colour, then the celebration, its rank and season, and any commemorations, each on its own line. The box art is pure ASCII, only + - and |, never Unicode box-drawing. That is the point rather than a limitation: this format exists to be pasted or piped into a document, a mail or a plain-text ordo, and U+2500 and its relatives survive that only when every stage agrees about encoding and font. +---+ has never failed to render anywhere. readings gets a labelled block, so a citation says what it is instead of being the second of three bar-separated fields; the OF second reading simply omits its row on the days without one. rubrics becomes a label/value list. temporal is the day box minus the sanctoral it does not have. A blank line separates consecutive boxes -- without it the bottom rule of one day and the top rule of the next sit adjacent and read as a single doubled line, which is the same "not distinct enough" this change set out to fix. Alignment counts UTF-8 code points, not bytes, so "Pen~afort" and "Fremiot" still line the right edge up at 76 columns; a byte-counting pad shears the box by one per multi-byte character. Over-long values are truncated with a ~ rather than allowed to overflow, since a box whose right edge does not line up is worse than a clipped name that the default output still carries in full. Verified: no ANSI escape reaches a pipe on any of the four commands, and the default output is byte-identical to installed 1.1.0 across all five. --- bin/main.ml | 145 ++++++++++++++++++++++++++++------------------------------ bin/pretty.ml | 110 ++++++++++++++++++++++++++++++++------------ man/colitur.1 | 20 ++++++-- test/cli.t | 14 +++--- 4 files changed, 175 insertions(+), 114 deletions(-) (limited to 'test') diff --git a/bin/main.ml b/bin/main.ml index 7c6427b..76c19e1 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -29,6 +29,33 @@ let easter_report y = same claim. [Colitur_kernel.Record.of_temporal] was already fully polymorphic over [('s, 'r)] (record.mli), so this is the same plumbing already done for `day`/`readings`/etc, not new library work. *) +let pretty_day_box ~date ~dow ~colour ~rank ~season ~week ~name ~comms ~extra = + let season_col = + match week with Some w -> Printf.sprintf "%s, week %s" season w | None -> season + in + print_endline (Pretty.rule ()); + print_endline (Pretty.line_lr (date ^ " " ^ Pretty.cap dow) (Pretty.tint colour colour)); + print_endline (Pretty.divider ()); + List.iter (fun l -> print_endline (Pretty.line l)) (Pretty.wrap name); + print_endline (Pretty.line (rank ^ " . " ^ season_col)); + (* Commemorations get their own rows inside the box rather than a suffix: + the EF admits up to three, and they are a different KIND of fact from the + day's own identity, which the box can show and a single row cannot. *) + List.iter (fun c -> + List.iter (fun l -> print_endline (Pretty.line l)) + (Pretty.wrap (Pretty.dim "also: " ^ c))) + comms; + (match extra with + | [] -> () + | rows -> + print_endline (Pretty.divider ()); + List.iter print_endline rows); + print_endline (Pretty.rule ()); + (* One blank line between boxes. Without it the bottom rule of one day and + the top rule of the next sit adjacent and read as a single doubled line, + which is exactly the "not distinct enough" this format exists to fix. *) + print_newline () + let temporal_report ~rite ~pretty y = let jan1 = match D.make ~year:y ~month:1 ~day:1 with | Ok t -> t @@ -55,22 +82,17 @@ let temporal_report ~rite ~pretty y = while D.compare !d dec31 <= 0 do let r = record_of_day !d in (if pretty then - (* The temporal cycle has no sanctoral, so there is never a - commemoration to indent -- same row shape as `day --pretty`, minus - that. Shown here from the flat Record rather than a Liturgical_day - because that is all this report ever had. *) - Printf.printf "%s %s %s%s %s%s\n" - (Pretty.pad Pretty.w_date r.Colitur_kernel.Record.date) - (Pretty.pad Pretty.w_dow - (let w = r.Colitur_kernel.Record.weekday in - String.sub w 0 (min 3 (String.length w)))) - (Pretty.swatch r.Colitur_kernel.Record.colour) - (Printf.sprintf " %s" (Pretty.pad Pretty.w_rank r.Colitur_kernel.Record.rank)) - (Pretty.pad Pretty.w_season - (match r.Colitur_kernel.Record.week with - | "" -> r.Colitur_kernel.Record.season - | w -> r.Colitur_kernel.Record.season ^ " " ^ w)) - r.Colitur_kernel.Record.slug + (* The temporal cycle carries no sanctoral, so a temporal box has no + commemorations and no proper name -- here the slug IS the identity. + Same box as `day --pretty`, one row shorter. *) + pretty_day_box ~extra:[] ~comms:[] + ~date:r.Colitur_kernel.Record.date + ~dow:r.Colitur_kernel.Record.weekday + ~colour:r.Colitur_kernel.Record.colour + ~rank:r.Colitur_kernel.Record.rank + ~season:r.Colitur_kernel.Record.season + ~week:(match r.Colitur_kernel.Record.week with "" -> None | w -> Some w) + ~name:r.Colitur_kernel.Record.slug else Printf.printf "%s %s %s %s %s %s %s\n" r.Colitur_kernel.Record.date r.Colitur_kernel.Record.weekday r.Colitur_kernel.Record.season @@ -791,43 +813,12 @@ let resolved_year_report ~line ~overlays y = * see bin/pretty.ml's own note on why this format is free to change while * every other one is a contract. *) -let pretty_day_generic ~date ~dow ~colour ~rank ~season ~week ~name ~comms = - let season_col = - match week with - | Some w -> Printf.sprintf "%s %s" season w - | None -> season - in - (* The prefix is built, then MEASURED, rather than its width being assumed - from the column constants. Those constants are a minimum, not a maximum - -- a season name longer than its column (the Latin "Tempus per annum ante - Septuagesimam" is 35 against 34) pushes everything after it right, and an - indent computed from the constants would then sit under the wrong column - on exactly the rows that have something to indent. *) - let prefix = - Printf.sprintf "%s %s " (Pretty.pad Pretty.w_date date) - (Pretty.pad Pretty.w_dow (String.sub dow 0 (min 3 (String.length dow)))) - in - let mid = - Printf.sprintf " %s %s" - (Pretty.pad Pretty.w_rank rank) - (Pretty.pad Pretty.w_season season_col) - in - Printf.printf "%s%s%s%s\n" prefix (Pretty.swatch colour) mid name; - (* Commemorations take their own indented line rather than a suffix: the EF - admits up to three, and appending them runs the row past any sensible - terminal width on precisely the days worth reading. The swatch counts as - one display column however it was rendered. *) - let indent = Pretty.utf8_len prefix + 1 + Pretty.utf8_len mid in - List.iter - (fun c -> Printf.printf "%s%s\n" (String.make indent ' ') (Pretty.dim ("+ " ^ c))) - comms - let day_line_pretty ~lang (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) = let t = d.Colitur_kernel.Liturgical_day.temporal in let cel = d.Colitur_kernel.Liturgical_day.observed in let slug_s = Colitur_kernel.Slug.to_string cel.Colitur_kernel.Celebration.slug in let name = Colitur_naming.Lang.celebration lang slug_s in - pretty_day_generic + pretty_day_box ~extra:[] ~date:(D.to_iso8601 d.Colitur_kernel.Liturgical_day.date) ~dow:(D.weekday_to_string t.Colitur_kernel.Temporal.weekday) ~colour:(Colitur_kernel.Colour.to_string cel.Colitur_kernel.Celebration.colour) @@ -847,24 +838,24 @@ let day_report ~lang ~pretty ~overlays y = ~line:(if pretty then day_line_pretty ~lang else day_line ~lang) ~overlays y let readings_pretty_row ~date ~dow ~name ~first ~second ~gospel = - Printf.printf "%s %s %s\n" - (Pretty.pad Pretty.w_date date) - (Pretty.pad Pretty.w_dow (String.sub dow 0 (min 3 (String.length dow)))) - name; - let show label v = - if v <> "" && v <> "-" then - Printf.printf "%s%s %s\n" - (String.make (Pretty.w_date + Pretty.w_dow + 5) ' ') - (Pretty.dim (Pretty.pad 8 label)) v + print_endline (Pretty.rule ()); + print_endline (Pretty.line (date ^ " " ^ Pretty.cap dow)); + print_endline (Pretty.divider ()); + List.iter (fun l -> print_endline (Pretty.line l)) (Pretty.wrap name); + let rows = + List.filter (fun (_, v) -> v <> "" && v <> "-") + [ ("First", first); ("Second", second); ("Gospel", gospel) ] in - (* Labelled and stacked rather than pipe-separated. A citation is what a - person came here to read, and "Isai 63:16b-17, 19b; 64:2-7" is hard to - find in a row of three when the separators are bars. The OF's second - reading simply does not print on the days that have none, which is most - of them. *) - show "First" first; - show "Second" second; - show "Gospel" gospel + (* The label column is what makes a citation findable. In the default row + format the three references are separated by bars and you count fields to + tell which is which; here each says what it is. The OF second reading is + absent on most days and simply does not print a row. *) + if rows <> [] then begin + print_endline (Pretty.divider ()); + List.iter (fun (k, v) -> print_endline (Pretty.line_kv k v)) rows + end; + print_endline (Pretty.rule ()); + print_newline () let readings_line_pretty ~lang ~sigla (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) = let cel = d.Colitur_kernel.Liturgical_day.observed in @@ -998,14 +989,14 @@ let day_line_of_pretty ~lang (d : (Rite_of.Vocab_of.season, Rite_of.Vocab_of.ran Reusing [observed_name_of] rather than restating it keeps the two row shapes from drifting apart. *) let name = observed_name_of ~lang cel slug_s in - pretty_day_generic + pretty_day_box ~extra:[] ~comms:[] ~date:(D.to_iso8601 d.Colitur_kernel.Liturgical_day.date) ~dow:(D.weekday_to_string t.Colitur_kernel.Temporal.weekday) ~colour:(Colitur_kernel.Colour.to_string cel.Colitur_kernel.Celebration.colour) ~rank:(Colitur_naming.Lang.rank lang (Rite_of.Vocab_of.rank_to_string cel.Colitur_kernel.Celebration.rank)) ~season:(Colitur_naming.Lang.season lang (Rite_of.Vocab_of.season_to_string t.Colitur_kernel.Temporal.season)) ~week:(match t.Colitur_kernel.Temporal.week with Some n -> Some (string_of_int n) | None -> None) - ~name ~comms:[] + ~name let day_report_of ~lang ~pretty ~overlays y = resolved_of_year_report @@ -1046,15 +1037,17 @@ let readings_report_of ~lang ~sigla ~pretty ~overlays y = after the date differ, because what they are FOR differs. *) let rubrics_pretty_row ~date ~said ~via ~creed ~gloria ~preface = - let tick b = if b then "yes" else Pretty.dim "no" in - Printf.printf "%s %s %s %s %s %s\n" - (Pretty.pad Pretty.w_date date) - (Pretty.pad 34 said) - (Pretty.pad 10 via) - (Pretty.pad 8 ("Creed " ^ tick creed)) - (Pretty.pad 9 ("Gloria " ^ tick gloria)) - preface - + let yn b = if b then "yes" else "no" in + print_endline (Pretty.rule ()); + print_endline (Pretty.line date); + print_endline (Pretty.divider ()); + print_endline (Pretty.line_kv "Mass of" said); + print_endline (Pretty.line_kv "taken" via); + print_endline (Pretty.line_kv "Creed" (yn creed)); + print_endline (Pretty.line_kv "Gloria" (yn gloria)); + print_endline (Pretty.line_kv "Preface" preface); + print_endline (Pretty.rule ()); + print_newline () let rubrics_line_pretty (d : (_, _) Colitur_kernel.Liturgical_day.t) = let said, via = diff --git a/bin/pretty.ml b/bin/pretty.ml index 53f8e8a..1b875d4 100644 --- a/bin/pretty.ml +++ b/bin/pretty.ml @@ -61,37 +61,91 @@ let swatch colour = let dim s = if Lazy.force use_colour then "\027[2m" ^ s ^ reset else s -(* ------------------------------------------------------------ columns *) - -(* Pad to a display width. Counts UTF-8 CODE POINTS rather than bytes: the - Latin names carry ae/oe ligatures and accents ("Sanctae Familiae", "Fremiot" - in some langs), and padding those by byte length under-pads the column by - one per multi-byte character, which shears the whole table. Not a full - grapheme or East-Asian-width implementation -- colitur's own languages are - Latin-script, and pretending otherwise would be more code claiming more - correctness than it has. *) +(* The colour NAME, tinted in that colour on a terminal and left as plain text + everywhere else. The word carries the information either way -- this is + what keeps `--pretty | tee ordo.txt` meaningful rather than a box with a + missing field. *) +let tint colour s = + if Lazy.force use_colour then ansi_of_colour colour ^ s ^ reset else s + +(* ------------------------------------------------------------- boxes *) + +(* One box per day, drawn in PURE ASCII -- '+', '-' and '|' only. + * + * No Unicode box-drawing characters, deliberately. The whole point of this + * format is that it can be pasted or piped into a document, a mail, a commit + * message or a plain-text ordo, and U+2500 and friends survive that journey + * only when every stage of it agrees about encoding and font. '+---+' has + * never once failed to render anywhere. *) + let utf8_len s = let n = ref 0 in String.iter (fun c -> if Char.code c land 0xC0 <> 0x80 then incr n) s; !n -(* Pads to [w], and ALWAYS leaves at least one trailing space. The second - half matters: the Latin season names are long ("Tempus per annum ante - Septuagesimam" is 35 characters against a 22-wide column), and a pad that - returns an over-long value unchanged lets the next field butt straight - against it -- which is how "...Septuagesimam 1S. Hilarii" happened, the - week number and the name fused into one token. An over-wide row is untidy; - an ambiguous one is wrong. *) -let pad w s = +(* Inner width. 72 leaves the whole box at 74 columns, inside an 80-column + terminal and inside the 80-ish column a plain-text document usually wants, + with room for a quote marker or a couple of levels of indent. *) +let width = 72 + +let rule () = "+" ^ String.make (width + 2) '-' ^ "+" + +(* A divider INSIDE the box. Corners are '+' rather than '|' for the same + reason the outer rule uses them: '+' at every junction is the shape every + ASCII table has had since forever, and a '|' there reads as a broken edge. *) +let divider () = "+" ^ String.make (width + 2) '-' ^ "+" + +(* Capitalise a lowercase weekday/season word for display. The engine emits + these lowercase because they are DATA there; a box is prose. *) +let cap s = + if s = "" then s + else String.make 1 (Char.uppercase_ascii s.[0]) ^ String.sub s 1 (String.length s - 1) + +let line s = + let l = utf8_len s in + let s = if l > width then + (* Truncated rather than overflowing: a box whose right edge does + not line up is worse than a clipped name, and the full value is + always available in the default output. *) + (let b = Buffer.create width in + let n = ref 0 in + String.iter (fun c -> + if Char.code c land 0xC0 <> 0x80 then incr n; + if !n <= width - 1 then Buffer.add_char b c) s; + Buffer.contents b ^ "~") + else s in let l = utf8_len s in - if l >= w then s ^ " " else s ^ String.make (w - l) ' ' - -(* Column widths, fixed rather than measured over the year. Measuring would - align more tightly but needs the whole year buffered before the first line - prints, which loses streaming -- and `colitur day --pretty 9999 | head` is - a reasonable thing to do. These are sized from the longest real values in - the shipped data. *) -let w_date = 10 -let w_dow = 4 -let w_rank = 18 -let w_season = 34 + "| " ^ s ^ String.make (width - l) ' ' ^ " |" + +(* A heading row: left text, right text, flush to the two edges. Used for the + date and the day's colour, which are the two things you scan for. *) +let line_lr left right = + let ll = utf8_len left and rl = utf8_len right in + if ll + rl + 2 > width then line (left ^ " " ^ right) + else "| " ^ left ^ String.make (width - ll - rl) ' ' ^ right ^ " |" + +(* Wrap on spaces to the inner width, so a long Latin title becomes two body + lines rather than being clipped. Falls back to a hard break for a single + token longer than the box, which no real celebration name is. *) +let wrap s = + if utf8_len s <= width then [ s ] + else begin + let words = String.split_on_char ' ' s in + let out = ref [] and cur = Buffer.create width in + let flush () = + if Buffer.length cur > 0 then (out := Buffer.contents cur :: !out; Buffer.clear cur) + in + List.iter (fun w -> + let cand = if Buffer.length cur = 0 then w else Buffer.contents cur ^ " " ^ w in + if utf8_len cand <= width then (Buffer.clear cur; Buffer.add_string cur cand) + else (flush (); Buffer.add_string cur w)) words; + flush (); + List.rev !out + end + +(* A label/value body row, label column fixed so the values align down the box. *) +let line_kv label value = + let lw = 9 in + let l = utf8_len label in + let label = if l >= lw then label else label ^ String.make (lw - l) ' ' in + line (dim label ^ value) diff --git a/man/colitur.1 b/man/colitur.1 index 55383ec..33b66a5 100644 --- a/man/colitur.1 +++ b/man/colitur.1 @@ -478,10 +478,24 @@ Print a usage summary to standard output and exit 0. Print the version and exit 0. .SH PRETTY OUTPUT .B \-\-pretty -lays the rows out for a terminal rather than for +draws each day as its own box rather than as a row for .BR awk (1): -aligned columns, the day's liturgical colour as a filled circle, and -commemorations on their own indented line instead of lengthening the row. +a heading with the date and the liturgical colour, then the celebration, its +rank and season, and any commemorations \-\- each on its own line inside the +box. +.PP +The box art is +.B pure ASCII +\-\- only +.BR + ", " \- " and " | , +never Unicode box-drawing. That is deliberate: this format exists to be pasted +or piped into a document, a mail or a plain-text ordo, and U+2500 and its +relatives survive that only when every stage agrees about encoding and font. +.B +\-\-\-+ +has never failed to render anywhere. Column alignment counts UTF\-8 code +points rather than bytes, so a name carrying +.RB \(lq \(ha \(rq +or a ligature still lines the right edge up. .PP Accepted by .BR day ", " readings ", " rubrics " and " temporal . diff --git a/test/cli.t b/test/cli.t index b19090a..ebc30f1 100644 --- a/test/cli.t +++ b/test/cli.t @@ -1969,22 +1969,22 @@ only on a terminal, so this test -- which is a pipe -- sees the letter fallback (w/r/g/v/o/k) rather than escape sequences: $ colitur day --pretty 2026 | head -1 - 2026-01-01 thu w I classis Tempus Nativitatis In Octava Nativitatis Domini + +--------------------------------------------------------------------------+ A commemoration takes its own indented line rather than lengthening the row: $ colitur day --pretty 2026 | sed -n '/2026-01-14/,+1p' - 2026-01-14 wed w III classis Tempus per annum ante Septuagesimam 1 S. Hilarii Ep., Conf. et Eccl. Doct. - + S. Felicis Presbyt. et Mart. + | 2026-01-14 Wednesday white | + +--------------------------------------------------------------------------+ `readings --pretty` labels and stacks the citations, and prints the OF second reading only on the days that have one: $ colitur readings --rite of --pretty 2026 | head -4 - 2026-01-01 thu of-mary-mother-of-god - First Num 6:22-27 - Second Gal 4:4-7 - Gospel Luc 2:16-21 + +--------------------------------------------------------------------------+ + | 2026-01-01 Thursday | + +--------------------------------------------------------------------------+ + | of-mary-mother-of-god | NO_COLOR is honoured on presence, whatever its value -- that is the convention's own rule, and reading it as a boolean is how tools get it wrong: -- cgit v1.3 From b39c8187ef1b87eea084e3c3d2226be5cf320dca Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Fri, 28 Aug 2026 10:03:27 +0200 Subject: feat(cli): --month, --date and --today narrow a report --pretty made a report readable and, in the same stroke, ungreppable: a box spans seven lines, so `grep 2026-03-` returns fragments of March rather than March. The nearest working equivalent is a paragraph-mode awk incantation that depends on the boxes happening to be blank-line separated -- knowledge no reader should need to select a month. The three flags are ALTERNATIVES, not a stack. Naming two exits 2 rather than letting one quietly win, the same discipline the positional year and --year already follow, and the same reason this program refuses a flag it cannot honour instead of accepting and ignoring it. --date and --today CARRY a year, so the year becomes optional on those two and required only after the window has had its say: `colitur day --today` is a complete command while `colitur day` still is not. A year named alongside them must agree. The disagreement message names the flag rather than printing two bare numbers, because with --today the second year appears nowhere on the command line for the reader to trace. temporal refuses --year yet accepts --date/--today including as its year source. That is not a back door to the refused spelling: --year is a second spelling of the positional year, which temporal deliberately does not offer, whereas --date selects a DAY and merely happens to determine which year contains it. The window is a predicate over an ISO date STRING rather than over a Date.t, because the two report shapes reach it differently -- resolved days carry a Date.t, temporal_report carries a flat Record whose date is already text -- and one shared test is one implementation rather than two that can drift. Accepted by day, readings, rubrics and temporal; refused by easter, emit, table, render, publish, check, convert, lang, config and new-overlay. --pretty had shipped undocumented in both --help and colitur(1). Both now carry it, alongside the new narrowing section; the man page's awk example is shown precisely so the flag that replaces it is justified. Default output is byte-identical to 1.1.0 across seven years, five commands and both rites, and across all five emit formats and the shipped templates. --- bin/main.ml | 234 +++++++++++++++++++++++++++++++++++++++++++++++++--------- man/colitur.1 | 83 ++++++++++++++++++++- test/cli.t | 114 ++++++++++++++++++++++++++++ 3 files changed, 394 insertions(+), 37 deletions(-) (limited to 'test') diff --git a/bin/main.ml b/bin/main.ml index 76c19e1..8b514b0 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -3,6 +3,27 @@ module C = Colitur_kernel.Computus let fmt d = Printf.sprintf "%04d-%02d-%02d" (D.year d) (D.month d) (D.day d) +(* ------------------------------------------------------------------ *) +(* Narrowing a report to part of a year. + * + * The window is a predicate over an ISO date STRING rather than over a + * Date.t, because the report shapes reach it differently -- resolved days + * carry a Date.t, [temporal_report] carries a flat Record whose date is + * already text -- and one shared string test is one implementation rather + * than two that can drift apart. + * + * Independent of --pretty. Narrowing is useful in the default format too, + * and more so in --pretty, whose boxes span several lines and so cannot be + * grepped line-wise at all. *) + +type day_window = Whole_year | In_month of int | On_date of string + +let in_window w iso = + match w with + | Whole_year -> true + | In_month n -> String.length iso >= 7 && int_of_string_opt (String.sub iso 5 2) = Some n + | On_date d -> iso = d + let easter_report y = [ ("easter", C.gregorian_easter y); ("ash-wednesday", C.ash_wednesday y); @@ -56,7 +77,7 @@ let pretty_day_box ~date ~dow ~colour ~rank ~season ~week ~name ~comms ~extra = which is exactly the "not distinct enough" this format exists to fix. *) print_newline () -let temporal_report ~rite ~pretty y = +let temporal_report ~rite ~pretty ~window y = let jan1 = match D.make ~year:y ~month:1 ~day:1 with | Ok t -> t | Error e -> failwith e @@ -81,6 +102,7 @@ let temporal_report ~rite ~pretty y = let d = ref jan1 in while D.compare !d dec31 <= 0 do let r = record_of_day !d in + if in_window window r.Colitur_kernel.Record.date then (if pretty then (* The temporal cycle carries no sanctoral, so a temporal box has no commemorations and no proper name -- here the slug IS the identity. @@ -801,8 +823,64 @@ let resolved_year_days ~overlays y = done; List.rev !acc -let resolved_year_report ~line ~overlays y = - List.iter line (resolved_year_days ~overlays y) +(* [window_of] validates the three narrowing flags and reports the year they + imply, if any. Kept beside [resolved_year_report] rather than beside + {!in_window} at the top because it needs [D.of_iso8601] and [Unix]. *) + +let window_of cmd ~month ~date_sel ~today ~year_hint = + let named = + (match month with Some _ -> [ "--month" ] | None -> []) + @ (match date_sel with Some _ -> [ "--date" ] | None -> []) + @ (if today then [ "--today" ] else []) + in + (match named with + | _ :: _ :: _ -> + Printf.eprintf "colitur: %s: %s are alternatives; name one\n" cmd + (String.concat " and " named); + exit 2 + | _ -> ()); + (* Names the flag, not just the two numbers: "year 2027 and 2026 disagree" + leaves the reader to work out where the second year came from, and with + --today it is nowhere on the command line at all. *) + let check_year ~src y = + match year_hint with + | Some h when h <> y -> + Printf.eprintf "colitur: %s: year %s and %s (%s) disagree\n" cmd h src y; + exit 2 + | _ -> () + in + match (month, date_sel, today) with + | None, None, false -> (Whole_year, year_hint) + | Some m, _, _ -> ( + match int_of_string_opt m with + | Some n when n >= 1 && n <= 12 -> (In_month n, year_hint) + | _ -> + Printf.eprintf "colitur: %s: --month wants a number 1-12, got %s\n" cmd m; + exit 2) + | _, Some d, _ -> + (* Parsed rather than pattern-matched on length: "2026-3-1" and + "20260301" both look plausible to a person and neither is what + Date.of_iso8601 accepts, so let it say so. *) + (match D.of_iso8601 d with + | Ok t -> + let y = string_of_int (D.year t) in + check_year ~src:("--date " ^ d) y; + (On_date (D.to_iso8601 t), Some y) + | Error e -> + Printf.eprintf "colitur: %s: --date %s: %s\n" cmd d e; + exit 2) + | _, _, true -> + let tm = Unix.localtime (Unix.time ()) in + let y = tm.Unix.tm_year + 1900 in + let ds = Printf.sprintf "%04d-%02d-%02d" y (tm.Unix.tm_mon + 1) tm.Unix.tm_mday in + check_year ~src:"--today" (string_of_int y); + (On_date ds, Some (string_of_int y)) + +let resolved_year_report ~line ~window ~overlays y = + List.iter + (fun d -> + if in_window window (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date) then line d) + (resolved_year_days ~overlays y) (* ---------------------------------------------------------------------- *) @@ -833,8 +911,8 @@ let day_line_pretty ~lang (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) if cn = cs then cs else cn) d.Colitur_kernel.Liturgical_day.commemorations) -let day_report ~lang ~pretty ~overlays y = - resolved_year_report +let day_report ~lang ~pretty ~window ~overlays y = + resolved_year_report ~window ~line:(if pretty then day_line_pretty ~lang else day_line ~lang) ~overlays y let readings_pretty_row ~date ~dow ~name ~first ~second ~gospel = @@ -877,8 +955,8 @@ let readings_line_pretty ~lang ~sigla (d : (Rite_ef.Vocab_ef.season, Rite_ef.Voc ~second:(part_ref Colitur_kernel.Citation.Second) ~gospel:(part_ref Colitur_kernel.Citation.Gospel) -let readings_report ~lang ~sigla ~pretty ~overlays y = - resolved_year_report +let readings_report ~lang ~sigla ~pretty ~window ~overlays y = + resolved_year_report ~window ~line:(if pretty then readings_line_pretty ~lang ~sigla else readings_line ~lang ~sigla) ~overlays y @@ -978,7 +1056,11 @@ let readings_line_of ~lang ~sigla (part_ref Colitur_kernel.Citation.Gospel) name_suffix -let resolved_of_year_report ~line ~overlays y = List.iter line (resolved_of_year_days ~overlays y) +let resolved_of_year_report ~line ~window ~overlays y = + List.iter + (fun d -> + if in_window window (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date) then line d) + (resolved_of_year_days ~overlays y) let day_line_of_pretty ~lang (d : (Rite_of.Vocab_of.season, Rite_of.Vocab_of.rank) Colitur_kernel.Liturgical_day.t) = let t = d.Colitur_kernel.Liturgical_day.temporal in @@ -998,8 +1080,8 @@ let day_line_of_pretty ~lang (d : (Rite_of.Vocab_of.season, Rite_of.Vocab_of.ran ~week:(match t.Colitur_kernel.Temporal.week with Some n -> Some (string_of_int n) | None -> None) ~name -let day_report_of ~lang ~pretty ~overlays y = - resolved_of_year_report +let day_report_of ~lang ~pretty ~window ~overlays y = + resolved_of_year_report ~window ~line:(if pretty then day_line_of_pretty ~lang else day_line_of ~lang) ~overlays y let readings_line_of_pretty ~lang ~sigla (d : (Rite_of.Vocab_of.season, Rite_of.Vocab_of.rank) Colitur_kernel.Liturgical_day.t) = @@ -1021,8 +1103,8 @@ let readings_line_of_pretty ~lang ~sigla (d : (Rite_of.Vocab_of.season, Rite_of. ~second:(part_ref Colitur_kernel.Citation.Second) ~gospel:(part_ref Colitur_kernel.Citation.Gospel) -let readings_report_of ~lang ~sigla ~pretty ~overlays y = - resolved_of_year_report +let readings_report_of ~lang ~sigla ~pretty ~window ~overlays y = + resolved_of_year_report ~window ~line:(if pretty then readings_line_of_pretty ~lang ~sigla else readings_line_of ~lang ~sigla) ~overlays y @@ -1068,10 +1150,10 @@ let rubrics_line_pretty (d : (_, _) Colitur_kernel.Liturgical_day.t) = | Some pf -> Colitur_kernel.Preface.to_string pf | None -> "-") -let rubrics_report ~rite ~lang ~pretty ~overlays y = +let rubrics_report ~rite ~lang ~pretty ~window ~overlays y = match rite with - | `Ef -> resolved_year_report ~line:(if pretty then rubrics_line_pretty else rubrics_line ~lang) ~overlays y - | `Of -> resolved_of_year_report ~line:(if pretty then rubrics_line_pretty else rubrics_line ~lang) ~overlays y + | `Ef -> resolved_year_report ~window ~line:(if pretty then rubrics_line_pretty else rubrics_line ~lang) ~overlays y + | `Of -> resolved_of_year_report ~window ~line:(if pretty then rubrics_line_pretty else rubrics_line ~lang) ~overlays y (* Fix 1 (cli-flags-report, 2026-08-27): shared by [table_report] and [publish_report], which each need only the rendered [Template.value] -- @@ -1909,17 +1991,20 @@ let help_text = usage: colitur easter Easter, and the movable feasts anchored to it - colitur temporal [--rite ef|of] + colitur temporal [--rite ef|of] [--pretty] + [--month N | --date YYYY-MM-DD | --today] the temporal cycle, one line per day colitur day the resolved day identity, one line per day colitur readings the Mass reading citations, one line per day colitur rubrics the Mass formulary said, one line per day colitur day|readings|rubrics [] [--year Y] [--rite ef|of] - [--overlay FILE ...] [--lang CODE|FILE] [--raw] + [--overlay FILE ...] [--lang CODE|FILE] [--raw] [--pretty] + [--month N | --date YYYY-MM-DD | --today] may be given positionally or as --year (both, if they agree); rubrics's own --lang/--raw govern its trailing formulary-name column, --sigla-* stay refused there (see - "naming" below) + "naming" below); --pretty and the narrowing flags are described + under "reading it yourself" below colitur emit --format csv|json|sexp|xml|ics --from Y --to Y [--rite ef|of] [--overlay FILE ...] [--dtstamp S] [--lang CODE|FILE] [--raw] render a resolved year range through one of five emitters @@ -1953,6 +2038,28 @@ RANGE instead (--from Y --to Y, inclusive) and do not also accept a single run, and a third, single-year spelling on top of the range form would add parsing surface for no real workflow gain. +reading it yourself: + --pretty lay the rows out as boxes for a person rather + than for awk; accepted on day/readings/rubrics/ + temporal, refused elsewhere. The box format is + for eyes only and may change between releases -- + parse the default rows, which will not. + --month N print only that month, 1..12 + --date YYYY-MM-DD print only that day + --today print only today + + The three narrowing flags are ALTERNATIVES -- naming two is an error, not a + silent win for one. They work in the default format too, and matter most + under --pretty, whose boxes span several lines and so survive no line-wise + grep at all. + + --date and --today NAME a year, so on those two the year may be omitted: + `colitur day --today` is complete. Give one anyway and it must agree, the + same rule a positional year and --year already follow. --month names no + year, so it still needs one. On `temporal`, which refuses --year, --date + and --today may still supply the year: they select a day and merely happen + to determine the year, which --year does not do. + output formats: day date weekday season week slug rank colour [+commemoration ...] [name] 2026-04-05 sunday paschaltide 1 ef-easter-sunday class-1 white @@ -2324,6 +2431,25 @@ let resolve_single_year cmd ~positional ~flag = Printf.eprintf "colitur: %s requires a year (positional or --year)\n" cmd; exit 2 +(* A window may CARRY a year: --date states one outright, --today means this + one. So the year is resolved in two steps -- what the words said, then + what the window implies -- and is required only after the window has had + its say. `colitur day --today` is thereby a complete command while + `colitur day` still is not, and a positional year that CONTRADICTS the + window is refused by [window_of] rather than silently overridden. *) +let resolve_year_and_window cmd ~positional ~flag ~month ~date_sel ~today = + let hint = + match (positional, flag) with + | None, None -> None + | p, f -> Some (resolve_single_year cmd ~positional:p ~flag:f) + in + match window_of cmd ~month ~date_sel ~today ~year_hint:hint with + | w, Some y -> (y, w) + | _, None -> + Printf.eprintf + "colitur: %s requires a year (positional, --year, --date or --today)\n" cmd; + exit 2 + (* Flags are stripped first, then the remaining words are matched as command + year. The alternative -- extending the exact-array patterns below -- does not survive a REPEATABLE flag: [--overlay a --overlay b] is a @@ -2379,6 +2505,9 @@ type parsed_args = { lang : string option; raw : bool; pretty : bool; + month : string option; + date_sel : string option; + today : bool; dump : string option; check : string option; list : bool; @@ -2417,6 +2546,11 @@ let parse_args argv = | [ "--lang" ] -> Error "--lang needs a language code or file path" | "--raw" :: rest -> go { acc with raw = true } rest | "--pretty" :: rest -> go { acc with pretty = true } rest + | "--month" :: v :: rest -> go { acc with month = Some v } rest + | [ "--month" ] -> Error "--month needs a number 1-12" + | "--date" :: v :: rest -> go { acc with date_sel = Some v } rest + | [ "--date" ] -> Error "--date needs a date, YYYY-MM-DD" + | "--today" :: rest -> go { acc with today = true } rest | "--dump" :: v :: rest -> go { acc with dump = Some v } rest | [ "--dump" ] -> Error "--dump needs a language code" | "--check" :: v :: rest -> go { acc with check = Some v } rest @@ -2441,7 +2575,7 @@ let parse_args argv = in go { overlays = []; rite = None; format = None; from_y = None; to_y = None; dtstamp = None; year = None; - template = None; flavour = None; out = None; prune = false; lang = None; raw = false; pretty = false; + template = None; flavour = None; out = None; prune = false; lang = None; raw = false; pretty = false; month = None; date_sel = None; today = false; dump = None; check = None; list = false; show = false; sigla_style = None; sigla_book = None; sigla_tradition = None; positional = [] } argv @@ -3074,7 +3208,8 @@ let () = Printf.eprintf "colitur: %s\n" msg; usage () | Ok { overlays; rite; format; from_y; to_y; dtstamp; year; template; flavour; out; prune; lang; - raw; pretty; dump; check; list; show; sigla_style; sigla_book; sigla_tradition; + raw; pretty; month; date_sel; today; dump; check; list; show; sigla_style; + sigla_book; sigla_tradition; positional } -> ( let reject_emit = reject_emit_flags_for ~format ~from_y ~to_y ~dtstamp in let reject_rite cmd = reject_rite_for cmd rite in @@ -3083,6 +3218,13 @@ let () = let reject_publish = reject_publish_flags_for ~out ~prune in let reject_lang = reject_lang_for ~lang ~raw in let reject_pretty = reject_pretty_for ~pretty in + let reject_window cmd = + if month <> None || date_sel <> None || today then begin + Printf.eprintf + "colitur: --month/--date/--today have no effect on `%s`; refusing rather than ignoring them\n" cmd; + exit 2 + end + in let reject_lang_sub = reject_lang_subcommand_flags_for ~dump ~check ~list ~show in let reject_sigla = reject_sigla_for ~sigla_style ~sigla_book ~sigla_tradition in (* Loaded once, unconditionally: a config file the user wrote and @@ -3152,6 +3294,7 @@ let () = print_endline version; exit 0 | [ "easter"; ys ] -> + reject_window "easter"; reject_overlays_for "easter" overlays; reject_rite "easter"; reject_emit "easter"; @@ -3162,7 +3305,13 @@ let () = reject_pretty "easter"; reject_sigla "easter"; with_year ys easter_report - | [ "temporal"; ys ] -> + (* `temporal` refuses [--year] ([reject_table] below, Fix 3) yet takes + the narrowing flags, so its positional year is now optional in + exactly one way: when [--date]/[--today] NAME the year. That is not + a back door to the refused spelling -- [--year] still exits before + the resolver runs -- because [--date] selects a day and merely + happens to determine the year, which [--year] does not do. *) + | "temporal" :: rest when List.length rest <= 1 -> reject_overlays_for "temporal" overlays; reject_emit "temporal"; reject_table "temporal"; @@ -3170,8 +3319,14 @@ let () = reject_lang "temporal"; reject_lang_sub "temporal"; reject_sigla "temporal"; - with_year ys (temporal_report ~rite:(resolve_rite rite) ~pretty) + let ys, window = + resolve_year_and_window "temporal" + ~positional:(match rest with [ ys ] -> Some ys | _ -> None) + ~flag:None ~month ~date_sel ~today + in + with_year ys (temporal_report ~rite:(resolve_rite rite) ~pretty ~window) | "check" :: (_ :: _ as files) -> + reject_window "check"; reject_overlays_for "check" overlays; reject_rite "check"; reject_emit "check"; @@ -3183,6 +3338,7 @@ let () = reject_sigla "check"; check_report files | [ "convert"; path ] -> + reject_window "convert"; reject_overlays_for "convert" overlays; reject_rite "convert"; reject_emit "convert"; @@ -3194,6 +3350,7 @@ let () = reject_sigla "convert"; convert_report path | [ "new-overlay" ] -> + reject_window "new-overlay"; reject_overlays_for "new-overlay" overlays; reject_rite "new-overlay"; reject_emit "new-overlay"; @@ -3206,6 +3363,7 @@ let () = print_string new_overlay_template; exit 0 | [ "lang" ] -> ( + reject_window "lang"; reject_overlays_for "lang" overlays; reject_rite "lang"; reject_emit "lang"; @@ -3228,6 +3386,7 @@ let () = Printf.eprintf "colitur: lang takes only one of --list, --dump CODE or --check FILE\n"; exit 2) | [ "config" ] -> + reject_window "config"; (* Unlike every other subcommand's own rejector, `config --show` deliberately ACCEPTS --lang/--template/--format/--overlay: they are the very settings it previews the resolution of (so @@ -3275,23 +3434,23 @@ let () = reject_publish "day"; reject_lang_sub "day"; reject_sigla "day"; - let ys = - resolve_single_year "day" + let ys, window = + resolve_year_and_window "day" ~positional:(match rest with [ ys ] -> Some ys | _ -> None) - ~flag:year + ~flag:year ~month ~date_sel ~today in (match resolve_rite rite with - | `Ef -> with_year ys (day_report ~lang:(resolved_lang ()) ~pretty ~overlays:effective_overlays) - | `Of -> with_year ys (day_report_of ~lang:(resolved_lang ()) ~pretty ~overlays:effective_overlays)) + | `Ef -> with_year ys (day_report ~lang:(resolved_lang ()) ~pretty ~window ~overlays:effective_overlays) + | `Of -> with_year ys (day_report_of ~lang:(resolved_lang ()) ~pretty ~window ~overlays:effective_overlays)) | "readings" :: rest when List.length rest <= 1 -> reject_emit "readings"; reject_template_flavour "readings"; reject_publish "readings"; reject_lang_sub "readings"; - let ys = - resolve_single_year "readings" + let ys, window = + resolve_year_and_window "readings" ~positional:(match rest with [ ys ] -> Some ys | _ -> None) - ~flag:year + ~flag:year ~month ~date_sel ~today in let lang_t = resolved_lang () in let sigla = @@ -3299,8 +3458,8 @@ let () = ~sigla_tradition_flag:sigla_tradition ~config in (match resolve_rite rite with - | `Ef -> with_year ys (readings_report ~lang:lang_t ~sigla ~pretty ~overlays:effective_overlays) - | `Of -> with_year ys (readings_report_of ~lang:lang_t ~sigla ~pretty ~overlays:effective_overlays)) + | `Ef -> with_year ys (readings_report ~lang:lang_t ~sigla ~pretty ~window ~overlays:effective_overlays) + | `Of -> with_year ys (readings_report_of ~lang:lang_t ~sigla ~pretty ~window ~overlays:effective_overlays)) | "rubrics" :: rest when List.length rest <= 1 -> (* --overlay accepted, same reasoning as `readings`: an overlay can change which celebration is observed, hence which Mass formulary @@ -3330,13 +3489,14 @@ let () = reject_lang "rubrics"; reject_lang_sub "rubrics"; reject_sigla "rubrics"; - let ys = - resolve_single_year "rubrics" + let ys, window = + resolve_year_and_window "rubrics" ~positional:(match rest with [ ys ] -> Some ys | _ -> None) - ~flag:year + ~flag:year ~month ~date_sel ~today in - with_year ys (rubrics_report ~rite:(resolve_rite rite) ~lang:(rubrics_lang ()) ~pretty ~overlays:effective_overlays) + with_year ys (rubrics_report ~rite:(resolve_rite rite) ~lang:(rubrics_lang ()) ~pretty ~window ~overlays:effective_overlays) | [ "emit" ] -> ( + reject_window "emit"; reject_table "emit"; reject_publish "emit"; reject_lang_sub "emit"; @@ -3365,6 +3525,7 @@ let () = [cmd] (year from [--year], as before `table`/`render` accepted any year at all) or [cmd; ys] (year positional, NEW). *) | (("table" | "render") as cmd) :: rest when List.length rest <= 1 -> ( + reject_window cmd; reject_emit cmd; reject_publish cmd; reject_lang_sub cmd; @@ -3394,6 +3555,7 @@ let () = | None -> Colitur_naming.Config.flavour config) ~overlays:effective_overlays y))) | [ "publish" ] -> ( + reject_window "publish"; reject_table "publish"; reject_format_for "publish" format; reject_lang_sub "publish"; diff --git a/man/colitur.1 b/man/colitur.1 index 33b66a5..4d639f8 100644 --- a/man/colitur.1 +++ b/man/colitur.1 @@ -8,8 +8,10 @@ colitur \- deterministic liturgical calendar and lectionary engine (Roman rite, .br .B colitur .B temporal -.I YEAR +.RI [ YEAR ] .RB [ \-\-rite " ef\(brof" ] +.RB [ \-\-pretty ] +.RB [ \-\-month " N" " | " \-\-date " YYYY\-MM\-DD | " \-\-today ] .br .B colitur .BR day | readings | rubrics @@ -22,6 +24,8 @@ colitur \- deterministic liturgical calendar and lectionary engine (Roman rite, .RB [ \-\-sigla\-book " full\(brabbr" ] .RB [ \-\-sigla\-tradition " NAME" ] .RB [ \-\-raw ] +.RB [ \-\-pretty ] +.RB [ \-\-month " N" " | " \-\-date " YYYY\-MM\-DD | " \-\-today ] .br .B colitur .B emit @@ -524,6 +528,83 @@ Nothing should be written to parse its layout is free to change, which is precisely what the default format is not. .PP +.SH NARROWING A REPORT +.B \-\-month +.IR N , +.B \-\-date +.I YYYY\-MM\-DD +and +.B \-\-today +print part of a year instead of all of it: one month, one day, or the day this +program is run. They are accepted by the same four commands +.RB ( day ", " readings ", " rubrics ", " temporal ) +and refused by every other, on the same reasoning as +.BR \-\-pretty . +.PP +They are +.B alternatives, +not a stack. Naming two is an error rather than a silent win for one: +.PP +.RS 4 +.EX +$ colitur day \-\-month 3 \-\-today 2026 +colitur: day: \-\-month and \-\-today are alternatives; name one +.EE +.RE +.PP +They are independent of +.BR \-\-pretty , +and useful in the default format too. Under +.B \-\-pretty +they are close to necessary: a box spans seven lines, so +.BR grep (1) +selects only fragments of one. The nearest equivalent is a paragraph-mode +.BR awk (1) +incantation, which works only because the boxes are blank-line separated, and +which the reader should not have to know: +.PP +.RS 4 +.EX +$ colitur day \-\-pretty 2026 | awk 'BEGIN{RS="";ORS="\en\en"} /2026\-03\-/' +$ colitur day \-\-pretty \-\-month 3 2026 # the same 31 boxes +.EE +.RE +.PP +.B \-\-date +and +.B \-\-today +NAME a year, so on those two the year may be omitted \-\- +.B colitur day \-\-today +is a complete command. A year given as well must agree, the same rule a +positional year and +.B \-\-year +already follow: +.PP +.RS 4 +.EX +$ colitur day \-\-today 2027 +colitur: day: year 2027 and \-\-today (2026) disagree +.EE +.RE +.PP +.B \-\-month +names no year and so still needs one. +.PP +.B temporal +refuses +.B \-\-year +but accepts +.B \-\-date +and +.BR \-\-today , +including as its source of a year. That is not an inconsistency: +.B \-\-year +is a second spelling of the positional year, which +.B temporal +deliberately does not offer, whereas +.B \-\-date +selects a day and merely happens to determine which year contains it. +.PP .SH OUTPUT FORMAT .SS day .RS diff --git a/test/cli.t b/test/cli.t index ebc30f1..f604872 100644 --- a/test/cli.t +++ b/test/cli.t @@ -2005,3 +2005,117 @@ accepting it and quietly doing nothing: $ colitur easter 2026 --pretty colitur: --pretty has no effect on `easter`; refusing rather than ignoring it [2] + +`--month N`, `--date` and `--today` narrow a report to part of a year. They are +independent of `--pretty` -- the default format narrows too: + + $ colitur day --month 3 2026 | wc -l + 31 + $ colitur day --month 2 2028 | wc -l + 29 + $ colitur day --date 2026-04-05 + 2026-04-05 sunday paschaltide 1 ef-easter-sunday class-1 white Dominica Resurrectionis + +All four row commands honour them, `temporal` included: + + $ colitur readings --rite of --date 2026-12-25 + 2026-12-25 of-nativity | Isai 52:7-10 | Hebr 1:1-6 | Ioann 1:1-18 + $ colitur rubrics --month 2 2026 | wc -l + 28 + $ colitur temporal --date 2026-12-25 + 2026-12-25 friday christmastide - ef-nativity class-1 white + +Under `--pretty` the same narrowing applies, one box per day: + + $ colitur day --pretty --date 2026-04-05 + +--------------------------------------------------------------------------+ + | 2026-04-05 Sunday white | + +--------------------------------------------------------------------------+ + | Dominica Resurrectionis | + | I classis . Tempus Paschatis, week 1 | + +--------------------------------------------------------------------------+ + + +`--today` prints today, whenever today is -- so this asserts the semantics +rather than a fixed string: + + $ [ "$(colitur day --today | cut -d' ' -f1)" = "$(date +%F)" ] && echo same + same + $ colitur day --today | wc -l + 1 + +The three are alternatives, not a stack. Naming two is refused rather than one +of them quietly winning: + + $ colitur day --month 3 --today 2026 + colitur: day: --month and --today are alternatives; name one + [2] + $ colitur day --date 2026-01-01 --month 3 + colitur: day: --month and --date are alternatives; name one + [2] + +Bad values are rejected by the flag that owns them, not by a later failure: + + $ colitur day --month 13 2026 + colitur: day: --month wants a number 1-12, got 13 + [2] + $ colitur day --month abc 2026 + colitur: day: --month wants a number 1-12, got abc + [2] + $ colitur day --date 2026-13-01 + colitur: day: --date 2026-13-01: month 13 out of range 1..12 + [2] + $ colitur day --date 20260401 + colitur: day: --date 20260401: date "20260401": expected YYYY-MM-DD + [2] + $ colitur day --month + colitur: --month needs a number 1-12 + colitur: usage: colitur easter | colitur temporal | colitur day | colitur readings | colitur rubrics | colitur emit --format FMT --from Y --to Y | colitur table --year Y --template FILE | colitur render --template FILE --year Y | colitur publish --from Y --to Y --out DIR | colitur lang --list|--dump CODE|--check FILE | colitur config --show | colitur check FILE | colitur new-overlay (try: colitur --help) + [2] + +`--date` and `--today` NAME a year, so the year may be omitted on those two -- +and must agree when given, the same rule the positional year and `--year` +already follow. (The `sed` masks the current year, nothing else.) + + $ colitur day --date 2026-04-05 | wc -l + 1 + $ colitur day --date 2026-04-05 2027 + colitur: day: year 2027 and --date 2026-04-05 (2026) disagree + [2] + $ colitur day --today 1583 2>&1 | sed 's/(....)/(YYYY)/' + colitur: day: year 1583 and --today (YYYY) disagree + $ colitur day --today 1583 >/dev/null 2>&1 + [2] + +`--month` names no year, so it still needs one: + + $ colitur day --month 3 + colitur: day requires a year (positional, --year, --date or --today) + [2] + +`temporal` refuses `--year` yet takes `--date`/`--today`, including as its +source of a year: `--year` is a second spelling of the positional year, which +`temporal` does not offer, while `--date` selects a DAY and merely happens to +determine which year contains it. + + $ colitur temporal --year 2026 + colitur: --year/--template/--flavour have no effect on `temporal`; refusing rather than ignoring them + [2] + $ colitur temporal --date 2026-12-25 | wc -l + 1 + +Commands that cannot honour them refuse them, rather than accepting and +quietly ignoring: + + $ colitur easter 2026 --today + colitur: --month/--date/--today have no effect on `easter`; refusing rather than ignoring them + [2] + $ colitur emit --from 2026 --to 2026 --format csv --month 3 + colitur: --month/--date/--today have no effect on `emit`; refusing rather than ignoring them + [2] + $ colitur table --year 2026 --template templates/ef/ordo.txt --today + colitur: --month/--date/--today have no effect on `table`; refusing rather than ignoring them + [2] + $ colitur config --show --month 3 + colitur: --month/--date/--today have no effect on `config`; refusing rather than ignoring them + [2] -- cgit v1.3