background.go 2.6 KB

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