overlay.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. package layout
  2. import (
  3. "strings"
  4. "github.com/charmbracelet/lipgloss/v2"
  5. chAnsi "github.com/charmbracelet/x/ansi"
  6. "github.com/muesli/ansi"
  7. "github.com/muesli/reflow/truncate"
  8. "github.com/muesli/termenv"
  9. "github.com/sst/opencode/internal/util"
  10. )
  11. // Split a string into lines, additionally returning the size of the widest line.
  12. func getLines(s string) (lines []string, widest int) {
  13. lines = strings.Split(s, "\n")
  14. for _, l := range lines {
  15. w := ansi.PrintableRuneWidth(l)
  16. if widest < w {
  17. widest = w
  18. }
  19. }
  20. return lines, widest
  21. }
  22. // PlaceOverlay places fg on top of bg.
  23. func PlaceOverlay(
  24. x, y int,
  25. fg, bg string,
  26. opts ...WhitespaceOption,
  27. ) string {
  28. fgLines, fgWidth := getLines(fg)
  29. bgLines, bgWidth := getLines(bg)
  30. bgHeight := len(bgLines)
  31. fgHeight := len(fgLines)
  32. if fgWidth >= bgWidth && fgHeight >= bgHeight {
  33. // FIXME: return fg or bg?
  34. return fg
  35. }
  36. // TODO: allow placement outside of the bg box?
  37. x = util.Clamp(x, 0, bgWidth-fgWidth)
  38. y = util.Clamp(y, 0, bgHeight-fgHeight)
  39. ws := &whitespace{}
  40. for _, opt := range opts {
  41. opt(ws)
  42. }
  43. var b strings.Builder
  44. for i, bgLine := range bgLines {
  45. if i > 0 {
  46. b.WriteByte('\n')
  47. }
  48. if i < y || i >= y+fgHeight {
  49. b.WriteString(bgLine)
  50. continue
  51. }
  52. pos := 0
  53. if x > 0 {
  54. left := truncate.String(bgLine, uint(x))
  55. pos = ansi.PrintableRuneWidth(left)
  56. b.WriteString(left)
  57. if pos < x {
  58. b.WriteString(ws.render(x - pos))
  59. pos = x
  60. }
  61. }
  62. fgLine := fgLines[i-y]
  63. b.WriteString(fgLine)
  64. pos += ansi.PrintableRuneWidth(fgLine)
  65. right := cutLeft(bgLine, pos)
  66. bgWidth := ansi.PrintableRuneWidth(bgLine)
  67. rightWidth := ansi.PrintableRuneWidth(right)
  68. if rightWidth <= bgWidth-pos {
  69. b.WriteString(ws.render(bgWidth - rightWidth - pos))
  70. }
  71. b.WriteString(right)
  72. }
  73. return b.String()
  74. }
  75. // cutLeft cuts printable characters from the left.
  76. // This function is heavily based on muesli's ansi and truncate packages.
  77. func cutLeft(s string, cutWidth int) string {
  78. return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
  79. }
  80. type whitespace struct {
  81. style termenv.Style
  82. chars string
  83. }
  84. // Render whitespaces.
  85. func (w whitespace) render(width int) string {
  86. if w.chars == "" {
  87. w.chars = " "
  88. }
  89. r := []rune(w.chars)
  90. j := 0
  91. b := strings.Builder{}
  92. // Cycle through runes and print them into the whitespace.
  93. for i := 0; i < width; {
  94. b.WriteRune(r[j])
  95. j++
  96. if j >= len(r) {
  97. j = 0
  98. }
  99. i += ansi.PrintableRuneWidth(string(r[j]))
  100. }
  101. // Fill any extra gaps white spaces. This might be necessary if any runes
  102. // are more than one cell wide, which could leave a one-rune gap.
  103. short := width - ansi.PrintableRuneWidth(b.String())
  104. if short > 0 {
  105. b.WriteString(strings.Repeat(" ", short))
  106. }
  107. return w.style.Styled(b.String())
  108. }
  109. // WhitespaceOption sets a styling rule for rendering whitespace.
  110. type WhitespaceOption func(*whitespace)