diff options
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) |
