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}