diff options
Diffstat (limited to 'bin/pretty.ml')
| -rw-r--r-- | bin/pretty.ml | 97 |
1 files changed, 97 insertions, 0 deletions
diff --git a/bin/pretty.ml b/bin/pretty.ml new file mode 100644 index 0000000..53f8e8a --- /dev/null +++ b/bin/pretty.ml @@ -0,0 +1,97 @@ +(* 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 |
