border.go

  1package layout
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/charmbracelet/lipgloss"
  8	"github.com/kujtimiihoxha/opencode/internal/tui/styles"
  9)
 10
 11type BorderPosition int
 12
 13const (
 14	TopLeftBorder BorderPosition = iota
 15	TopMiddleBorder
 16	TopRightBorder
 17	BottomLeftBorder
 18	BottomMiddleBorder
 19	BottomRightBorder
 20)
 21
 22var (
 23	ActiveBorder          = styles.Blue
 24	InactivePreviewBorder = styles.Grey
 25)
 26
 27type BorderOptions struct {
 28	Active         bool
 29	EmbeddedText   map[BorderPosition]string
 30	ActiveColor    lipgloss.TerminalColor
 31	InactiveColor  lipgloss.TerminalColor
 32	ActiveBorder   lipgloss.Border
 33	InactiveBorder lipgloss.Border
 34}
 35
 36func Borderize(content string, opts BorderOptions) string {
 37	if opts.EmbeddedText == nil {
 38		opts.EmbeddedText = make(map[BorderPosition]string)
 39	}
 40	if opts.ActiveColor == nil {
 41		opts.ActiveColor = ActiveBorder
 42	}
 43	if opts.InactiveColor == nil {
 44		opts.InactiveColor = InactivePreviewBorder
 45	}
 46	if opts.ActiveBorder == (lipgloss.Border{}) {
 47		opts.ActiveBorder = lipgloss.ThickBorder()
 48	}
 49	if opts.InactiveBorder == (lipgloss.Border{}) {
 50		opts.InactiveBorder = lipgloss.NormalBorder()
 51	}
 52
 53	var (
 54		thickness = map[bool]lipgloss.Border{
 55			true:  opts.ActiveBorder,
 56			false: opts.InactiveBorder,
 57		}
 58		color = map[bool]lipgloss.TerminalColor{
 59			true:  opts.ActiveColor,
 60			false: opts.InactiveColor,
 61		}
 62		border = thickness[opts.Active]
 63		style  = lipgloss.NewStyle().Foreground(color[opts.Active])
 64		width  = lipgloss.Width(content)
 65	)
 66
 67	encloseInSquareBrackets := func(text string) string {
 68		if text != "" {
 69			return fmt.Sprintf("%s%s%s",
 70				style.Render(border.TopRight),
 71				text,
 72				style.Render(border.TopLeft),
 73			)
 74		}
 75		return text
 76	}
 77	buildHorizontalBorder := func(leftText, middleText, rightText, leftCorner, inbetween, rightCorner string) string {
 78		leftText = encloseInSquareBrackets(leftText)
 79		middleText = encloseInSquareBrackets(middleText)
 80		rightText = encloseInSquareBrackets(rightText)
 81		// Calculate length of border between embedded texts
 82		remaining := max(0, width-lipgloss.Width(leftText)-lipgloss.Width(middleText)-lipgloss.Width(rightText))
 83		leftBorderLen := max(0, (width/2)-lipgloss.Width(leftText)-(lipgloss.Width(middleText)/2))
 84		rightBorderLen := max(0, remaining-leftBorderLen)
 85		// Then construct border string
 86		s := leftText +
 87			style.Render(strings.Repeat(inbetween, leftBorderLen)) +
 88			middleText +
 89			style.Render(strings.Repeat(inbetween, rightBorderLen)) +
 90			rightText
 91		// Make it fit in the space available between the two corners.
 92		s = lipgloss.NewStyle().
 93			Inline(true).
 94			MaxWidth(width).
 95			Render(s)
 96		// Add the corners
 97		return style.Render(leftCorner) + s + style.Render(rightCorner)
 98	}
 99	// Stack top border, content and horizontal borders, and bottom border.
100	return strings.Join([]string{
101		buildHorizontalBorder(
102			opts.EmbeddedText[TopLeftBorder],
103			opts.EmbeddedText[TopMiddleBorder],
104			opts.EmbeddedText[TopRightBorder],
105			border.TopLeft,
106			border.Top,
107			border.TopRight,
108		),
109		lipgloss.NewStyle().
110			BorderForeground(color[opts.Active]).
111			Border(border, false, true, false, true).Render(content),
112		buildHorizontalBorder(
113			opts.EmbeddedText[BottomLeftBorder],
114			opts.EmbeddedText[BottomMiddleBorder],
115			opts.EmbeddedText[BottomRightBorder],
116			border.BottomLeft,
117			border.Bottom,
118			border.BottomRight,
119		),
120	}, "\n")
121}