shimmer.go 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. package util
  2. import (
  3. "math"
  4. "os"
  5. "strings"
  6. "time"
  7. "github.com/charmbracelet/lipgloss/v2"
  8. "github.com/charmbracelet/lipgloss/v2/compat"
  9. "github.com/sst/opencode/internal/styles"
  10. )
  11. var (
  12. shimmerStart = time.Now()
  13. trueColorSupport = hasTrueColor()
  14. )
  15. // Shimmer renders text with a moving foreground highlight.
  16. // bg is the background color, dim is the base text color, bright is the highlight color.
  17. func Shimmer(s string, bg compat.AdaptiveColor, _ compat.AdaptiveColor, _ compat.AdaptiveColor) string {
  18. if s == "" {
  19. return ""
  20. }
  21. runes := []rune(s)
  22. n := len(runes)
  23. if n == 0 {
  24. return s
  25. }
  26. pad := 10
  27. period := float64(n + pad*2)
  28. sweep := 2.5
  29. elapsed := time.Since(shimmerStart).Seconds()
  30. pos := (math.Mod(elapsed, sweep) / sweep) * period
  31. half := 2.0
  32. type seg struct {
  33. useHex bool
  34. hex string
  35. bold bool
  36. faint bool
  37. text string
  38. }
  39. segs := make([]seg, 0, n/4)
  40. useHex := trueColorSupport
  41. for i, r := range runes {
  42. ip := float64(i + pad)
  43. dist := math.Abs(ip - pos)
  44. bold := false
  45. faint := true
  46. hex := ""
  47. if dist <= half {
  48. // Simple 3-level brightness based on distance
  49. if dist <= half/3 {
  50. // Center: brightest
  51. bold = true
  52. faint = false
  53. if useHex {
  54. hex = "#ffffff"
  55. }
  56. } else {
  57. // Edge: medium bright
  58. bold = false
  59. faint = false
  60. if useHex {
  61. hex = "#cccccc"
  62. }
  63. }
  64. }
  65. if len(segs) == 0 ||
  66. segs[len(segs)-1].useHex != useHex ||
  67. segs[len(segs)-1].hex != hex ||
  68. segs[len(segs)-1].bold != bold ||
  69. segs[len(segs)-1].faint != faint {
  70. segs = append(segs, seg{useHex: useHex, hex: hex, bold: bold, faint: faint, text: string(r)})
  71. } else {
  72. segs[len(segs)-1].text += string(r)
  73. }
  74. }
  75. baseStyle := styles.NewStyle().Background(bg)
  76. var b strings.Builder
  77. b.Grow(len(s) * 2)
  78. for _, g := range segs {
  79. st := baseStyle
  80. if g.useHex && g.hex != "" {
  81. c := compat.AdaptiveColor{Dark: lipgloss.Color(g.hex), Light: lipgloss.Color(g.hex)}
  82. st = st.Foreground(c)
  83. }
  84. if g.bold {
  85. st = st.Bold(true)
  86. }
  87. if g.faint {
  88. st = st.Faint(true)
  89. }
  90. b.WriteString(st.Render(g.text))
  91. }
  92. return b.String()
  93. }
  94. func hasTrueColor() bool {
  95. c := strings.ToLower(os.Getenv("COLORTERM"))
  96. return strings.Contains(c, "truecolor") || strings.Contains(c, "24bit")
  97. }
  98. func rgbHex(r, g, b int) string {
  99. if r < 0 {
  100. r = 0
  101. }
  102. if r > 255 {
  103. r = 255
  104. }
  105. if g < 0 {
  106. g = 0
  107. }
  108. if g > 255 {
  109. g = 255
  110. }
  111. if b < 0 {
  112. b = 0
  113. }
  114. if b > 255 {
  115. b = 255
  116. }
  117. return "#" + hex2(r) + hex2(g) + hex2(b)
  118. }
  119. func hex2(v int) string {
  120. const digits = "0123456789abcdef"
  121. return string([]byte{digits[(v>>4)&0xF], digits[v&0xF]})
  122. }