overlay.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. package layout
  2. import (
  3. "fmt"
  4. "regexp"
  5. "strings"
  6. "unicode/utf8"
  7. "github.com/charmbracelet/lipgloss/v2"
  8. "github.com/charmbracelet/lipgloss/v2/compat"
  9. chAnsi "github.com/charmbracelet/x/ansi"
  10. "github.com/muesli/ansi"
  11. "github.com/muesli/reflow/truncate"
  12. "github.com/muesli/termenv"
  13. "github.com/sst/opencode/internal/util"
  14. )
  15. var (
  16. // ANSI escape sequence regex
  17. ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`)
  18. )
  19. // Split a string into lines, additionally returning the size of the widest line.
  20. func getLines(s string) (lines []string, widest int) {
  21. lines = strings.Split(s, "\n")
  22. for _, l := range lines {
  23. w := ansi.PrintableRuneWidth(l)
  24. if widest < w {
  25. widest = w
  26. }
  27. }
  28. return lines, widest
  29. }
  30. // overlayOptions holds configuration for overlay rendering
  31. type overlayOptions struct {
  32. whitespace *whitespace
  33. border bool
  34. borderColor *compat.AdaptiveColor
  35. }
  36. // OverlayOption sets options for overlay rendering
  37. type OverlayOption func(*overlayOptions)
  38. // PlaceOverlay places fg on top of bg.
  39. func PlaceOverlay(
  40. x, y int,
  41. fg, bg string,
  42. opts ...OverlayOption,
  43. ) string {
  44. fgLines, fgWidth := getLines(fg)
  45. bgLines, bgWidth := getLines(bg)
  46. bgHeight := len(bgLines)
  47. fgHeight := len(fgLines)
  48. // Parse options
  49. options := &overlayOptions{
  50. whitespace: &whitespace{},
  51. }
  52. for _, opt := range opts {
  53. opt(options)
  54. }
  55. // Adjust for borders if enabled
  56. if options.border {
  57. // Add space for left and right borders
  58. adjustedFgWidth := fgWidth + 2
  59. // Adjust placement to account for borders
  60. x = util.Clamp(x, 0, bgWidth-adjustedFgWidth)
  61. y = util.Clamp(y, 0, bgHeight-fgHeight)
  62. // Pad all foreground lines to the same width for consistent borders
  63. for i := range fgLines {
  64. lineWidth := ansi.PrintableRuneWidth(fgLines[i])
  65. if lineWidth < fgWidth {
  66. fgLines[i] += strings.Repeat(" ", fgWidth-lineWidth)
  67. }
  68. }
  69. } else {
  70. if fgWidth >= bgWidth && fgHeight >= bgHeight {
  71. // FIXME: return fg or bg?
  72. return fg
  73. }
  74. // TODO: allow placement outside of the bg box?
  75. x = util.Clamp(x, 0, bgWidth-fgWidth)
  76. y = util.Clamp(y, 0, bgHeight-fgHeight)
  77. }
  78. var b strings.Builder
  79. for i, bgLine := range bgLines {
  80. if i > 0 {
  81. b.WriteByte('\n')
  82. }
  83. if i < y || i >= y+fgHeight {
  84. b.WriteString(bgLine)
  85. continue
  86. }
  87. pos := 0
  88. // Handle left side of the line up to the overlay
  89. if x > 0 {
  90. left := truncate.String(bgLine, uint(x))
  91. pos = ansi.PrintableRuneWidth(left)
  92. b.WriteString(left)
  93. if pos < x {
  94. b.WriteString(options.whitespace.render(x - pos))
  95. pos = x
  96. }
  97. }
  98. // Render the overlay content with optional borders
  99. if options.border {
  100. // Get the foreground line
  101. fgLine := fgLines[i-y]
  102. fgLineWidth := ansi.PrintableRuneWidth(fgLine)
  103. // Extract the styles at the border positions
  104. // We need to get the style just before the border position to preserve background
  105. leftStyle := ansiStyle{}
  106. if pos > 0 {
  107. leftStyle = getStyleAtPosition(bgLine, pos-1)
  108. } else {
  109. leftStyle = getStyleAtPosition(bgLine, pos)
  110. }
  111. rightStyle := getStyleAtPosition(bgLine, pos+fgLineWidth)
  112. // Left border - combine background from original with border foreground
  113. leftSeq := combineStyles(leftStyle, options.borderColor)
  114. if leftSeq != "" {
  115. b.WriteString(leftSeq)
  116. }
  117. b.WriteString("┃")
  118. if leftSeq != "" {
  119. b.WriteString("\x1b[0m") // Reset all styles only if we applied any
  120. }
  121. pos++
  122. // Content
  123. b.WriteString(fgLine)
  124. pos += fgLineWidth
  125. // Right border - combine background from original with border foreground
  126. rightSeq := combineStyles(rightStyle, options.borderColor)
  127. if rightSeq != "" {
  128. b.WriteString(rightSeq)
  129. }
  130. b.WriteString("┃")
  131. if rightSeq != "" {
  132. b.WriteString("\x1b[0m") // Reset all styles only if we applied any
  133. }
  134. pos++
  135. } else {
  136. // No border, just render the content
  137. fgLine := fgLines[i-y]
  138. b.WriteString(fgLine)
  139. pos += ansi.PrintableRuneWidth(fgLine)
  140. }
  141. // Handle right side of the line after the overlay
  142. right := cutLeft(bgLine, pos)
  143. bgWidth := ansi.PrintableRuneWidth(bgLine)
  144. rightWidth := ansi.PrintableRuneWidth(right)
  145. if rightWidth <= bgWidth-pos {
  146. b.WriteString(options.whitespace.render(bgWidth - rightWidth - pos))
  147. }
  148. b.WriteString(right)
  149. }
  150. return b.String()
  151. }
  152. // cutLeft cuts printable characters from the left.
  153. // This function is heavily based on muesli's ansi and truncate packages.
  154. func cutLeft(s string, cutWidth int) string {
  155. return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
  156. }
  157. // ansiStyle represents parsed ANSI style attributes
  158. type ansiStyle struct {
  159. fgColor string
  160. bgColor string
  161. attrs []string
  162. }
  163. // parseANSISequence parses an ANSI escape sequence into its components
  164. func parseANSISequence(seq string) ansiStyle {
  165. style := ansiStyle{}
  166. // Extract the parameters from the sequence (e.g., \x1b[38;5;123;48;5;456m -> "38;5;123;48;5;456")
  167. if !strings.HasPrefix(seq, "\x1b[") || !strings.HasSuffix(seq, "m") {
  168. return style
  169. }
  170. params := seq[2 : len(seq)-1]
  171. if params == "" {
  172. return style
  173. }
  174. parts := strings.Split(params, ";")
  175. i := 0
  176. for i < len(parts) {
  177. switch parts[i] {
  178. case "0": // Reset
  179. // Mark this as a reset by adding it to attrs
  180. style.attrs = append(style.attrs, "0")
  181. // Don't clear the style here, let the caller handle it
  182. case "1", "2", "3", "4", "5", "6", "7", "8", "9": // Various attributes
  183. style.attrs = append(style.attrs, parts[i])
  184. case "38": // Foreground color
  185. if i+1 < len(parts) && parts[i+1] == "5" && i+2 < len(parts) {
  186. // 256 color mode
  187. style.fgColor = strings.Join(parts[i:i+3], ";")
  188. i += 2
  189. } else if i+1 < len(parts) && parts[i+1] == "2" && i+4 < len(parts) {
  190. // RGB color mode
  191. style.fgColor = strings.Join(parts[i:i+5], ";")
  192. i += 4
  193. }
  194. case "48": // Background color
  195. if i+1 < len(parts) && parts[i+1] == "5" && i+2 < len(parts) {
  196. // 256 color mode
  197. style.bgColor = strings.Join(parts[i:i+3], ";")
  198. i += 2
  199. } else if i+1 < len(parts) && parts[i+1] == "2" && i+4 < len(parts) {
  200. // RGB color mode
  201. style.bgColor = strings.Join(parts[i:i+5], ";")
  202. i += 4
  203. }
  204. case "30", "31", "32", "33", "34", "35", "36", "37": // Standard foreground colors
  205. style.fgColor = parts[i]
  206. case "40", "41", "42", "43", "44", "45", "46", "47": // Standard background colors
  207. style.bgColor = parts[i]
  208. case "90", "91", "92", "93", "94", "95", "96", "97": // Bright foreground colors
  209. style.fgColor = parts[i]
  210. case "100", "101", "102", "103", "104", "105", "106", "107": // Bright background colors
  211. style.bgColor = parts[i]
  212. }
  213. i++
  214. }
  215. return style
  216. }
  217. // combineStyles creates an ANSI sequence that combines background from one style with foreground from another
  218. func combineStyles(bgStyle ansiStyle, fgColor *compat.AdaptiveColor) string {
  219. if fgColor == nil && bgStyle.bgColor == "" && len(bgStyle.attrs) == 0 {
  220. return ""
  221. }
  222. var parts []string
  223. // Add attributes
  224. parts = append(parts, bgStyle.attrs...)
  225. // Add background color from the original style
  226. if bgStyle.bgColor != "" {
  227. parts = append(parts, bgStyle.bgColor)
  228. }
  229. // Add foreground color if specified
  230. if fgColor != nil {
  231. // Use the adaptive color which automatically selects based on terminal background
  232. // The RGBA method already handles light/dark selection
  233. r, g, b, _ := fgColor.RGBA()
  234. // RGBA returns 16-bit values, we need 8-bit
  235. parts = append(parts, fmt.Sprintf("38;2;%d;%d;%d", r>>8, g>>8, b>>8))
  236. }
  237. if len(parts) == 0 {
  238. return ""
  239. }
  240. return fmt.Sprintf("\x1b[%sm", strings.Join(parts, ";"))
  241. }
  242. // getStyleAtPosition extracts the active ANSI style at a given visual position
  243. func getStyleAtPosition(s string, targetPos int) ansiStyle {
  244. visualPos := 0
  245. currentStyle := ansiStyle{}
  246. i := 0
  247. for i < len(s) && visualPos <= targetPos {
  248. // Check if we're at an ANSI escape sequence
  249. if match := ansiRegex.FindStringIndex(s[i:]); match != nil && match[0] == 0 {
  250. // Found an ANSI sequence at current position
  251. seq := s[i : i+match[1]]
  252. parsedStyle := parseANSISequence(seq)
  253. // Check if this is a reset sequence
  254. if len(parsedStyle.attrs) > 0 && parsedStyle.attrs[0] == "0" {
  255. // Reset all styles
  256. currentStyle = ansiStyle{}
  257. } else {
  258. // Update current style (merge with existing)
  259. if parsedStyle.fgColor != "" {
  260. currentStyle.fgColor = parsedStyle.fgColor
  261. }
  262. if parsedStyle.bgColor != "" {
  263. currentStyle.bgColor = parsedStyle.bgColor
  264. }
  265. if len(parsedStyle.attrs) > 0 {
  266. currentStyle.attrs = parsedStyle.attrs
  267. }
  268. }
  269. i += match[1]
  270. } else if i < len(s) {
  271. // Regular character
  272. if visualPos == targetPos {
  273. return currentStyle
  274. }
  275. _, size := utf8.DecodeRuneInString(s[i:])
  276. i += size
  277. visualPos++
  278. }
  279. }
  280. return currentStyle
  281. }
  282. type whitespace struct {
  283. style termenv.Style
  284. chars string
  285. }
  286. // Render whitespaces.
  287. func (w whitespace) render(width int) string {
  288. if w.chars == "" {
  289. w.chars = " "
  290. }
  291. r := []rune(w.chars)
  292. j := 0
  293. b := strings.Builder{}
  294. // Cycle through runes and print them into the whitespace.
  295. for i := 0; i < width; {
  296. b.WriteRune(r[j])
  297. j++
  298. if j >= len(r) {
  299. j = 0
  300. }
  301. i += ansi.PrintableRuneWidth(string(r[j]))
  302. }
  303. // Fill any extra gaps white spaces. This might be necessary if any runes
  304. // are more than one cell wide, which could leave a one-rune gap.
  305. short := width - ansi.PrintableRuneWidth(b.String())
  306. if short > 0 {
  307. b.WriteString(strings.Repeat(" ", short))
  308. }
  309. return w.style.Styled(b.String())
  310. }
  311. // WhitespaceOption sets a styling rule for rendering whitespace.
  312. type WhitespaceOption func(*whitespace)
  313. // WithWhitespace sets whitespace options for the overlay
  314. func WithWhitespace(opts ...WhitespaceOption) OverlayOption {
  315. return func(o *overlayOptions) {
  316. for _, opt := range opts {
  317. opt(o.whitespace)
  318. }
  319. }
  320. }
  321. // WithOverlayBorder enables border rendering for the overlay
  322. func WithOverlayBorder() OverlayOption {
  323. return func(o *overlayOptions) {
  324. o.border = true
  325. }
  326. }
  327. // WithOverlayBorderColor sets the border color for the overlay
  328. func WithOverlayBorderColor(color compat.AdaptiveColor) OverlayOption {
  329. return func(o *overlayOptions) {
  330. o.borderColor = &color
  331. }
  332. }