package spectrum

  1. Overview
  2. Docs

Source file spectrum.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
module Capabilities = Spectrum_capabilities.Capabilities
module Lexer = Lexer
module Parser = Parser

module type Printer = Spectrum_intf.Printer

let stack_to_esc stack =
  "\027["
  ^ (
    Stack.to_seq stack
    |> List.of_seq
    |> List.rev
    |> String.concat ";"
  )
  ^ "m"

module type Serializer = sig
  val to_code : Parser.token list -> string
end

type Format.stag += Spectrum_stag of Parser.token list

module Stag = struct
  type color =
    | Named of string
    | Hex of string
    | Rgb of int * int * int
    | Hsl of float * float * float

  type t =
    | Bold
    | Dim
    | Italic
    | Underline
    | Blink
    | RapidBlink
    | Inverse
    | Hidden
    | Strikethru
    | Overline
    | Fg of color
    | Bg of color

  let color_def_of_color = function
    | Named name -> Parser.from_name name
    | Hex h ->
      let hex = if String.length h > 0 && h.[0] = '#' then h else "#" ^ h in
      Parser.from_hex hex
    | Rgb (r, g, b) ->
      Parser.from_rgb (string_of_int r) (string_of_int g) (string_of_int b)
    | Hsl (h, s, l) ->
      Parser.from_hsl (string_of_float h) (string_of_float s) (string_of_float l)

  let token_of_t = function
    | Fg c -> Parser.Foreground (color_def_of_color c)
    | Bg c -> Parser.Background (color_def_of_color c)
    | Bold -> Parser.Control Parser.Style.Bold
    | Dim -> Parser.Control Parser.Style.Dim
    | Italic -> Parser.Control Parser.Style.Italic
    | Underline -> Parser.Control Parser.Style.Underline
    | Blink -> Parser.Control Parser.Style.Blink
    | RapidBlink -> Parser.Control Parser.Style.RapidBlink
    | Inverse -> Parser.Control Parser.Style.Inverse
    | Hidden -> Parser.Control Parser.Style.Hidden
    | Strikethru -> Parser.Control Parser.Style.Strikethru
    | Overline -> Parser.Control Parser.Style.Overline

  let stag specs =
    Spectrum_stag (List.map token_of_t specs)
end

(*
  TODO:
  functor-ise these so that the Xterm256 (and Basic?) modules are
  configurable, along with the quantizer (i.e. Perceptual or whatever)
  i.e. could use custom palettes

  for 16 <-> 256 transformations we could build a translator for any
  two Palette modules via ppx, which could brute-force the best matches
  at compile time

  for arbitrary RGB -> palette color we need a runtime method since
  the heuristic shortcuts below won't work for abitrary palettes
  (although how useful is a palette which deviates far from Xterm?
  ...is it still useful to effectively map our RGB -> Xterm -> custom?)

  if we later need an acceleration index for arbitrary palettes,
  prefer integrating the external `oktree` package rather than keeping
  indexing research/prototypes in this repository.
*)
module True_color_Serializer : Serializer = struct
  let to_code tokens =
    let open Parser in
    List.map (function
        | Control s -> string_of_int @@ Style.to_code s
        | Foreground NamedBasicColor c -> string_of_int @@ Basic.to_code c
        | Foreground Named256Color c -> "38;5;" ^ string_of_int @@ Xterm256.to_code c
        | Foreground RgbColor c -> "38;2;" ^ Rgb.to_code c
        | Background NamedBasicColor c -> string_of_int @@ Basic.to_code c + 10
        | Background Named256Color c -> "48;5;" ^ string_of_int @@ Xterm256.to_code c
        | Background RgbColor c -> "48;2;" ^ Rgb.to_code c
      ) tokens
    |> String.concat ";"
end

module Xterm256_Serializer : Serializer = struct
  let to_code tokens =
    let open Parser in
    let quantized = Spectrum_tools.Convert.Perceptual.rgb_to_ansi256 in
    List.map (function
        | Control s -> string_of_int @@ Style.to_code s
        | Foreground NamedBasicColor c -> string_of_int @@ Basic.to_code c
        | Foreground Named256Color c -> "38;5;" ^ string_of_int @@ Xterm256.to_code c
        | Foreground RgbColor c -> "38;5;" ^ string_of_int @@ quantized c
        | Background NamedBasicColor c -> string_of_int @@ Basic.to_code c + 10
        | Background Named256Color c -> "48;5;" ^ string_of_int @@ Xterm256.to_code c
        | Background RgbColor c -> "48;5;" ^ string_of_int @@ quantized c
      ) tokens
    |> String.concat ";"
end

module Basic_Serializer : Serializer = struct
  let to_code tokens =
    let open Parser in
    let quantized = Spectrum_tools.Convert.Perceptual.rgb_to_ansi16 in
    List.map (function
        | Control s -> string_of_int @@ Style.to_code s
        | Foreground NamedBasicColor c -> string_of_int @@ Basic.to_code c
        | Foreground Named256Color c -> Xterm256.to_color c |> quantized |> string_of_int
        | Foreground RgbColor c -> string_of_int @@ quantized c
        | Background NamedBasicColor c -> string_of_int @@ Basic.to_code c + 10
        | Background Named256Color c -> Xterm256.to_color c |> quantized |> (+) 10 |> string_of_int
        | Background RgbColor c -> string_of_int @@ quantized c + 10
      ) tokens
    |> String.concat ";"
end

let make_printer raise_errors to_code =
  let module M = struct
    (** prepare the [ppf] as a side-effect, return [reset] to restore
        original state in the [kfprintf] callback *)
    let prepare_ppf ppf =
      let original_stag_functions = Format.pp_get_formatter_stag_functions ppf () in
      let original_mark_tags_state = Format.pp_get_mark_tags ppf () in
      let reset () =
        Format.pp_print_flush ppf ();
        Format.pp_set_mark_tags ppf original_mark_tags_state;
        Format.pp_set_formatter_stag_functions ppf (original_stag_functions);
      in
      (* if error and not raising, we won't output any codes for the open stag *)
      let conditionally_raise e stack =
        match raise_errors with
        | true -> reset (); raise e
        | false -> Stack.clear stack
      in
      let materialise stack =
        match Stack.is_empty stack with
        | true -> ""
        | false -> stack_to_esc stack
      in
      (*
        Rather than trying to turn on/off individual styles as tags are opened
        and closed it is easier (and probably more reliable) to just output
        the whole style stack at each transition. To ensure accurate rendering
        the first element in the stack is always the 'reset' code.
        See:
        https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
      *)
      let stack = Stack.of_seq @@ Seq.return "0" in
      let mark_open_stag stag =
        let _ =
          match stag with
          | Format.String_tag s -> begin
              match Lexer.tag_to_compound_style @@ String.lowercase_ascii s with
              | Ok c -> Stack.push (to_code c) stack
              | Error e -> conditionally_raise e stack
            end
          | Spectrum_stag tokens ->
            Stack.push (to_code tokens) stack
          | _ -> ignore @@ original_stag_functions.mark_open_stag stag
        in
        materialise stack
      in
      let mark_close_stag _ =
        match Stack.is_empty stack with
        | true -> ""
        | false -> ignore @@ Stack.pop stack; materialise stack
      in
      let color_tag_funs = { original_stag_functions with mark_open_stag; mark_close_stag } in
      Format.pp_set_formatter_stag_functions ppf color_tag_funs;
      Format.pp_set_mark_tags ppf true;
      reset

    (*
      these methods expose a handy one-shot interface that does not require
      explicitly configuring a ppf beforehand, at the cost of being less
      efficient if you have a program making many styled print calls to the
      same ppf
    *)
    module Simple = struct
      let fprintf (ppf : Format.formatter) fmt =
        let reset = prepare_ppf ppf in
        Format.kfprintf (fun _ -> reset ()) ppf fmt

      let printf fmt = fprintf Format.std_formatter fmt

      let eprintf fmt = fprintf Format.err_formatter fmt

      let flush_buffer_formatter buf ppf =
        Format.pp_print_flush ppf ();
        let s = Buffer.contents buf in
        Buffer.reset buf;
        s

      let sprintf fmt =
        let b = Buffer.create 512 in
        let ppf = Format.formatter_of_buffer b in
        let reset = prepare_ppf ppf in
        Format.kfprintf
          (fun ppf ->
             let result = flush_buffer_formatter b ppf in
             reset ();
             result)
          ppf
          fmt
    end
  end in
  (module M : Printer)

(*
  Select the appropriate serializer based on terminal capability detection.

  For Unsupported, we still emit Basic ANSI codes rather than stripping all
  formatting - this allows colors to work in terminals that support them even
  if not detected, while degrading gracefully in truly unsupported environments.
  To fully disable colors, users should avoid calling Spectrum in the first place
  or check Capabilities.supported_color_levels themselves.
*)
let select_serializer () =
  let open Capabilities in
  let levels = supported_color_levels () in
  (* Use stdout capability for selecting serializer *)
  match levels.stdout with
  | True_color -> True_color_Serializer.to_code
  | Eight_bit -> Xterm256_Serializer.to_code
  | Basic -> Basic_Serializer.to_code
  | Unsupported -> Basic_Serializer.to_code

module Exn = (val (make_printer true (select_serializer ())) : Printer)

module Noexn = (val (make_printer false (select_serializer ())) : Printer)

include Noexn

(** Expose serializers and printer constructor for testing purposes under Private module *)
module Private = struct
  module True_color_Serializer = True_color_Serializer
  module Xterm256_Serializer = Xterm256_Serializer
  module Basic_Serializer = Basic_Serializer
  let make_printer = make_printer
end