diff options
| -rw-r--r-- | bin/main.ml | 145 | ||||
| -rw-r--r-- | bin/pretty.ml | 106 | ||||
| -rw-r--r-- | man/colitur.1 | 20 | ||||
| -rw-r--r-- | test/cli.t | 14 |
4 files changed, 173 insertions, 112 deletions
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 *) +(* 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. *) -(* 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. *) 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) ' ' + "| " ^ 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 -(* 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 +(* 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 . @@ -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: |
