(* pretty -- terminal presentation for the row commands. * * This module is PRESENTATION ONLY. It never decides what a day is, only how * it is shown, and nothing else in colitur reads it. That separation matters * here more than usual: every other output format this program has is a * contract something parses, and a change to one is a breaking change. This * one is for a person reading a terminal, so it is free to change. * * The default output is deliberately unaffected. `--pretty` is opt-in, and a * command that does not take it refuses rather than ignoring it, like every * other flag here. *) (* ------------------------------------------------------------ colour *) (* Colour is emitted only when stdout is a terminal AND NO_COLOR is unset. * * The TTY test is what keeps `colitur day --pretty 2026 | less` and * `> file` free of escape sequences: alignment survives the pipe, colour does * not, which is the behaviour a person actually wants from both. * * NO_COLOR (https://no-color.org) is honoured on PRESENCE, whatever its * value -- that is the convention's own rule, and reading it as a boolean * ("NO_COLOR=0 means colour") is the usual way tools get it wrong. *) let use_colour = lazy (match Sys.getenv_opt "NO_COLOR" with | Some _ -> false | None -> ( try Unix.isatty Unix.stdout with Unix.Unix_error _ -> false)) (* The six liturgical colours the engine can emit, as the nearest sensible ANSI. Rose is the one that needs a note: it is a distinct liturgical colour (Gaudete, Laetare), not a shade of red, so it gets bright magenta rather than being folded into red -- collapsing them would lose a distinction the calendar deliberately makes. Black is rendered bright-black (grey) because true black is invisible on a dark terminal, which is where this is mostly read. *) let ansi_of_colour = function | "white" -> "\027[97m" | "red" -> "\027[31m" | "green" -> "\027[32m" | "violet" -> "\027[35m" | "rose" -> "\027[95m" | "black" -> "\027[90m" | _ -> "\027[37m" let reset = "\027[0m" (* A filled circle in the day's colour. Chosen over tinting the whole row: violet and black text are hard to read on a dark background, and a fully coloured line reads as a status indicator (red = error) rather than as liturgical information. *) let swatch colour = if Lazy.force use_colour then ansi_of_colour colour ^ "\xe2\x97\x8f" ^ reset else (* Without colour the swatch would be six identical dots, carrying nothing. Print the colour's own initial instead, so the information survives a pipe rather than silently vanishing with the escapes. *) (match colour with | "white" -> "w" | "red" -> "r" | "green" -> "g" | "violet" -> "v" | "rose" -> "o" | "black" -> "k" | _ -> "?") 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. *) 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 = 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