aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bin/main.ml145
-rw-r--r--bin/pretty.ml106
-rw-r--r--man/colitur.120
-rw-r--r--test/cli.t14
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 .
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: