background.go

  1package styles
  2
  3import (
  4	"fmt"
  5	"regexp"
  6	"strings"
  7
  8	"github.com/charmbracelet/lipgloss"
  9)
 10
 11var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m")
 12
 13func getColorRGB(c lipgloss.TerminalColor) (uint8, uint8, uint8) {
 14	r, g, b, a := c.RGBA()
 15
 16	// Un-premultiply alpha if needed
 17	if a > 0 && a < 0xffff {
 18		r = (r * 0xffff) / a
 19		g = (g * 0xffff) / a
 20		b = (b * 0xffff) / a
 21	}
 22
 23	// Convert from 16-bit to 8-bit color
 24	return uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)
 25}
 26
 27// ForceReplaceBackgroundWithLipgloss replaces any ANSI background color codes
 28// in `input` with a single 24โ€‘bit background (48;2;R;G;B).
 29func ForceReplaceBackgroundWithLipgloss(input string, newBgColor lipgloss.TerminalColor) string {
 30	// Precompute our new-bg sequence once
 31	r, g, b := getColorRGB(newBgColor)
 32	newBg := fmt.Sprintf("48;2;%d;%d;%d", r, g, b)
 33
 34	return ansiEscape.ReplaceAllStringFunc(input, func(seq string) string {
 35		const (
 36			escPrefixLen = 2 // "\x1b["
 37			escSuffixLen = 1 // "m"
 38		)
 39
 40		raw := seq
 41		start := escPrefixLen
 42		end := len(raw) - escSuffixLen
 43
 44		var sb strings.Builder
 45		// reserve enough space: original content minus bg codes + our newBg
 46		sb.Grow((end - start) + len(newBg) + 2)
 47
 48		// scan from start..end, token by token
 49		for i := start; i < end; {
 50			// find the next ';' or end
 51			j := i
 52			for j < end && raw[j] != ';' {
 53				j++
 54			}
 55			token := raw[i:j]
 56
 57			// fastโ€‘path: skip "48;5;N" or "48;2;R;G;B"
 58			if len(token) == 2 && token[0] == '4' && token[1] == '8' {
 59				k := j + 1
 60				if k < end {
 61					// find next token
 62					l := k
 63					for l < end && raw[l] != ';' {
 64						l++
 65					}
 66					next := raw[k:l]
 67					if next == "5" {
 68						// skip "48;5;N"
 69						m := l + 1
 70						for m < end && raw[m] != ';' {
 71							m++
 72						}
 73						i = m + 1
 74						continue
 75					} else if next == "2" {
 76						// skip "48;2;R;G;B"
 77						m := l + 1
 78						for count := 0; count < 3 && m < end; count++ {
 79							for m < end && raw[m] != ';' {
 80								m++
 81							}
 82							m++
 83						}
 84						i = m
 85						continue
 86					}
 87				}
 88			}
 89
 90			// decide whether to keep this token
 91			// manually parse ASCII digits to int
 92			isNum := true
 93			val := 0
 94			for p := i; p < j; p++ {
 95				c := raw[p]
 96				if c < '0' || c > '9' {
 97					isNum = false
 98					break
 99				}
100				val = val*10 + int(c-'0')
101			}
102			keep := !isNum ||
103				((val < 40 || val > 47) && (val < 100 || val > 107) && val != 49)
104
105			if keep {
106				if sb.Len() > 0 {
107					sb.WriteByte(';')
108				}
109				sb.WriteString(token)
110			}
111			// advance past this token (and the semicolon)
112			i = j + 1
113		}
114
115		// append our new background
116		if sb.Len() > 0 {
117			sb.WriteByte(';')
118		}
119		sb.WriteString(newBg)
120
121		return "\x1b[" + sb.String() + "m"
122	})
123}