1package list
2
3import (
4 "image"
5
6 "charm.land/lipgloss/v2"
7 uv "github.com/charmbracelet/ultraviolet"
8)
9
10// DefaultHighlighter is the default highlighter function that applies inverse style.
11var DefaultHighlighter Highlighter = func(s uv.Style) uv.Style {
12 s.Attrs |= uv.AttrReverse
13 return s
14}
15
16// Highlighter represents a function that defines how to highlight text.
17type Highlighter func(uv.Style) uv.Style
18
19// Highlight highlights a region of text within the given content and region.
20func Highlight(content string, area image.Rectangle, startLine, startCol, endLine, endCol int, highlighter Highlighter) string {
21 if startLine < 0 || startCol < 0 {
22 return content
23 }
24
25 if highlighter == nil {
26 highlighter = DefaultHighlighter
27 }
28
29 width, height := area.Dx(), area.Dy()
30 buf := uv.NewScreenBuffer(width, height)
31 styled := uv.NewStyledString(content)
32 styled.Draw(&buf, area)
33
34 // Treat -1 as "end of content"
35 if endLine < 0 {
36 endLine = height - 1
37 }
38 if endCol < 0 {
39 endCol = width
40 }
41
42 for y := startLine; y <= endLine && y < height; y++ {
43 if y >= buf.Height() {
44 break
45 }
46
47 line := buf.Line(y)
48
49 // Determine column range for this line
50 colStart := 0
51 if y == startLine {
52 colStart = min(startCol, len(line))
53 }
54
55 colEnd := len(line)
56 if y == endLine {
57 colEnd = min(endCol, len(line))
58 }
59
60 // Track last non-empty position as we go
61 lastContentX := -1
62
63 // Single pass: check content and track last non-empty position
64 for x := colStart; x < colEnd; x++ {
65 cell := line.At(x)
66 if cell == nil {
67 continue
68 }
69
70 // Update last content position if non-empty
71 if cell.Content != "" && cell.Content != " " {
72 lastContentX = x
73 }
74 }
75
76 // Only apply highlight up to last content position
77 highlightEnd := colEnd
78 if lastContentX >= 0 {
79 highlightEnd = lastContentX + 1
80 } else if lastContentX == -1 {
81 highlightEnd = colStart // No content on this line
82 }
83
84 // Apply highlight style only to cells with content
85 for x := colStart; x < highlightEnd; x++ {
86 if !image.Pt(x, y).In(area) {
87 continue
88 }
89 cell := line.At(x)
90 cell.Style = highlighter(cell.Style)
91 }
92 }
93
94 return buf.Render()
95}
96
97// ToHighlighter converts a [lipgloss.Style] to a [Highlighter].
98func ToHighlighter(lgStyle lipgloss.Style) Highlighter {
99 return func(uv.Style) uv.Style {
100 return ToStyle(lgStyle)
101 }
102}
103
104// ToStyle converts an inline [lipgloss.Style] to a [uv.Style].
105func ToStyle(lgStyle lipgloss.Style) uv.Style {
106 var uvStyle uv.Style
107
108 // Colors are already color.Color
109 uvStyle.Fg = lgStyle.GetForeground()
110 uvStyle.Bg = lgStyle.GetBackground()
111
112 // Build attributes using bitwise OR
113 var attrs uint8
114
115 if lgStyle.GetBold() {
116 attrs |= uv.AttrBold
117 }
118
119 if lgStyle.GetItalic() {
120 attrs |= uv.AttrItalic
121 }
122
123 if lgStyle.GetUnderline() {
124 uvStyle.Underline = uv.UnderlineSingle
125 }
126
127 if lgStyle.GetStrikethrough() {
128 attrs |= uv.AttrStrikethrough
129 }
130
131 if lgStyle.GetFaint() {
132 attrs |= uv.AttrFaint
133 }
134
135 if lgStyle.GetBlink() {
136 attrs |= uv.AttrBlink
137 }
138
139 if lgStyle.GetReverse() {
140 attrs |= uv.AttrReverse
141 }
142
143 uvStyle.Attrs = attrs
144
145 return uvStyle
146}
147
148// AdjustArea adjusts the given area rectangle by subtracting margins, borders,
149// and padding from the style.
150func AdjustArea(area image.Rectangle, style lipgloss.Style) image.Rectangle {
151 topMargin, rightMargin, bottomMargin, leftMargin := style.GetMargin()
152 topBorder, rightBorder, bottomBorder, leftBorder := style.GetBorderTopSize(),
153 style.GetBorderRightSize(),
154 style.GetBorderBottomSize(),
155 style.GetBorderLeftSize()
156 topPadding, rightPadding, bottomPadding, leftPadding := style.GetPadding()
157
158 return image.Rectangle{
159 Min: image.Point{
160 X: area.Min.X + leftMargin + leftBorder + leftPadding,
161 Y: area.Min.Y + topMargin + topBorder + topPadding,
162 },
163 Max: image.Point{
164 X: area.Max.X - (rightMargin + rightBorder + rightPadding),
165 Y: area.Max.Y - (bottomMargin + bottomBorder + bottomPadding),
166 },
167 }
168}