|
| 1 | +package color |
| 2 | + |
| 3 | +import ( |
| 4 | + "strconv" |
| 5 | + "strings" |
| 6 | + |
| 7 | + "github.com/gookit/color" |
| 8 | + "github.com/jandedobbeleer/oh-my-posh/src/log" |
| 9 | + "github.com/lucasb-eyer/go-colorful" |
| 10 | +) |
| 11 | + |
| 12 | +const ( |
| 13 | + gradientPrefix = "linear-gradient(" |
| 14 | + gradientSuffix = ")" |
| 15 | +) |
| 16 | + |
| 17 | +// IsGradient reports whether c is a gradient definition, e.g. `linear-gradient(#FF0000, #0000FF)`. |
| 18 | +func (c Ansi) IsGradient() bool { |
| 19 | + return strings.HasPrefix(c.String(), gradientPrefix) |
| 20 | +} |
| 21 | + |
| 22 | +// GradientStops performs syntax parsing only: it splits the comma-separated stop list inside |
| 23 | +// `linear-gradient(...)` and trims whitespace around each stop. It does not resolve palette |
| 24 | +// references or validate that a stop is a color. Returns nil when c is not a gradient, the |
| 25 | +// closing paren is missing, the body contains a nested paren (angle/direction syntax is |
| 26 | +// reserved but not implemented), or any stop is empty. |
| 27 | +func (c Ansi) GradientStops() []Ansi { |
| 28 | + if !c.IsGradient() { |
| 29 | + return nil |
| 30 | + } |
| 31 | + |
| 32 | + value := c.String() |
| 33 | + if !strings.HasSuffix(value, gradientSuffix) { |
| 34 | + return nil |
| 35 | + } |
| 36 | + |
| 37 | + body := value[len(gradientPrefix) : len(value)-len(gradientSuffix)] |
| 38 | + if strings.ContainsAny(body, "()") { |
| 39 | + return nil |
| 40 | + } |
| 41 | + |
| 42 | + parts := strings.Split(body, ",") |
| 43 | + stops := make([]Ansi, 0, len(parts)) |
| 44 | + |
| 45 | + for _, part := range parts { |
| 46 | + stop := strings.TrimSpace(part) |
| 47 | + if stop == "" { |
| 48 | + return nil |
| 49 | + } |
| 50 | + |
| 51 | + stops = append(stops, Ansi(stop)) |
| 52 | + } |
| 53 | + |
| 54 | + return stops |
| 55 | +} |
| 56 | + |
| 57 | +// GradientFirst returns the first stop of the gradient. It returns c unchanged when c is not |
| 58 | +// a gradient, or when the gradient syntax is invalid. |
| 59 | +func (c Ansi) GradientFirst() Ansi { |
| 60 | + stops := c.GradientStops() |
| 61 | + if len(stops) == 0 { |
| 62 | + return c |
| 63 | + } |
| 64 | + |
| 65 | + return stops[0] |
| 66 | +} |
| 67 | + |
| 68 | +// GradientLast returns the last stop of the gradient. It returns c unchanged when c is not |
| 69 | +// a gradient, or when the gradient syntax is invalid. |
| 70 | +func (c Ansi) GradientLast() Ansi { |
| 71 | + stops := c.GradientStops() |
| 72 | + if len(stops) == 0 { |
| 73 | + return c |
| 74 | + } |
| 75 | + |
| 76 | + return stops[len(stops)-1] |
| 77 | +} |
| 78 | + |
| 79 | +// GradientCells resolves each stop of the gradient c — keywords like parentBackground against |
| 80 | +// current/parents, palette references through resolver — parses the result as a hex color, and |
| 81 | +// interpolates across cells steps in HCL space. It returns one ready-to-print ANSI color code |
| 82 | +// per cell, honoring the package-level TrueColor flag: a truecolor escape when true, a gookit |
| 83 | +// C256 downgrade when false. cells == 1 returns only the first stop. Returns nil when fewer |
| 84 | +// than two stops resolve to a valid color; the caller falls back to a single collapsed color |
| 85 | +// per the gradient-invalid rule. |
| 86 | +func GradientCells(c Ansi, cells int, resolver String, isBackground bool, current *Set, parents []*Set) []Ansi { |
| 87 | + if cells <= 0 { |
| 88 | + return nil |
| 89 | + } |
| 90 | + |
| 91 | + stops := c.GradientStops() |
| 92 | + if len(stops) == 0 { |
| 93 | + log.Errorf("gradient %s: invalid syntax, expected linear-gradient(stop, stop, ...)", c) |
| 94 | + return nil |
| 95 | + } |
| 96 | + |
| 97 | + colors := make([]colorful.Color, 0, len(stops)) |
| 98 | + |
| 99 | + for _, stop := range stops { |
| 100 | + // a keyword stop (parentBackground, foreground, ...) resolves against the |
| 101 | + // segment context first; a parent gradient collapses to its last stop there. |
| 102 | + resolved := stop.Resolve(current, parents) |
| 103 | + |
| 104 | + resolved, err := resolver.Resolve(resolved) |
| 105 | + if err != nil { |
| 106 | + log.Errorf("gradient %s: unable to resolve stop %s: %s", c, stop, err) |
| 107 | + continue |
| 108 | + } |
| 109 | + |
| 110 | + // the OS accent color only resolves in ToAnsi, to a truecolor payload |
| 111 | + // rather than hex; parseTrueColor recovers the RGB triplet from it. |
| 112 | + if resolved == Accent { |
| 113 | + resolved = resolver.ToAnsi(Accent, false) |
| 114 | + } |
| 115 | + |
| 116 | + clr, err := colorful.Hex(resolved.String()) |
| 117 | + if err != nil { |
| 118 | + var ok bool |
| 119 | + if clr, ok = parseTrueColor(resolved); !ok { |
| 120 | + log.Errorf("gradient %s: stop %s does not resolve to a color, only hex colors, palette references, and keywords resolving to a color can be interpolated", c, stop) |
| 121 | + continue |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + colors = append(colors, clr) |
| 126 | + } |
| 127 | + |
| 128 | + if len(colors) < 2 { |
| 129 | + log.Errorf("gradient %s: needs at least two valid stops, rendering the last stop as a solid color", c) |
| 130 | + return nil |
| 131 | + } |
| 132 | + |
| 133 | + if cached, ok := gradientCellCache[gradientKey(colors, cells, isBackground)]; ok { |
| 134 | + return cached |
| 135 | + } |
| 136 | + |
| 137 | + if cells == 1 { |
| 138 | + return cacheGradientCells(colors, cells, isBackground, []Ansi{ansiFromColorful(colors[0], isBackground)}) |
| 139 | + } |
| 140 | + |
| 141 | + segments := len(colors) - 1 |
| 142 | + result := make([]Ansi, cells) |
| 143 | + |
| 144 | + for i := range cells { |
| 145 | + position := float64(i) / float64(cells-1) * float64(segments) |
| 146 | + |
| 147 | + segment := int(position) |
| 148 | + if segment >= segments { |
| 149 | + segment = segments - 1 |
| 150 | + } |
| 151 | + |
| 152 | + blended := colors[segment].BlendHcl(colors[segment+1], position-float64(segment)).Clamped() |
| 153 | + result[i] = ansiFromColorful(blended, isBackground) |
| 154 | + } |
| 155 | + |
| 156 | + return cacheGradientCells(colors, cells, isBackground, result) |
| 157 | +} |
| 158 | + |
| 159 | +// gradientCellCache memoizes interpolation results keyed on the RESOLVED stop colors |
| 160 | +// (keyword and palette stops resolve before the key is built, so context changes miss |
| 161 | +// the cache correctly), the cell count, the channel, and the TrueColor mode. Prompt |
| 162 | +// rendering is single-threaded (see the terminal writer's package state), so a plain |
| 163 | +// map suffices. Bounded to keep long-lived daemons from growing it unchecked. |
| 164 | +var gradientCellCache = make(map[string][]Ansi) |
| 165 | + |
| 166 | +const gradientCellCacheLimit = 128 |
| 167 | + |
| 168 | +func gradientKey(colors []colorful.Color, cells int, isBackground bool) string { |
| 169 | + buf := make([]byte, 0, 8+len(colors)*12) |
| 170 | + |
| 171 | + for _, clr := range colors { |
| 172 | + r, g, b := clr.RGB255() |
| 173 | + buf = strconv.AppendUint(buf, uint64(r), 10) |
| 174 | + buf = append(buf, ';') |
| 175 | + buf = strconv.AppendUint(buf, uint64(g), 10) |
| 176 | + buf = append(buf, ';') |
| 177 | + buf = strconv.AppendUint(buf, uint64(b), 10) |
| 178 | + buf = append(buf, ',') |
| 179 | + } |
| 180 | + |
| 181 | + buf = strconv.AppendInt(buf, int64(cells), 10) |
| 182 | + |
| 183 | + if isBackground { |
| 184 | + buf = append(buf, 'b') |
| 185 | + } |
| 186 | + |
| 187 | + if TrueColor { |
| 188 | + buf = append(buf, 't') |
| 189 | + } |
| 190 | + |
| 191 | + return string(buf) |
| 192 | +} |
| 193 | + |
| 194 | +func cacheGradientCells(colors []colorful.Color, cells int, isBackground bool, result []Ansi) []Ansi { |
| 195 | + if len(gradientCellCache) >= gradientCellCacheLimit { |
| 196 | + gradientCellCache = make(map[string][]Ansi) |
| 197 | + } |
| 198 | + |
| 199 | + gradientCellCache[gradientKey(colors, cells, isBackground)] = result |
| 200 | + return result |
| 201 | +} |
| 202 | + |
| 203 | +// parseTrueColor parses a truecolor ANSI payload ("38;2;r;g;b" or "48;2;r;g;b") back into |
| 204 | +// a colorful.Color. The OS accent color resolves to this form instead of hex. |
| 205 | +func parseTrueColor(c Ansi) (colorful.Color, bool) { |
| 206 | + parts := strings.Split(c.String(), ";") |
| 207 | + if len(parts) != 5 || (parts[0] != "38" && parts[0] != "48") || parts[1] != "2" { |
| 208 | + return colorful.Color{}, false |
| 209 | + } |
| 210 | + |
| 211 | + rgb := make([]uint8, 3) |
| 212 | + for i, part := range parts[2:] { |
| 213 | + val, err := strconv.ParseUint(part, 10, 8) |
| 214 | + if err != nil { |
| 215 | + return colorful.Color{}, false |
| 216 | + } |
| 217 | + |
| 218 | + rgb[i] = uint8(val) |
| 219 | + } |
| 220 | + |
| 221 | + return colorful.Color{R: float64(rgb[0]) / 255.0, G: float64(rgb[1]) / 255.0, B: float64(rgb[2]) / 255.0}, true |
| 222 | +} |
| 223 | + |
| 224 | +// ansiFromColorful converts an interpolated HCL color to a ready-to-print ANSI code, honoring |
| 225 | +// the package-level TrueColor flag. The truecolor payload is built with strconv appends |
| 226 | +// rather than gookit's fmt.Sprintf path: one allocation per cell instead of four, on what |
| 227 | +// is the gradient hot path's dominant allocation site. |
| 228 | +func ansiFromColorful(c colorful.Color, isBackground bool) Ansi { |
| 229 | + r, g, b := c.RGB255() |
| 230 | + |
| 231 | + if !TrueColor { |
| 232 | + return Ansi(color.RGB(r, g, b, isBackground).C256().String()) |
| 233 | + } |
| 234 | + |
| 235 | + buf := make([]byte, 0, 16) |
| 236 | + |
| 237 | + if isBackground { |
| 238 | + buf = append(buf, "48;2;"...) |
| 239 | + } else { |
| 240 | + buf = append(buf, "38;2;"...) |
| 241 | + } |
| 242 | + |
| 243 | + buf = strconv.AppendUint(buf, uint64(r), 10) |
| 244 | + buf = append(buf, ';') |
| 245 | + buf = strconv.AppendUint(buf, uint64(g), 10) |
| 246 | + buf = append(buf, ';') |
| 247 | + buf = strconv.AppendUint(buf, uint64(b), 10) |
| 248 | + |
| 249 | + return Ansi(buf) |
| 250 | +} |
0 commit comments