color.go

  1package termenv
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"math"
  7	"strings"
  8
  9	"github.com/lucasb-eyer/go-colorful"
 10)
 11
 12// ErrInvalidColor gets returned when a color is invalid.
 13var ErrInvalidColor = errors.New("invalid color")
 14
 15// Foreground and Background sequence codes.
 16const (
 17	Foreground = "38"
 18	Background = "48"
 19)
 20
 21// Color is an interface implemented by all colors that can be converted to an
 22// ANSI sequence.
 23type Color interface {
 24	// Sequence returns the ANSI Sequence for the color.
 25	Sequence(bg bool) string
 26}
 27
 28// NoColor is a nop for terminals that don't support colors.
 29type NoColor struct{}
 30
 31func (c NoColor) String() string {
 32	return ""
 33}
 34
 35// ANSIColor is a color (0-15) as defined by the ANSI Standard.
 36type ANSIColor int
 37
 38func (c ANSIColor) String() string {
 39	return ansiHex[c]
 40}
 41
 42// ANSI256Color is a color (16-255) as defined by the ANSI Standard.
 43type ANSI256Color int
 44
 45func (c ANSI256Color) String() string {
 46	return ansiHex[c]
 47}
 48
 49// RGBColor is a hex-encoded color, e.g. "#abcdef".
 50type RGBColor string
 51
 52// ConvertToRGB converts a Color to a colorful.Color.
 53func ConvertToRGB(c Color) colorful.Color {
 54	var hex string
 55	switch v := c.(type) {
 56	case RGBColor:
 57		hex = string(v)
 58	case ANSIColor:
 59		hex = ansiHex[v]
 60	case ANSI256Color:
 61		hex = ansiHex[v]
 62	}
 63
 64	ch, _ := colorful.Hex(hex)
 65	return ch
 66}
 67
 68// Sequence returns the ANSI Sequence for the color.
 69func (c NoColor) Sequence(_ bool) string {
 70	return ""
 71}
 72
 73// Sequence returns the ANSI Sequence for the color.
 74//
 75//nolint:mnd
 76func (c ANSIColor) Sequence(bg bool) string {
 77	col := int(c)
 78	bgMod := func(c int) int {
 79		if bg {
 80			return c + 10
 81		}
 82		return c
 83	}
 84
 85	if col < 8 {
 86		return fmt.Sprintf("%d", bgMod(col)+30) //nolint:mnd
 87	}
 88	return fmt.Sprintf("%d", bgMod(col-8)+90) //nolint:mnd
 89}
 90
 91// Sequence returns the ANSI Sequence for the color.
 92func (c ANSI256Color) Sequence(bg bool) string {
 93	prefix := Foreground
 94	if bg {
 95		prefix = Background
 96	}
 97	return fmt.Sprintf("%s;5;%d", prefix, c)
 98}
 99
100// Sequence returns the ANSI Sequence for the color.
101func (c RGBColor) Sequence(bg bool) string {
102	f, err := colorful.Hex(string(c))
103	if err != nil {
104		return ""
105	}
106
107	prefix := Foreground
108	if bg {
109		prefix = Background
110	}
111	return fmt.Sprintf("%s;2;%d;%d;%d", prefix, uint8(f.R*255), uint8(f.G*255), uint8(f.B*255)) //nolint:mnd
112}
113
114func xTermColor(s string) (RGBColor, error) {
115	if len(s) < 24 || len(s) > 25 {
116		return RGBColor(""), ErrInvalidColor
117	}
118
119	switch {
120	case strings.HasSuffix(s, string(BEL)):
121		s = strings.TrimSuffix(s, string(BEL))
122	case strings.HasSuffix(s, string(ESC)):
123		s = strings.TrimSuffix(s, string(ESC))
124	case strings.HasSuffix(s, ST):
125		s = strings.TrimSuffix(s, ST)
126	default:
127		return RGBColor(""), ErrInvalidColor
128	}
129
130	s = s[4:]
131
132	prefix := ";rgb:"
133	if !strings.HasPrefix(s, prefix) {
134		return RGBColor(""), ErrInvalidColor
135	}
136	s = strings.TrimPrefix(s, prefix)
137
138	h := strings.Split(s, "/")
139	hex := fmt.Sprintf("#%s%s%s", h[0][:2], h[1][:2], h[2][:2])
140	return RGBColor(hex), nil
141}
142
143func ansi256ToANSIColor(c ANSI256Color) ANSIColor {
144	var r int
145	md := math.MaxFloat64
146
147	h, _ := colorful.Hex(ansiHex[c])
148	for i := 0; i <= 15; i++ {
149		hb, _ := colorful.Hex(ansiHex[i])
150		d := h.DistanceHSLuv(hb)
151
152		if d < md {
153			md = d
154			r = i
155		}
156	}
157
158	return ANSIColor(r)
159}
160
161//nolint:mnd
162func hexToANSI256Color(c colorful.Color) ANSI256Color {
163	v2ci := func(v float64) int {
164		if v < 48 {
165			return 0
166		}
167		if v < 115 {
168			return 1
169		}
170		return int((v - 35) / 40)
171	}
172
173	// Calculate the nearest 0-based color index at 16..231
174	r := v2ci(c.R * 255.0) // 0..5 each
175	g := v2ci(c.G * 255.0)
176	b := v2ci(c.B * 255.0)
177	ci := 36*r + 6*g + b /* 0..215 */
178
179	// Calculate the represented colors back from the index
180	i2cv := [6]int{0, 0x5f, 0x87, 0xaf, 0xd7, 0xff}
181	cr := i2cv[r] // r/g/b, 0..255 each
182	cg := i2cv[g]
183	cb := i2cv[b]
184
185	// Calculate the nearest 0-based gray index at 232..255
186	var grayIdx int
187	average := (r + g + b) / 3
188	if average > 238 {
189		grayIdx = 23
190	} else {
191		grayIdx = (average - 3) / 10 // 0..23
192	}
193	gv := 8 + 10*grayIdx // same value for r/g/b, 0..255
194
195	// Return the one which is nearer to the original input rgb value
196	c2 := colorful.Color{R: float64(cr) / 255.0, G: float64(cg) / 255.0, B: float64(cb) / 255.0}
197	g2 := colorful.Color{R: float64(gv) / 255.0, G: float64(gv) / 255.0, B: float64(gv) / 255.0}
198	colorDist := c.DistanceHSLuv(c2)
199	grayDist := c.DistanceHSLuv(g2)
200
201	if colorDist <= grayDist {
202		return ANSI256Color(16 + ci)
203	}
204	return ANSI256Color(232 + grayIdx)
205}