summaryrefslogtreecommitdiff
path: root/bin/pretty.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukasz@arcofasiagroup.com>2026-08-27 15:45:49 +0200
committerLukasz Kasprzak <lukasz@arcofasiagroup.com>2026-08-27 15:45:49 +0200
commitf1d562a3a4d53a707385334ab553b8a6cad36b1b (patch)
treef3bb14fbdd1ddb78aadb25168e8af17491ae953c /bin/pretty.ml
parent34a36fcb0956db7f06c4b8860414283c03996293 (diff)
downloadcolitur-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.ml106
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)