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	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}