1package layout
2
3import (
4 "strings"
5
6 "github.com/charmbracelet/lipgloss"
7 chAnsi "github.com/charmbracelet/x/ansi"
8 "github.com/muesli/ansi"
9 "github.com/muesli/reflow/truncate"
10 "github.com/muesli/termenv"
11 "github.com/opencode-ai/opencode/internal/tui/styles"
12 "github.com/opencode-ai/opencode/internal/tui/util"
13)
14
15// Most of this code is borrowed from
16// https://github.com/charmbracelet/lipgloss/pull/102
17// as well as the lipgloss library, with some modification for what I needed.
18
19// Split a string into lines, additionally returning the size of the widest
20// line.
21func getLines(s string) (lines []string, widest int) {
22 lines = strings.Split(s, "\n")
23
24 for _, l := range lines {
25 w := ansi.PrintableRuneWidth(l)
26 if widest < w {
27 widest = w
28 }
29 }
30
31 return lines, widest
32}
33
34// PlaceOverlay places fg on top of bg.
35func PlaceOverlay(
36 x, y int,
37 fg, bg string,
38 shadow bool, opts ...WhitespaceOption,
39) string {
40 fgLines, fgWidth := getLines(fg)
41 bgLines, bgWidth := getLines(bg)
42 bgHeight := len(bgLines)
43 fgHeight := len(fgLines)
44
45 if shadow {
46 var shadowbg string = ""
47 shadowchar := lipgloss.NewStyle().
48 Background(styles.BackgroundDarker).
49 Foreground(styles.Background).
50 Render("░")
51 bgchar := styles.BaseStyle.Render(" ")
52 for i := 0; i <= fgHeight; i++ {
53 if i == 0 {
54 shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
55 } else {
56 shadowbg += bgchar + strings.Repeat(shadowchar, fgWidth) + "\n"
57 }
58 }
59
60 fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...)
61 fgLines, fgWidth = getLines(fg)
62 fgHeight = len(fgLines)
63 }
64
65 if fgWidth >= bgWidth && fgHeight >= bgHeight {
66 // FIXME: return fg or bg?
67 return fg
68 }
69 // TODO: allow placement outside of the bg box?
70 x = util.Clamp(x, 0, bgWidth-fgWidth)
71 y = util.Clamp(y, 0, bgHeight-fgHeight)
72
73 ws := &whitespace{}
74 for _, opt := range opts {
75 opt(ws)
76 }
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
88 pos := 0
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(ws.render(x - pos))
95 pos = x
96 }
97 }
98
99 fgLine := fgLines[i-y]
100 b.WriteString(fgLine)
101 pos += ansi.PrintableRuneWidth(fgLine)
102
103 right := cutLeft(bgLine, pos)
104 bgWidth := ansi.PrintableRuneWidth(bgLine)
105 rightWidth := ansi.PrintableRuneWidth(right)
106 if rightWidth <= bgWidth-pos {
107 b.WriteString(ws.render(bgWidth - rightWidth - pos))
108 }
109
110 b.WriteString(right)
111 }
112
113 return b.String()
114}
115
116// cutLeft cuts printable characters from the left.
117// This function is heavily based on muesli's ansi and truncate packages.
118func cutLeft(s string, cutWidth int) string {
119 return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
120}
121
122func max(a, b int) int {
123 if a > b {
124 return a
125 }
126 return b
127}
128
129type whitespace struct {
130 style termenv.Style
131 chars string
132}
133
134// Render whitespaces.
135func (w whitespace) render(width int) string {
136 if w.chars == "" {
137 w.chars = " "
138 }
139
140 r := []rune(w.chars)
141 j := 0
142 b := strings.Builder{}
143
144 // Cycle through runes and print them into the whitespace.
145 for i := 0; i < width; {
146 b.WriteRune(r[j])
147 j++
148 if j >= len(r) {
149 j = 0
150 }
151 i += ansi.PrintableRuneWidth(string(r[j]))
152 }
153
154 // Fill any extra gaps white spaces. This might be necessary if any runes
155 // are more than one cell wide, which could leave a one-rune gap.
156 short := width - ansi.PrintableRuneWidth(b.String())
157 if short > 0 {
158 b.WriteString(strings.Repeat(" ", short))
159 }
160
161 return w.style.Styled(b.String())
162}
163
164// WhitespaceOption sets a styling rule for rendering whitespace.
165type WhitespaceOption func(*whitespace)