123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204(** Color conversion utilities for terminal output.
Provides functions to convert RGB colors to ANSI color codes using
different quantization strategies. *)openUtils(** Reference to the Palette module from spectrum_palette_ppx. *)modulePalette=Spectrum_palette_ppx.Palette(*
What we call "ansi256" here are the xterm 256 color palette
i.e. colours set by using the ESC[38;5;<code>m sequence
The palette is organised such that:
0-15: the basic 'system' colours, RGB values of 0, 128, 255
plus an extra grey of 192,192,192
16-231: non-grey colours, RGB values of 0, 95, 135, 175, 215, 255
so intervals of 40 with darkest interval missing and all offset +15
232-255: greys in intervals of 10, offset and truncated to 8..238
The non-grey colours are organised into 'rows', with the six values of
the B component as columns - the rows start with R:0 and G:0,
incrementing G for the current R before incrementing R.
starting at 16: 0,0,0
The 16 basic colours are organised as:
0: 0,0,0
1-6: combinations of 0,128
7: 192,192,192
8: 128,128,128
9-14: combinations of 0,255 (symmetrical to 1-6)
15: 255,255,255
NOTE: there are no 128+255 combinations, only 0+128 and 0+255
(RGB values according to https://www.ditig.com/256-colors-cheat-sheet)
but terminals are configurable and Wikipedia shows different apps
choose different defaults for the 16 colour base palette:
https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
*)(** Extended Color module with additional types and conversion functions. *)moduleColor=structincludeColor(** RGBA color type with integer components (0-255) and float alpha (0.0-1.0). *)moduleRgba=structtypet={r:int;g:int;b:int;a:float}end(** RGBA color type with float components (0.0-1.0). *)moduleRgba'=structtypet={r:float;g:float;b:float;a:float}end(** Create a color from RGB integer values (0-255). *)letof_rgbrgb=Rgb.(vrgb|>to_gg)(** Convert a color to RGBA with integer components. *)letto_rgbacolor=letc=Gg.Color.to_srgbcolorin{Rgba.r=int_of_float(Float.round(255.*.Gg.Color.rc));g=int_of_float(Float.round(255.*.Gg.Color.gc));b=int_of_float(Float.round(255.*.Gg.Color.bc));a=Gg.Color.ac;}(** Convert a color to RGBA with float components. *)letto_rgba'color=letc=Gg.Color.to_srgbcolorin{Rgba'.r=Gg.Color.rc;g=Gg.Color.gc;b=Gg.Color.bc;a=Gg.Color.ac;}(** Create a color from HSL values. *)letof_hslhsl=Hsl.(vhsl|>to_gg)(** HSVA color type (Hue, Saturation, Value, Alpha). *)moduleHsva=structtypet={h:float;s:float;v:float;a:float}end(** Convert a color to HSVA representation.
https://github.com/Qix-/color-convert/blob/master/conversions.js#L94 *)letto_hsvacolor_v4:Hsva.t=letc=to_rgba'color_v4inletv=max3c.rc.gc.binletdiff=v-.(min3c.rc.gc.b)inletdiffcc'=(v-.c')/.6./.(diff+.1.)/.2.inleth,s=matchdiffwith|0.->0.,0.|_->beginletrdiff=diffcc.randgdiff=diffcc.gandbdiff=diffcc.binlets=diff/.vinleth=ifc.r==vthenbdiff-.gdiffelseifc.g==vthen(1./.3.)+.rdiff-.bdiffelse(2./.3.)+.gdiff-.rdiffinleth=ifh<0.thenh+.1.elseifh>1.thenh-.1.elsehinh,sendin{h=h*.360.;s=s*.100.;v=v*.100.;a=1.;}end(** Converter module type for RGB to ANSI color code conversion. *)moduletypeConverter=sig(** Convert RGB color to ANSI-256 color code (0-255).
@param grey_threshold Optional threshold for grey detection (currently unused). *)valrgb_to_ansi256:?grey_threshold:int->Gg.v4->int(** Convert RGB color to ANSI-16 color code (30-37, 90-97). *)valrgb_to_ansi16:Gg.v4->intend(** Perceptual color converter using LAB color space for nearest-neighbor matching.
This converter uses the CIE LAB color space to find the nearest terminal color,
which provides better perceptual accuracy than Euclidean RGB distance.
For perceptual matching we delegate nearest-colour search to the shared
`spectrum_palettes` modules, which expose [nearest] backed by an octree
built in LAB space (see spectrum_palette_ppx/palette.ml).
For ANSI-16 we search the full 16-colour palette.
For ANSI-256 we preserve historical behaviour by searching only xterm
codes 16..255 (colour cube + greys), excluding basic codes 0..15.
Idea:
Possibly the 'OKLab' colourspace is even better for perceptual matching
See: https://meat.io/oksolar
https://bottosson.github.io/posts/oklab/
...but for now it's convenient that Gg already provides LAB conversion *)modulePerceptual:Converter=structmoduleAnsi16_palette=Spectrum_palettes.Terminal.BasicmoduleAnsi256_palette=Spectrum_palettes.Terminal.Xterm256(* Match historical behaviour: ansi256 conversion targets xterm codes 16-255
(colour cube + greys), not the basic 0-15 ANSI colours. *)letansi256_target_colors=List.filteri(funi_->i>=16)Ansi256_palette.color_listletansi256_nearest=Palette.nearest_of_listansi256_target_colorsletindex_of_color_exncolorstarget~msg=letrecfind_indexi=function|[]->invalid_argmsg|c::rest->ifc=targetthenielsefind_index(i+1)restinfind_index0colorsletrgb_to_ansi16_code(r,g,b)=(* Find the matching color in the palette and return its code *)lettarget=Color.of_rgbrgbinleti=index_of_color_exnAnsi16_palette.color_listtarget~msg:"Not in ANSI 16-color palette"inifi<8then30+ielse90+(i-8)letrgb_to_ansi256?grey_threshold:_color_v4=leti=ansi256_nearestcolor_v4|>index_of_color_exnansi256_target_colors~msg:"Not in ANSI 256-color target palette"ini+16letrgb_to_ansi16color_v4=Ansi16_palette.nearestcolor_v4|>Color.to_rgba|>func->rgb_to_ansi16_code(c.r,c.g,c.b)end