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/pretty.ml | 110 +++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 82 insertions(+), 28 deletions(-) (limited to 'bin/pretty.ml') 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) -- cgit v1.3