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