spectrum

Library for colour and formatting in the terminal.

It's a little DSL which is exposed via OCaml Format module's "semantic tags" feature. String tags are defined for ANSI styles such as bold, underline etc and for named colours from the xterm 256-color palette, as well as 24-bit colours via CSS-style hex codes and RGB or HSL values.

It's inspired by the examples given in "Format Unraveled", a paper by Richard Bonichon & Pierre Weis, which also explains the cleverness behind OCaml's highly type-safe format string system.

Goals

Non-goals

See also

These two OCaml libs both provide support for styling console text with the basic 16 ANSI colours, and both also offer other features useful for formatting and interactivity in the terminal.

In contrast, Spectrum focuses only on coloured text styling but offers deeper colour support. Hopefully it's complementary to the stdlib and other libs you may be using.

Installation

It's released on opam, so:

opam install spectrum

Usage

To use Spectrum we have to configure a pretty-print formatter (type: Format.formatter, often just called a ppf) in order to enable our custom tag handling.

This looks something like:

let reset_ppf = Spectrum.prepare_ppf Format.std_formatter;;
Format.printf "@{<green>%s@}\n" "Hello world 👋";;
(* when you're done with Spectrum printing you can use the returned function
   to restore the original ppf state (Spectrum disabled)... *)
reset_ppf ();;

The pattern is @{<TAG-NAME>CONTENT@}.

So in the example above <green> is matching one of the 256 xterm color names. Tag names are case-insensitive.

Spectrum also provides an "instant gratification" interface, where the prepare/reset of the ppf happens automatically. This looks like:

Spectrum.Simple.printf "@{<green>%s@}\n" "Hello world 👋";;

This is handy when doing ad hoc printing, but bear in mind that it is doing the prepare/reset, as well as flushing the output buffer, every time you call one the methods. For most efficient use in your application it's better to use the explicit prepare_ppf form.

NOTE: Format.sprintf uses its own buffer (not the Format.str_formatter singleton) so AFAICT there is no way for prepare_pff to enable Spectrum with it. This means if you need a styled sprintf you have to use Spectrum.Simple.sprintf, or use the longer way with Format.fprintf and your own buffer described in the Format docs.

Tags

You can have arbitrarily nested tags, e.g.:

Spectrum.Simple.printf "@{<green>%s @{<bold>%s@} %s@}\n" "Hello" "world" "I'm here";;

Which should look like:

Screenshot 2022-01-15 at 19 08 09

Above, the tag bold is used to output one the ANSI style codes.

Spectrum defines tags for these styles:

As well as the named palette colours you can directly specify an arbitrary colour using short or long CSS-style hex codes:

Spectrum.Simple.printf "@{<#f0c090>%s@}\n" "Hello world 👋";;
Spectrum.Simple.printf "@{<#f00>%s@}\n" "RED ALERT";;

...or CSS-style rgb(...) or hsl(...) formats:

Spectrum.Simple.printf "@{<rgb(240 192 144)>%s@}\n" "Hello world 👋";;
Spectrum.Simple.printf "@{<hsl(60 100 50)>%s@}\n" "YELLOW ALERT";;
Screenshot 2022-01-15 at 19 16 50

As in CSS, comma separators between the RGB or HSL components are optional.

NOTE: in CSS you would specify HSL colour as (<hue degrees> <saturation>% <lightness>%) but in a format string the % has to be escaped as %%. Since that is ugly Spectrum will also accept HSL colors without % sign (see above). As in CSS, negative Hue angles are supported and angles > 360 will wrap around.

Foreground/background

By default you are setting the "foreground" colour, i.e. the text colour.

Any colour tag can be prefixed with a foreground fg: or background bg: qualifier, e.g.:

Spectrum.Simple.printf "@{<bg:#f00>%s@}\n" "RED ALERT";;
Screenshot 2022-01-15 at 19 24 22

Finally, Spectrum also supports compound tags in comma-separated format, e.g.:

Spectrum.Simple.printf "@{<bg:#f00,bold,yellow>%s@}\n" "RED ALERT";;
Screenshot 2022-01-15 at 19 25 27

Interface

Spectrum provides two versions of the main module:

  1. The default is Spectrum and, like stdlib Format, it will swallow any errors so that invalid tags will simply have no effect on the output string.
  2. Alternatively Spectrum.Exn will raise an exception if your tags are invalid (i.e. malformed or unrecognised colour name, style name).

Both modules expose the same interface:

val prepare_ppf : Format.formatter -> unit -> unit

module Simple : sig
  (** equivalent to [Format.printf] *)
  val printf : ('a, Format.formatter, unit, unit) format4 -> 'a

  (** equivalent to [Format.eprintf] *)
  val eprintf : ('a, Format.formatter, unit, unit) format4 -> 'a

  (** equivalent to [Format.sprintf] *)
  val sprintf : ('a, Format.formatter, unit, string) format4 -> 'a
end

As you can see in the examples in the previous section, Spectrum.Simple.printf works just like Format.printf from the OCaml stdlib, and eprintf and sprintf also work just like their Format counterparts.

Capabilities detection

I've ported the logic from the https://github.com/chalk/supports-color/ nodejs lib, which performs some heuristics based on env vars to determine what level of color support is available in the current terminal.

In most cases you can also override the detected level by setting the FORCE_COLOR env var.

The following method is provided:

Spectrum.Capabilities.supported_color_levels () -> color_level_info

type color_level_info = {
  stdout : color_level;
  stderr : color_level;
}

The following levels are recognised:

type color_level =
  | Unsupported (* FORCE_COLOR=0 or FORCE_COLOR=false *)
  | Basic       (* FORCE_COLOR=1 or FORCE_COLOR=true *)
  | Eight_bit   (* FORCE_COLOR=2 *)
  | True_color  (* FORCE_COLOR=3 *)

Changelog

0.6.0
0.5.0
0.4.0
0.3.0
0.2.0

TODOs