diff options
| author | Lukasz Kasprzak <lukasz@arcofasiagroup.com> | 2026-08-27 15:45:49 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukasz@arcofasiagroup.com> | 2026-08-27 15:45:49 +0200 |
| commit | f1d562a3a4d53a707385334ab553b8a6cad36b1b (patch) | |
| tree | f3bb14fbdd1ddb78aadb25168e8af17491ae953c /bin/pretty.ml | |
| parent | 34a36fcb0956db7f06c4b8860414283c03996293 (diff) | |
| download | colitur-f1d562a3a4d53a707385334ab553b8a6cad36b1b.tar.gz colitur-f1d562a3a4d53a707385334ab553b8a6cad36b1b.zip | |
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.
Diffstat (limited to 'bin/pretty.ml')
| -rw-r--r-- | bin/pretty.ml | 106 |
1 files changed, 80 insertions, 26 deletions
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) |
