1package lipgloss
2
3import (
4 "image/color"
5 "strings"
6
7 "github.com/charmbracelet/x/ansi"
8 "github.com/rivo/uniseg"
9)
10
11// Border contains a series of values which comprise the various parts of a
12// border.
13type Border struct {
14 Top string
15 Bottom string
16 Left string
17 Right string
18 TopLeft string
19 TopRight string
20 BottomLeft string
21 BottomRight string
22 MiddleLeft string
23 MiddleRight string
24 Middle string
25 MiddleTop string
26 MiddleBottom string
27}
28
29// GetTopSize returns the width of the top border. If borders contain runes of
30// varying widths, the widest rune is returned. If no border exists on the top
31// edge, 0 is returned.
32func (b Border) GetTopSize() int {
33 return getBorderEdgeWidth(b.TopLeft, b.Top, b.TopRight)
34}
35
36// GetRightSize returns the width of the right border. If borders contain
37// runes of varying widths, the widest rune is returned. If no border exists on
38// the right edge, 0 is returned.
39func (b Border) GetRightSize() int {
40 return getBorderEdgeWidth(b.TopRight, b.Right, b.BottomRight)
41}
42
43// GetBottomSize returns the width of the bottom border. If borders contain
44// runes of varying widths, the widest rune is returned. If no border exists on
45// the bottom edge, 0 is returned.
46func (b Border) GetBottomSize() int {
47 return getBorderEdgeWidth(b.BottomLeft, b.Bottom, b.BottomRight)
48}
49
50// GetLeftSize returns the width of the left border. If borders contain runes
51// of varying widths, the widest rune is returned. If no border exists on the
52// left edge, 0 is returned.
53func (b Border) GetLeftSize() int {
54 return getBorderEdgeWidth(b.TopLeft, b.Left, b.BottomLeft)
55}
56
57func getBorderEdgeWidth(borderParts ...string) (maxWidth int) {
58 for _, piece := range borderParts {
59 w := maxRuneWidth(piece)
60 if w > maxWidth {
61 maxWidth = w
62 }
63 }
64 return maxWidth
65}
66
67var (
68 noBorder = Border{}
69
70 normalBorder = Border{
71 Top: "─",
72 Bottom: "─",
73 Left: "│",
74 Right: "│",
75 TopLeft: "┌",
76 TopRight: "┐",
77 BottomLeft: "└",
78 BottomRight: "┘",
79 MiddleLeft: "├",
80 MiddleRight: "┤",
81 Middle: "┼",
82 MiddleTop: "┬",
83 MiddleBottom: "┴",
84 }
85
86 roundedBorder = Border{
87 Top: "─",
88 Bottom: "─",
89 Left: "│",
90 Right: "│",
91 TopLeft: "╭",
92 TopRight: "╮",
93 BottomLeft: "╰",
94 BottomRight: "╯",
95 MiddleLeft: "├",
96 MiddleRight: "┤",
97 Middle: "┼",
98 MiddleTop: "┬",
99 MiddleBottom: "┴",
100 }
101
102 blockBorder = Border{
103 Top: "█",
104 Bottom: "█",
105 Left: "█",
106 Right: "█",
107 TopLeft: "█",
108 TopRight: "█",
109 BottomLeft: "█",
110 BottomRight: "█",
111 MiddleLeft: "█",
112 MiddleRight: "█",
113 Middle: "█",
114 MiddleTop: "█",
115 MiddleBottom: "█",
116 }
117
118 outerHalfBlockBorder = Border{
119 Top: "▀",
120 Bottom: "▄",
121 Left: "▌",
122 Right: "▐",
123 TopLeft: "▛",
124 TopRight: "▜",
125 BottomLeft: "▙",
126 BottomRight: "▟",
127 }
128
129 innerHalfBlockBorder = Border{
130 Top: "▄",
131 Bottom: "▀",
132 Left: "▐",
133 Right: "▌",
134 TopLeft: "▗",
135 TopRight: "▖",
136 BottomLeft: "▝",
137 BottomRight: "▘",
138 }
139
140 thickBorder = Border{
141 Top: "━",
142 Bottom: "━",
143 Left: "┃",
144 Right: "┃",
145 TopLeft: "┏",
146 TopRight: "┓",
147 BottomLeft: "┗",
148 BottomRight: "┛",
149 MiddleLeft: "┣",
150 MiddleRight: "┫",
151 Middle: "╋",
152 MiddleTop: "┳",
153 MiddleBottom: "┻",
154 }
155
156 doubleBorder = Border{
157 Top: "═",
158 Bottom: "═",
159 Left: "║",
160 Right: "║",
161 TopLeft: "╔",
162 TopRight: "╗",
163 BottomLeft: "╚",
164 BottomRight: "╝",
165 MiddleLeft: "╠",
166 MiddleRight: "╣",
167 Middle: "╬",
168 MiddleTop: "╦",
169 MiddleBottom: "╩",
170 }
171
172 hiddenBorder = Border{
173 Top: " ",
174 Bottom: " ",
175 Left: " ",
176 Right: " ",
177 TopLeft: " ",
178 TopRight: " ",
179 BottomLeft: " ",
180 BottomRight: " ",
181 MiddleLeft: " ",
182 MiddleRight: " ",
183 Middle: " ",
184 MiddleTop: " ",
185 MiddleBottom: " ",
186 }
187
188 markdownBorder = Border{
189 Top: "-",
190 Bottom: "-",
191 Left: "|",
192 Right: "|",
193 TopLeft: "|",
194 TopRight: "|",
195 BottomLeft: "|",
196 BottomRight: "|",
197 MiddleLeft: "|",
198 MiddleRight: "|",
199 Middle: "|",
200 MiddleTop: "|",
201 MiddleBottom: "|",
202 }
203
204 asciiBorder = Border{
205 Top: "-",
206 Bottom: "-",
207 Left: "|",
208 Right: "|",
209 TopLeft: "+",
210 TopRight: "+",
211 BottomLeft: "+",
212 BottomRight: "+",
213 MiddleLeft: "+",
214 MiddleRight: "+",
215 Middle: "+",
216 MiddleTop: "+",
217 MiddleBottom: "+",
218 }
219)
220
221// NormalBorder returns a standard-type border with a normal weight and 90
222// degree corners.
223func NormalBorder() Border {
224 return normalBorder
225}
226
227// RoundedBorder returns a border with rounded corners.
228func RoundedBorder() Border {
229 return roundedBorder
230}
231
232// BlockBorder returns a border that takes the whole block.
233func BlockBorder() Border {
234 return blockBorder
235}
236
237// OuterHalfBlockBorder returns a half-block border that sits outside the frame.
238func OuterHalfBlockBorder() Border {
239 return outerHalfBlockBorder
240}
241
242// InnerHalfBlockBorder returns a half-block border that sits inside the frame.
243func InnerHalfBlockBorder() Border {
244 return innerHalfBlockBorder
245}
246
247// ThickBorder returns a border that's thicker than the one returned by
248// NormalBorder.
249func ThickBorder() Border {
250 return thickBorder
251}
252
253// DoubleBorder returns a border comprised of two thin strokes.
254func DoubleBorder() Border {
255 return doubleBorder
256}
257
258// HiddenBorder returns a border that renders as a series of single-cell
259// spaces. It's useful for cases when you want to remove a standard border but
260// maintain layout positioning. This said, you can still apply a background
261// color to a hidden border.
262func HiddenBorder() Border {
263 return hiddenBorder
264}
265
266// MarkdownBorder return a table border in markdown style.
267//
268// Make sure to disable top and bottom border for the best result. This will
269// ensure that the output is valid markdown.
270//
271// table.New().Border(lipgloss.MarkdownBorder()).BorderTop(false).BorderBottom(false)
272func MarkdownBorder() Border {
273 return markdownBorder
274}
275
276// ASCIIBorder returns a table border with ASCII characters.
277func ASCIIBorder() Border {
278 return asciiBorder
279}
280
281func (s Style) applyBorder(str string) string {
282 var (
283 border = s.getBorderStyle()
284 hasTop = s.getAsBool(borderTopKey, false)
285 hasRight = s.getAsBool(borderRightKey, false)
286 hasBottom = s.getAsBool(borderBottomKey, false)
287 hasLeft = s.getAsBool(borderLeftKey, false)
288
289 topFG = s.getAsColor(borderTopForegroundKey)
290 rightFG = s.getAsColor(borderRightForegroundKey)
291 bottomFG = s.getAsColor(borderBottomForegroundKey)
292 leftFG = s.getAsColor(borderLeftForegroundKey)
293
294 topBG = s.getAsColor(borderTopBackgroundKey)
295 rightBG = s.getAsColor(borderRightBackgroundKey)
296 bottomBG = s.getAsColor(borderBottomBackgroundKey)
297 leftBG = s.getAsColor(borderLeftBackgroundKey)
298 )
299
300 // If a border is set and no sides have been specifically turned on or off
301 // render borders on all sides.
302 if s.isBorderStyleSetWithoutSides() {
303 hasTop = true
304 hasRight = true
305 hasBottom = true
306 hasLeft = true
307 }
308
309 // If no border is set or all borders are been disabled, abort.
310 if border == noBorder || (!hasTop && !hasRight && !hasBottom && !hasLeft) {
311 return str
312 }
313
314 lines, width := getLines(str)
315
316 if hasLeft {
317 if border.Left == "" {
318 border.Left = " "
319 }
320 width += maxRuneWidth(border.Left)
321 }
322
323 if hasRight && border.Right == "" {
324 border.Right = " "
325 }
326
327 // If corners should be rendered but are set with the empty string, fill them
328 // with a single space.
329 if hasTop && hasLeft && border.TopLeft == "" {
330 border.TopLeft = " "
331 }
332 if hasTop && hasRight && border.TopRight == "" {
333 border.TopRight = " "
334 }
335 if hasBottom && hasLeft && border.BottomLeft == "" {
336 border.BottomLeft = " "
337 }
338 if hasBottom && hasRight && border.BottomRight == "" {
339 border.BottomRight = " "
340 }
341
342 // Figure out which corners we should actually be using based on which
343 // sides are set to show.
344 if hasTop {
345 switch {
346 case !hasLeft && !hasRight:
347 border.TopLeft = ""
348 border.TopRight = ""
349 case !hasLeft:
350 border.TopLeft = ""
351 case !hasRight:
352 border.TopRight = ""
353 }
354 }
355 if hasBottom {
356 switch {
357 case !hasLeft && !hasRight:
358 border.BottomLeft = ""
359 border.BottomRight = ""
360 case !hasLeft:
361 border.BottomLeft = ""
362 case !hasRight:
363 border.BottomRight = ""
364 }
365 }
366
367 // For now, limit corners to one rune.
368 border.TopLeft = getFirstRuneAsString(border.TopLeft)
369 border.TopRight = getFirstRuneAsString(border.TopRight)
370 border.BottomRight = getFirstRuneAsString(border.BottomRight)
371 border.BottomLeft = getFirstRuneAsString(border.BottomLeft)
372
373 var out strings.Builder
374
375 // Render top
376 if hasTop {
377 top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
378 top = s.styleBorder(top, topFG, topBG)
379 out.WriteString(top)
380 out.WriteRune('\n')
381 }
382
383 leftRunes := []rune(border.Left)
384 leftIndex := 0
385
386 rightRunes := []rune(border.Right)
387 rightIndex := 0
388
389 // Render sides
390 for i, l := range lines {
391 if hasLeft {
392 r := string(leftRunes[leftIndex])
393 leftIndex++
394 if leftIndex >= len(leftRunes) {
395 leftIndex = 0
396 }
397 out.WriteString(s.styleBorder(r, leftFG, leftBG))
398 }
399 out.WriteString(l)
400 if hasRight {
401 r := string(rightRunes[rightIndex])
402 rightIndex++
403 if rightIndex >= len(rightRunes) {
404 rightIndex = 0
405 }
406 out.WriteString(s.styleBorder(r, rightFG, rightBG))
407 }
408 if i < len(lines)-1 {
409 out.WriteRune('\n')
410 }
411 }
412
413 // Render bottom
414 if hasBottom {
415 bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
416 bottom = s.styleBorder(bottom, bottomFG, bottomBG)
417 out.WriteRune('\n')
418 out.WriteString(bottom)
419 }
420
421 return out.String()
422}
423
424// Render the horizontal (top or bottom) portion of a border.
425func renderHorizontalEdge(left, middle, right string, width int) string {
426 if middle == "" {
427 middle = " "
428 }
429
430 leftWidth := ansi.StringWidth(left)
431 rightWidth := ansi.StringWidth(right)
432
433 runes := []rune(middle)
434 j := 0
435
436 out := strings.Builder{}
437 out.WriteString(left)
438 for i := leftWidth + rightWidth; i < width+rightWidth; {
439 out.WriteRune(runes[j])
440 j++
441 if j >= len(runes) {
442 j = 0
443 }
444 i += ansi.StringWidth(string(runes[j]))
445 }
446 out.WriteString(right)
447
448 return out.String()
449}
450
451// Apply foreground and background styling to a border.
452func (s Style) styleBorder(border string, fg, bg color.Color) string {
453 if fg == noColor && bg == noColor {
454 return border
455 }
456
457 var style ansi.Style
458 if fg != noColor {
459 style = style.ForegroundColor(fg)
460 }
461 if bg != noColor {
462 style = style.BackgroundColor(bg)
463 }
464
465 return style.Styled(border)
466}
467
468func maxRuneWidth(str string) int {
469 var width int
470
471 state := -1
472 for len(str) > 0 {
473 var w int
474 _, str, w, state = uniseg.FirstGraphemeClusterInString(str, state)
475 if w > width {
476 width = w
477 }
478 }
479
480 return width
481}
482
483func getFirstRuneAsString(str string) string {
484 if str == "" {
485 return str
486 }
487 r := []rune(str)
488 return string(r[0])
489}