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