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}