aboutsummaryrefslogtreecommitdiff
path: root/bin/pretty.ml
diff options
context:
space:
mode:
Diffstat (limited to 'bin/pretty.ml')
-rw-r--r--bin/pretty.ml97
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