2021-03-25 20:43:40 -04:00
|
|
|
package lipgloss
|
|
|
|
|
|
|
|
|
|
import (
|
2024-08-22 14:25:17 -04:00
|
|
|
"image/color"
|
2025-09-04 11:56:32 -04:00
|
|
|
"slices"
|
2021-03-25 20:43:40 -04:00
|
|
|
"strings"
|
2025-10-30 16:55:12 -04:00
|
|
|
"unicode/utf8"
|
2021-03-25 20:43:40 -04:00
|
|
|
|
2024-05-15 14:58:58 -04:00
|
|
|
"github.com/charmbracelet/x/ansi"
|
2026-01-05 09:51:53 -05:00
|
|
|
"github.com/clipperhouse/displaywidth"
|
2024-02-14 02:03:24 +11:00
|
|
|
"github.com/rivo/uniseg"
|
2021-03-25 20:43:40 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Border contains a series of values which comprise the various parts of a
|
|
|
|
|
// border.
|
|
|
|
|
type Border struct {
|
2023-10-05 12:26:16 -04:00
|
|
|
Top string
|
|
|
|
|
Bottom string
|
|
|
|
|
Left string
|
|
|
|
|
Right string
|
|
|
|
|
TopLeft string
|
|
|
|
|
TopRight string
|
|
|
|
|
BottomLeft string
|
|
|
|
|
BottomRight string
|
|
|
|
|
MiddleLeft string
|
|
|
|
|
MiddleRight string
|
|
|
|
|
Middle string
|
|
|
|
|
MiddleTop string
|
|
|
|
|
MiddleBottom string
|
2021-03-25 20:43:40 -04:00
|
|
|
}
|
|
|
|
|
|
2021-08-19 12:59:21 -04:00
|
|
|
// GetTopSize returns the width of the top border. If borders contain runes of
|
2021-08-18 21:54:20 -04:00
|
|
|
// varying widths, the widest rune is returned. If no border exists on the top
|
|
|
|
|
// edge, 0 is returned.
|
2021-08-19 12:59:21 -04:00
|
|
|
func (b Border) GetTopSize() int {
|
2021-08-18 21:54:20 -04:00
|
|
|
return getBorderEdgeWidth(b.TopLeft, b.Top, b.TopRight)
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-19 12:59:21 -04:00
|
|
|
// GetRightSize returns the width of the right border. If borders contain
|
2021-08-18 21:54:20 -04:00
|
|
|
// runes of varying widths, the widest rune is returned. If no border exists on
|
|
|
|
|
// the right edge, 0 is returned.
|
2021-08-19 12:59:21 -04:00
|
|
|
func (b Border) GetRightSize() int {
|
2023-04-29 10:51:31 -03:00
|
|
|
return getBorderEdgeWidth(b.TopRight, b.Right, b.BottomRight)
|
2021-08-18 21:54:20 -04:00
|
|
|
}
|
|
|
|
|
|
2021-08-19 12:59:21 -04:00
|
|
|
// GetBottomSize returns the width of the bottom border. If borders contain
|
2021-08-18 21:54:20 -04:00
|
|
|
// runes of varying widths, the widest rune is returned. If no border exists on
|
|
|
|
|
// the bottom edge, 0 is returned.
|
2021-08-19 12:59:21 -04:00
|
|
|
func (b Border) GetBottomSize() int {
|
2021-08-18 21:54:20 -04:00
|
|
|
return getBorderEdgeWidth(b.BottomLeft, b.Bottom, b.BottomRight)
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-19 12:59:21 -04:00
|
|
|
// GetLeftSize returns the width of the left border. If borders contain runes
|
2021-08-18 21:54:20 -04:00
|
|
|
// of varying widths, the widest rune is returned. If no border exists on the
|
|
|
|
|
// left edge, 0 is returned.
|
2021-08-19 12:59:21 -04:00
|
|
|
func (b Border) GetLeftSize() int {
|
2023-04-29 10:51:31 -03:00
|
|
|
return getBorderEdgeWidth(b.TopLeft, b.Left, b.BottomLeft)
|
2021-08-18 21:54:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getBorderEdgeWidth(borderParts ...string) (maxWidth int) {
|
|
|
|
|
for _, piece := range borderParts {
|
2026-01-05 09:51:53 -05:00
|
|
|
maxWidth = max(maxWidth, maxRuneWidth(piece))
|
2021-08-18 21:54:20 -04:00
|
|
|
}
|
|
|
|
|
return maxWidth
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 20:43:40 -04:00
|
|
|
var (
|
|
|
|
|
noBorder = Border{}
|
|
|
|
|
|
|
|
|
|
normalBorder = Border{
|
2023-10-05 12:26:16 -04:00
|
|
|
Top: "─",
|
|
|
|
|
Bottom: "─",
|
|
|
|
|
Left: "│",
|
|
|
|
|
Right: "│",
|
|
|
|
|
TopLeft: "┌",
|
|
|
|
|
TopRight: "┐",
|
|
|
|
|
BottomLeft: "└",
|
|
|
|
|
BottomRight: "┘",
|
|
|
|
|
MiddleLeft: "├",
|
|
|
|
|
MiddleRight: "┤",
|
|
|
|
|
Middle: "┼",
|
|
|
|
|
MiddleTop: "┬",
|
|
|
|
|
MiddleBottom: "┴",
|
2021-03-25 20:43:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
roundedBorder = Border{
|
2023-10-05 12:26:16 -04:00
|
|
|
Top: "─",
|
|
|
|
|
Bottom: "─",
|
|
|
|
|
Left: "│",
|
|
|
|
|
Right: "│",
|
|
|
|
|
TopLeft: "╭",
|
|
|
|
|
TopRight: "╮",
|
|
|
|
|
BottomLeft: "╰",
|
|
|
|
|
BottomRight: "╯",
|
|
|
|
|
MiddleLeft: "├",
|
|
|
|
|
MiddleRight: "┤",
|
|
|
|
|
Middle: "┼",
|
|
|
|
|
MiddleTop: "┬",
|
|
|
|
|
MiddleBottom: "┴",
|
2021-03-25 20:43:40 -04:00
|
|
|
}
|
|
|
|
|
|
2022-10-27 21:02:57 +02:00
|
|
|
blockBorder = Border{
|
2025-03-12 11:46:06 -07:00
|
|
|
Top: "█",
|
|
|
|
|
Bottom: "█",
|
|
|
|
|
Left: "█",
|
|
|
|
|
Right: "█",
|
|
|
|
|
TopLeft: "█",
|
|
|
|
|
TopRight: "█",
|
|
|
|
|
BottomLeft: "█",
|
|
|
|
|
BottomRight: "█",
|
|
|
|
|
MiddleLeft: "█",
|
|
|
|
|
MiddleRight: "█",
|
|
|
|
|
Middle: "█",
|
|
|
|
|
MiddleTop: "█",
|
|
|
|
|
MiddleBottom: "█",
|
2022-10-27 21:02:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
outerHalfBlockBorder = Border{
|
|
|
|
|
Top: "▀",
|
|
|
|
|
Bottom: "▄",
|
|
|
|
|
Left: "▌",
|
|
|
|
|
Right: "▐",
|
|
|
|
|
TopLeft: "▛",
|
|
|
|
|
TopRight: "▜",
|
|
|
|
|
BottomLeft: "▙",
|
|
|
|
|
BottomRight: "▟",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
innerHalfBlockBorder = Border{
|
|
|
|
|
Top: "▄",
|
|
|
|
|
Bottom: "▀",
|
|
|
|
|
Left: "▐",
|
|
|
|
|
Right: "▌",
|
|
|
|
|
TopLeft: "▗",
|
|
|
|
|
TopRight: "▖",
|
|
|
|
|
BottomLeft: "▝",
|
|
|
|
|
BottomRight: "▘",
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 20:43:40 -04:00
|
|
|
thickBorder = Border{
|
2023-10-05 12:26:16 -04:00
|
|
|
Top: "━",
|
|
|
|
|
Bottom: "━",
|
|
|
|
|
Left: "┃",
|
|
|
|
|
Right: "┃",
|
|
|
|
|
TopLeft: "┏",
|
|
|
|
|
TopRight: "┓",
|
|
|
|
|
BottomLeft: "┗",
|
|
|
|
|
BottomRight: "┛",
|
|
|
|
|
MiddleLeft: "┣",
|
|
|
|
|
MiddleRight: "┫",
|
|
|
|
|
Middle: "╋",
|
|
|
|
|
MiddleTop: "┳",
|
|
|
|
|
MiddleBottom: "┻",
|
2021-03-25 20:43:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
doubleBorder = Border{
|
2023-10-05 12:26:16 -04:00
|
|
|
Top: "═",
|
|
|
|
|
Bottom: "═",
|
|
|
|
|
Left: "║",
|
|
|
|
|
Right: "║",
|
|
|
|
|
TopLeft: "╔",
|
|
|
|
|
TopRight: "╗",
|
|
|
|
|
BottomLeft: "╚",
|
|
|
|
|
BottomRight: "╝",
|
|
|
|
|
MiddleLeft: "╠",
|
|
|
|
|
MiddleRight: "╣",
|
|
|
|
|
Middle: "╬",
|
|
|
|
|
MiddleTop: "╦",
|
|
|
|
|
MiddleBottom: "╩",
|
2021-03-25 20:43:40 -04:00
|
|
|
}
|
2021-08-18 20:57:35 -04:00
|
|
|
|
|
|
|
|
hiddenBorder = Border{
|
2023-10-05 12:26:16 -04:00
|
|
|
Top: " ",
|
|
|
|
|
Bottom: " ",
|
|
|
|
|
Left: " ",
|
|
|
|
|
Right: " ",
|
|
|
|
|
TopLeft: " ",
|
|
|
|
|
TopRight: " ",
|
|
|
|
|
BottomLeft: " ",
|
|
|
|
|
BottomRight: " ",
|
|
|
|
|
MiddleLeft: " ",
|
|
|
|
|
MiddleRight: " ",
|
|
|
|
|
Middle: " ",
|
|
|
|
|
MiddleTop: " ",
|
|
|
|
|
MiddleBottom: " ",
|
2021-08-18 20:57:35 -04:00
|
|
|
}
|
2025-03-12 11:46:06 -07:00
|
|
|
|
|
|
|
|
markdownBorder = Border{
|
|
|
|
|
Top: "-",
|
|
|
|
|
Bottom: "-",
|
|
|
|
|
Left: "|",
|
|
|
|
|
Right: "|",
|
|
|
|
|
TopLeft: "|",
|
|
|
|
|
TopRight: "|",
|
|
|
|
|
BottomLeft: "|",
|
|
|
|
|
BottomRight: "|",
|
|
|
|
|
MiddleLeft: "|",
|
|
|
|
|
MiddleRight: "|",
|
|
|
|
|
Middle: "|",
|
|
|
|
|
MiddleTop: "|",
|
|
|
|
|
MiddleBottom: "|",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
asciiBorder = Border{
|
|
|
|
|
Top: "-",
|
|
|
|
|
Bottom: "-",
|
|
|
|
|
Left: "|",
|
|
|
|
|
Right: "|",
|
|
|
|
|
TopLeft: "+",
|
|
|
|
|
TopRight: "+",
|
|
|
|
|
BottomLeft: "+",
|
|
|
|
|
BottomRight: "+",
|
|
|
|
|
MiddleLeft: "+",
|
|
|
|
|
MiddleRight: "+",
|
|
|
|
|
Middle: "+",
|
|
|
|
|
MiddleTop: "+",
|
|
|
|
|
MiddleBottom: "+",
|
|
|
|
|
}
|
2021-03-25 20:43:40 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// NormalBorder returns a standard-type border with a normal weight and 90
|
|
|
|
|
// degree corners.
|
|
|
|
|
func NormalBorder() Border {
|
|
|
|
|
return normalBorder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RoundedBorder returns a border with rounded corners.
|
|
|
|
|
func RoundedBorder() Border {
|
|
|
|
|
return roundedBorder
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-27 21:02:57 +02:00
|
|
|
// BlockBorder returns a border that takes the whole block.
|
|
|
|
|
func BlockBorder() Border {
|
|
|
|
|
return blockBorder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OuterHalfBlockBorder returns a half-block border that sits outside the frame.
|
|
|
|
|
func OuterHalfBlockBorder() Border {
|
|
|
|
|
return outerHalfBlockBorder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// InnerHalfBlockBorder returns a half-block border that sits inside the frame.
|
|
|
|
|
func InnerHalfBlockBorder() Border {
|
|
|
|
|
return innerHalfBlockBorder
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-14 11:39:57 -05:00
|
|
|
// ThickBorder returns a border that's thicker than the one returned by
|
2021-03-25 20:43:40 -04:00
|
|
|
// NormalBorder.
|
|
|
|
|
func ThickBorder() Border {
|
|
|
|
|
return thickBorder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DoubleBorder returns a border comprised of two thin strokes.
|
|
|
|
|
func DoubleBorder() Border {
|
|
|
|
|
return doubleBorder
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-18 20:57:35 -04:00
|
|
|
// HiddenBorder returns a border that renders as a series of single-cell
|
|
|
|
|
// spaces. It's useful for cases when you want to remove a standard border but
|
|
|
|
|
// maintain layout positioning. This said, you can still apply a background
|
|
|
|
|
// color to a hidden border.
|
|
|
|
|
func HiddenBorder() Border {
|
|
|
|
|
return hiddenBorder
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-12 11:46:06 -07:00
|
|
|
// MarkdownBorder return a table border in markdown style.
|
|
|
|
|
//
|
|
|
|
|
// Make sure to disable top and bottom border for the best result. This will
|
|
|
|
|
// ensure that the output is valid markdown.
|
|
|
|
|
//
|
|
|
|
|
// table.New().Border(lipgloss.MarkdownBorder()).BorderTop(false).BorderBottom(false)
|
|
|
|
|
func MarkdownBorder() Border {
|
|
|
|
|
return markdownBorder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ASCIIBorder returns a table border with ASCII characters.
|
|
|
|
|
func ASCIIBorder() Border {
|
|
|
|
|
return asciiBorder
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 11:56:32 -04:00
|
|
|
type borderBlend struct {
|
|
|
|
|
topGradient []color.Color
|
|
|
|
|
rightGradient []color.Color
|
|
|
|
|
bottomGradient []color.Color
|
|
|
|
|
leftGradient []color.Color
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s Style) borderBlend(width, height int, colors ...color.Color) *borderBlend {
|
|
|
|
|
gradient := Blend1D(
|
|
|
|
|
(height+width+2)*2,
|
|
|
|
|
colors...,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Rotate array forward or reverse based on the offset if provided.
|
|
|
|
|
if r := -s.getAsInt(borderForegroundBlendOffsetKey); r != 0 {
|
|
|
|
|
n := len(gradient)
|
|
|
|
|
r %= n
|
|
|
|
|
if r < 0 {
|
|
|
|
|
r += n
|
|
|
|
|
}
|
|
|
|
|
slices.Reverse(gradient[:r])
|
|
|
|
|
slices.Reverse(gradient[r:])
|
|
|
|
|
slices.Reverse(gradient)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
offset := 0
|
|
|
|
|
getFromOffset := func(size int) (s []color.Color) {
|
|
|
|
|
s = gradient[offset : offset+size]
|
|
|
|
|
offset += size
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
blend := &borderBlend{
|
|
|
|
|
topGradient: getFromOffset(width + 2),
|
|
|
|
|
rightGradient: getFromOffset(height),
|
|
|
|
|
bottomGradient: getFromOffset(width + 2),
|
|
|
|
|
leftGradient: getFromOffset(height),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// bottom and left gradients are reversed because they are drawn in reverse order.
|
|
|
|
|
slices.Reverse(blend.bottomGradient)
|
|
|
|
|
slices.Reverse(blend.leftGradient)
|
|
|
|
|
|
|
|
|
|
return blend
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-03 13:11:44 -04:00
|
|
|
func (s Style) applyBorder(str string) string {
|
2021-03-25 20:43:40 -04:00
|
|
|
var (
|
2021-08-19 10:08:00 -04:00
|
|
|
border = s.getBorderStyle()
|
2021-03-25 20:43:40 -04:00
|
|
|
hasTop = s.getAsBool(borderTopKey, false)
|
|
|
|
|
hasRight = s.getAsBool(borderRightKey, false)
|
|
|
|
|
hasBottom = s.getAsBool(borderBottomKey, false)
|
|
|
|
|
hasLeft = s.getAsBool(borderLeftKey, false)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// If a border is set and no sides have been specifically turned on or off
|
|
|
|
|
// render borders on all sides.
|
2025-05-23 11:24:10 -03:00
|
|
|
if s.isBorderStyleSetWithoutSides() {
|
2021-03-25 20:43:40 -04:00
|
|
|
hasTop = true
|
|
|
|
|
hasRight = true
|
|
|
|
|
hasBottom = true
|
|
|
|
|
hasLeft = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If no border is set or all borders are been disabled, abort.
|
|
|
|
|
if border == noBorder || (!hasTop && !hasRight && !hasBottom && !hasLeft) {
|
|
|
|
|
return str
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lines, width := getLines(str)
|
|
|
|
|
|
|
|
|
|
if hasLeft {
|
2021-04-28 18:27:08 -04:00
|
|
|
if border.Left == "" {
|
|
|
|
|
border.Left = " "
|
|
|
|
|
}
|
|
|
|
|
width += maxRuneWidth(border.Left)
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 11:56:32 -04:00
|
|
|
if hasRight {
|
|
|
|
|
if border.Right == "" {
|
|
|
|
|
border.Right = " "
|
|
|
|
|
}
|
|
|
|
|
width += maxRuneWidth(border.Right)
|
2021-04-28 18:27:08 -04:00
|
|
|
}
|
|
|
|
|
|
2022-11-08 10:55:11 +01:00
|
|
|
// If corners should be rendered but are set with the empty string, fill them
|
2021-04-28 18:27:08 -04:00
|
|
|
// with a single space.
|
|
|
|
|
if hasTop && hasLeft && border.TopLeft == "" {
|
|
|
|
|
border.TopLeft = " "
|
|
|
|
|
}
|
|
|
|
|
if hasTop && hasRight && border.TopRight == "" {
|
|
|
|
|
border.TopRight = " "
|
|
|
|
|
}
|
|
|
|
|
if hasBottom && hasLeft && border.BottomLeft == "" {
|
|
|
|
|
border.BottomLeft = " "
|
|
|
|
|
}
|
|
|
|
|
if hasBottom && hasRight && border.BottomRight == "" {
|
|
|
|
|
border.BottomRight = " "
|
2021-03-25 20:43:40 -04:00
|
|
|
}
|
|
|
|
|
|
2021-03-29 20:49:50 -04:00
|
|
|
// Figure out which corners we should actually be using based on which
|
|
|
|
|
// sides are set to show.
|
|
|
|
|
if hasTop {
|
|
|
|
|
switch {
|
|
|
|
|
case !hasLeft && !hasRight:
|
|
|
|
|
border.TopLeft = ""
|
|
|
|
|
border.TopRight = ""
|
|
|
|
|
case !hasLeft:
|
|
|
|
|
border.TopLeft = ""
|
|
|
|
|
case !hasRight:
|
|
|
|
|
border.TopRight = ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if hasBottom {
|
|
|
|
|
switch {
|
|
|
|
|
case !hasLeft && !hasRight:
|
|
|
|
|
border.BottomLeft = ""
|
|
|
|
|
border.BottomRight = ""
|
|
|
|
|
case !hasLeft:
|
|
|
|
|
border.BottomLeft = ""
|
|
|
|
|
case !hasRight:
|
|
|
|
|
border.BottomRight = ""
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-25 20:43:40 -04:00
|
|
|
|
2021-04-28 20:45:40 -04:00
|
|
|
// For now, limit corners to one rune.
|
|
|
|
|
border.TopLeft = getFirstRuneAsString(border.TopLeft)
|
|
|
|
|
border.TopRight = getFirstRuneAsString(border.TopRight)
|
|
|
|
|
border.BottomRight = getFirstRuneAsString(border.BottomRight)
|
|
|
|
|
border.BottomLeft = getFirstRuneAsString(border.BottomLeft)
|
|
|
|
|
|
2025-09-04 11:56:32 -04:00
|
|
|
var topFG, rightFG, bottomFG, leftFG color.Color
|
|
|
|
|
var (
|
|
|
|
|
blendFG = s.getAsColors(borderForegroundBlendKey)
|
|
|
|
|
topBG = s.getAsColor(borderTopBackgroundKey)
|
|
|
|
|
rightBG = s.getAsColor(borderRightBackgroundKey)
|
|
|
|
|
bottomBG = s.getAsColor(borderBottomBackgroundKey)
|
|
|
|
|
leftBG = s.getAsColor(borderLeftBackgroundKey)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var blend *borderBlend
|
|
|
|
|
if len(blendFG) > 0 {
|
|
|
|
|
blend = s.borderBlend(width, len(lines), blendFG...)
|
|
|
|
|
} else {
|
|
|
|
|
topFG = s.getAsColor(borderTopForegroundKey)
|
|
|
|
|
rightFG = s.getAsColor(borderRightForegroundKey)
|
|
|
|
|
bottomFG = s.getAsColor(borderBottomForegroundKey)
|
|
|
|
|
leftFG = s.getAsColor(borderLeftForegroundKey)
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 20:43:40 -04:00
|
|
|
var out strings.Builder
|
|
|
|
|
|
|
|
|
|
// Render top
|
|
|
|
|
if hasTop {
|
|
|
|
|
top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
|
2025-09-04 11:56:32 -04:00
|
|
|
if blend != nil {
|
|
|
|
|
out.WriteString(s.styleBorderBlend(top, blend.topGradient, topBG))
|
|
|
|
|
} else {
|
|
|
|
|
out.WriteString(s.styleBorder(top, topFG, topBG))
|
|
|
|
|
}
|
2021-03-25 20:43:40 -04:00
|
|
|
out.WriteRune('\n')
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-28 18:27:08 -04:00
|
|
|
leftRunes := []rune(border.Left)
|
|
|
|
|
leftIndex := 0
|
|
|
|
|
|
|
|
|
|
rightRunes := []rune(border.Right)
|
|
|
|
|
rightIndex := 0
|
|
|
|
|
|
2021-03-25 20:43:40 -04:00
|
|
|
// Render sides
|
2025-09-04 11:56:32 -04:00
|
|
|
var r string
|
2021-03-25 20:43:40 -04:00
|
|
|
for i, l := range lines {
|
|
|
|
|
if hasLeft {
|
2025-09-04 11:56:32 -04:00
|
|
|
r = string(leftRunes[leftIndex])
|
2021-04-28 18:27:08 -04:00
|
|
|
leftIndex++
|
|
|
|
|
if leftIndex >= len(leftRunes) {
|
|
|
|
|
leftIndex = 0
|
|
|
|
|
}
|
2025-09-04 11:56:32 -04:00
|
|
|
if blend != nil {
|
|
|
|
|
out.WriteString(s.styleBorder(r, blend.leftGradient[i], leftBG))
|
|
|
|
|
} else {
|
|
|
|
|
out.WriteString(s.styleBorder(r, leftFG, leftBG))
|
|
|
|
|
}
|
2021-03-25 20:43:40 -04:00
|
|
|
}
|
|
|
|
|
out.WriteString(l)
|
|
|
|
|
if hasRight {
|
2025-09-04 11:56:32 -04:00
|
|
|
r = string(rightRunes[rightIndex])
|
2021-04-28 18:27:08 -04:00
|
|
|
rightIndex++
|
|
|
|
|
if rightIndex >= len(rightRunes) {
|
|
|
|
|
rightIndex = 0
|
|
|
|
|
}
|
2025-09-04 11:56:32 -04:00
|
|
|
if blend != nil {
|
|
|
|
|
out.WriteString(s.styleBorder(r, blend.rightGradient[i], rightBG))
|
|
|
|
|
} else {
|
|
|
|
|
out.WriteString(s.styleBorder(r, rightFG, rightBG))
|
|
|
|
|
}
|
2021-03-25 20:43:40 -04:00
|
|
|
}
|
|
|
|
|
if i < len(lines)-1 {
|
|
|
|
|
out.WriteRune('\n')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render bottom
|
|
|
|
|
if hasBottom {
|
2021-03-26 18:15:16 -04:00
|
|
|
bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
|
2021-03-25 20:43:40 -04:00
|
|
|
out.WriteRune('\n')
|
2025-09-04 11:56:32 -04:00
|
|
|
if blend != nil {
|
|
|
|
|
out.WriteString(s.styleBorderBlend(bottom, blend.bottomGradient, bottomBG))
|
|
|
|
|
} else {
|
|
|
|
|
out.WriteString(s.styleBorder(bottom, bottomFG, bottomBG))
|
|
|
|
|
}
|
2021-03-25 20:43:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return out.String()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render the horizontal (top or bottom) portion of a border.
|
|
|
|
|
func renderHorizontalEdge(left, middle, right string, width int) string {
|
|
|
|
|
if middle == "" {
|
|
|
|
|
middle = " "
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-29 15:05:18 -04:00
|
|
|
leftWidth := ansi.StringWidth(left)
|
|
|
|
|
rightWidth := ansi.StringWidth(right)
|
2021-03-25 20:43:40 -04:00
|
|
|
|
2021-04-28 18:27:08 -04:00
|
|
|
runes := []rune(middle)
|
|
|
|
|
j := 0
|
|
|
|
|
|
2021-03-25 20:43:40 -04:00
|
|
|
out := strings.Builder{}
|
|
|
|
|
out.WriteString(left)
|
2025-09-04 11:56:32 -04:00
|
|
|
|
|
|
|
|
for i := 0; i < width-leftWidth-rightWidth; {
|
|
|
|
|
r := runes[j]
|
|
|
|
|
out.WriteRune(r)
|
|
|
|
|
i += ansi.StringWidth(string(r))
|
2021-04-28 18:27:08 -04:00
|
|
|
j++
|
|
|
|
|
if j >= len(runes) {
|
|
|
|
|
j = 0
|
|
|
|
|
}
|
2021-03-25 20:43:40 -04:00
|
|
|
}
|
|
|
|
|
|
2025-09-04 11:56:32 -04:00
|
|
|
out.WriteString(right)
|
2021-03-25 20:43:40 -04:00
|
|
|
return out.String()
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 11:56:32 -04:00
|
|
|
// styleBorder applies foreground and background styling to a border.
|
2024-08-22 14:25:17 -04:00
|
|
|
func (s Style) styleBorder(border string, fg, bg color.Color) string {
|
2021-03-25 20:43:40 -04:00
|
|
|
if fg == noColor && bg == noColor {
|
|
|
|
|
return border
|
|
|
|
|
}
|
2024-07-22 12:24:23 -04:00
|
|
|
var style ansi.Style
|
2021-03-25 20:43:40 -04:00
|
|
|
if fg != noColor {
|
2024-07-22 12:24:23 -04:00
|
|
|
style = style.ForegroundColor(fg)
|
2021-03-25 20:43:40 -04:00
|
|
|
}
|
|
|
|
|
if bg != noColor {
|
2024-07-22 12:24:23 -04:00
|
|
|
style = style.BackgroundColor(bg)
|
2021-03-25 20:43:40 -04:00
|
|
|
}
|
|
|
|
|
return style.Styled(border)
|
|
|
|
|
}
|
2021-04-28 18:27:08 -04:00
|
|
|
|
2025-09-04 11:56:32 -04:00
|
|
|
// styleBorderBlend applies foreground and background styling to a border, using blending.
|
|
|
|
|
func (s Style) styleBorderBlend(border string, fg []color.Color, bg color.Color) string {
|
|
|
|
|
var out strings.Builder
|
|
|
|
|
var style ansi.Style
|
|
|
|
|
var i int
|
|
|
|
|
|
|
|
|
|
gr := uniseg.NewGraphemes(border)
|
|
|
|
|
for gr.Next() {
|
|
|
|
|
style = style[:0]
|
|
|
|
|
if fg[i] != noColor {
|
|
|
|
|
style = style.ForegroundColor(fg[i])
|
|
|
|
|
}
|
|
|
|
|
if bg != noColor {
|
|
|
|
|
style = style.BackgroundColor(bg)
|
|
|
|
|
}
|
|
|
|
|
_, _ = out.WriteString(style.String())
|
|
|
|
|
_, _ = out.Write(gr.Bytes())
|
|
|
|
|
i++
|
|
|
|
|
}
|
|
|
|
|
_, _ = out.WriteString(ansi.ResetStyle)
|
|
|
|
|
return out.String()
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-14 02:03:24 +11:00
|
|
|
func maxRuneWidth(str string) int {
|
2026-01-05 09:51:53 -05:00
|
|
|
switch len(str) {
|
|
|
|
|
case 0:
|
|
|
|
|
return 0
|
|
|
|
|
case 1:
|
|
|
|
|
return displaywidth.String(str)
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-14 02:03:24 +11:00
|
|
|
var width int
|
|
|
|
|
|
2026-01-05 09:51:53 -05:00
|
|
|
g := displaywidth.StringGraphemes(str)
|
|
|
|
|
for g.Next() {
|
|
|
|
|
width = max(width, g.Width())
|
2021-04-28 18:27:08 -04:00
|
|
|
}
|
|
|
|
|
return width
|
|
|
|
|
}
|
2021-04-28 20:45:40 -04:00
|
|
|
|
|
|
|
|
func getFirstRuneAsString(str string) string {
|
|
|
|
|
if str == "" {
|
|
|
|
|
return str
|
|
|
|
|
}
|
2025-10-30 16:55:12 -04:00
|
|
|
_, size := utf8.DecodeRuneInString(str)
|
|
|
|
|
return str[:size]
|
2021-04-28 20:45:40 -04:00
|
|
|
}
|