highlight.go

  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}