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 for y := startLine; y <= endLine && y < height; y++ {
35 if y >= buf.Height() {
36 break
37 }
38
39 line := buf.Line(y)
40
41 // Determine column range for this line
42 colStart := 0
43 if y == startLine {
44 colStart = min(startCol, len(line))
45 }
46
47 colEnd := len(line)
48 if y == endLine {
49 colEnd = min(endCol, len(line))
50 }
51
52 // Track last non-empty position as we go
53 lastContentX := -1
54
55 // Single pass: check content and track last non-empty position
56 for x := colStart; x < colEnd; x++ {
57 cell := line.At(x)
58 if cell == nil {
59 continue
60 }
61
62 // Update last content position if non-empty
63 if cell.Content != "" && cell.Content != " " {
64 lastContentX = x
65 }
66 }
67
68 // Only apply highlight up to last content position
69 highlightEnd := colEnd
70 if lastContentX >= 0 {
71 highlightEnd = lastContentX + 1
72 } else if lastContentX == -1 {
73 highlightEnd = colStart // No content on this line
74 }
75
76 // Apply highlight style only to cells with content
77 for x := colStart; x < highlightEnd; x++ {
78 if !image.Pt(x, y).In(area) {
79 continue
80 }
81 cell := line.At(x)
82 cell.Style = highlighter(cell.Style)
83 }
84 }
85
86 return buf.Render()
87}
88
89// ToHighlighter converts a [lipgloss.Style] to a [Highlighter].
90func ToHighlighter(lgStyle lipgloss.Style) Highlighter {
91 return func(uv.Style) uv.Style {
92 return ToStyle(lgStyle)
93 }
94}
95
96// ToStyle converts an inline [lipgloss.Style] to a [uv.Style].
97func ToStyle(lgStyle lipgloss.Style) uv.Style {
98 var uvStyle uv.Style
99
100 // Colors are already color.Color
101 uvStyle.Fg = lgStyle.GetForeground()
102 uvStyle.Bg = lgStyle.GetBackground()
103
104 // Build attributes using bitwise OR
105 var attrs uint8
106
107 if lgStyle.GetBold() {
108 attrs |= uv.AttrBold
109 }
110
111 if lgStyle.GetItalic() {
112 attrs |= uv.AttrItalic
113 }
114
115 if lgStyle.GetUnderline() {
116 uvStyle.Underline = uv.UnderlineSingle
117 }
118
119 if lgStyle.GetStrikethrough() {
120 attrs |= uv.AttrStrikethrough
121 }
122
123 if lgStyle.GetFaint() {
124 attrs |= uv.AttrFaint
125 }
126
127 if lgStyle.GetBlink() {
128 attrs |= uv.AttrBlink
129 }
130
131 if lgStyle.GetReverse() {
132 attrs |= uv.AttrReverse
133 }
134
135 uvStyle.Attrs = attrs
136
137 return uvStyle
138}
139
140// AdjustArea adjusts the given area rectangle by subtracting margins, borders,
141// and padding from the style.
142func AdjustArea(area image.Rectangle, style lipgloss.Style) image.Rectangle {
143 topMargin, rightMargin, bottomMargin, leftMargin := style.GetMargin()
144 topBorder, rightBorder, bottomBorder, leftBorder := style.GetBorderTopSize(),
145 style.GetBorderRightSize(),
146 style.GetBorderBottomSize(),
147 style.GetBorderLeftSize()
148 topPadding, rightPadding, bottomPadding, leftPadding := style.GetPadding()
149
150 return image.Rectangle{
151 Min: image.Point{
152 X: area.Min.X + leftMargin + leftBorder + leftPadding,
153 Y: area.Min.Y + topMargin + topBorder + topPadding,
154 },
155 Max: image.Point{
156 X: area.Max.X - (rightMargin + rightBorder + rightPadding),
157 Y: area.Max.Y - (bottomMargin + bottomBorder + bottomPadding),
158 },
159 }
160}