diff options
| author | Lukasz Kasprzak <lukasz@arcofasiagroup.com> | 2026-08-27 14:12:43 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukasz@arcofasiagroup.com> | 2026-08-27 14:12:43 +0200 |
| commit | 34a36fcb0956db7f06c4b8860414283c03996293 (patch) | |
| tree | 09d642eb39545c1fe4a19a6f5afbe3785d6e653e /bin/pretty.ml | |
| parent | 4b3c18d61a243f1ec9c39e6279f7c687402b8dda (diff) | |
| download | colitur-34a36fcb0956db7f06c4b8860414283c03996293.tar.gz colitur-34a36fcb0956db7f06c4b8860414283c03996293.zip | |
feat(cli): --pretty, for reading in a terminal
The row commands print for awk: single-space fields, slugs, a variable tail.
That is right for the default and wrong for a person, who mostly wants to know
what today is.
--pretty gives the same four commands -- day, readings, rubrics, temporal --
aligned columns, the day's liturgical colour as a swatch, and commemorations
on their own indented line rather than lengthening the row. The colour was
already computed and simply thrown away on a terminal.
Colour is written only when stdout is a terminal, so piping or redirecting
yields plain aligned text: the alignment survives, the escapes do not, and the
swatch degrades to the colour's initial so the information is not lost with
them. NO_COLOR is honoured on PRESENCE whatever its value, which is the
convention's own rule -- treating it as a boolean is the usual way to get it
wrong.
Every other command refuses the flag rather than accepting it and doing
nothing: emit, table, render and publish already choose their shape through
--format and --template, and easter prints six key/value lines, not a grid.
An intermediate version accepted it everywhere and silently ignored it on
five commands, which is the failure mode this program refuses everywhere else.
Two things the layout had to learn. Column widths are a minimum, not a
maximum: the Latin season names run past them ("Tempus per annum ante
Septuagesimam" is 35 against 34), so pad always leaves a separator or the next
field fuses onto it -- that is how "Septuagesimam 1S. Hilarii" happened. And
the commemoration indent is measured from the row actually printed rather than
computed from the column constants, or it sits under the wrong column on
exactly the rows that have something to indent.
Presentation only: bin/pretty.ml decides nothing about what a day is, and
nothing reads it. Default output is byte-identical to v1.1.0 -- verified
across day, readings, rubrics, temporal and easter for 2026, 1583 and 9999.
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 |
