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}