@@ -1,6 +1,7 @@
11package list
22
33import (
4+ "image"
45 "strings"
56
67 "charm.land/lipgloss/v2"
@@ -10,6 +11,50 @@ import (
1011 "github.com/charmbracelet/ultraviolet/screen"
1112)
1213
14+// toUVStyle converts a lipgloss.Style to a uv.Style, stripping multiline attributes.
15+func toUVStyle(lgStyle lipgloss.Style) uv.Style {
16+ var uvStyle uv.Style
17+
18+ // Colors are already color.Color
19+ uvStyle.Fg = lgStyle.GetForeground()
20+ uvStyle.Bg = lgStyle.GetBackground()
21+
22+ // Build attributes using bitwise OR
23+ var attrs uint8
24+
25+ if lgStyle.GetBold() {
26+ attrs |= uv.AttrBold
27+ }
28+
29+ if lgStyle.GetItalic() {
30+ attrs |= uv.AttrItalic
31+ }
32+
33+ if lgStyle.GetUnderline() {
34+ uvStyle.Underline = uv.UnderlineSingle
35+ }
36+
37+ if lgStyle.GetStrikethrough() {
38+ attrs |= uv.AttrStrikethrough
39+ }
40+
41+ if lgStyle.GetFaint() {
42+ attrs |= uv.AttrFaint
43+ }
44+
45+ if lgStyle.GetBlink() {
46+ attrs |= uv.AttrBlink
47+ }
48+
49+ if lgStyle.GetReverse() {
50+ attrs |= uv.AttrReverse
51+ }
52+
53+ uvStyle.Attrs = attrs
54+
55+ return uvStyle
56+}
57+
1358// Item represents a list item that can draw itself to a UV buffer.
1459// Items implement the uv.Drawable interface.
1560type Item interface {
@@ -31,6 +76,17 @@ type Focusable interface {
3176 IsFocused() bool
3277}
3378
79+// Highlightable is an optional interface for items that support highlighting.
80+// When implemented, items can highlight specific regions (e.g. for search matches).
81+type Highlightable interface {
82+ // SetHighlight sets the highlight region (startLine, startCol) to (endLine, endCol).
83+ // Use -1 for all values to clear highlighting.
84+ SetHighlight(startLine, startCol, endLine, endCol int)
85+
86+ // GetHighlight returns the current highlight region.
87+ GetHighlight() (startLine, startCol, endLine, endCol int)
88+}
89+
3490// BaseFocusable provides common focus state and styling for items.
3591// Embed this type to add focus behavior to any item.
3692type BaseFocusable struct {
@@ -74,11 +130,148 @@ func (b *BaseFocusable) SetFocusStyles(focusStyle, blurStyle *lipgloss.Style) {
74130 b.blurStyle = blurStyle
75131}
76132
133+// BaseHighlightable provides common highlight state for items.
134+// Embed this type to add highlight behavior to any item.
135+type BaseHighlightable struct {
136+ highlightStartLine int
137+ highlightStartCol int
138+ highlightEndLine int
139+ highlightEndCol int
140+ highlightStyle CellStyler
141+}
142+
143+// SetHighlight implements Highlightable interface.
144+func (b *BaseHighlightable) SetHighlight(startLine, startCol, endLine, endCol int) {
145+ b.highlightStartLine = startLine
146+ b.highlightStartCol = startCol
147+ b.highlightEndLine = endLine
148+ b.highlightEndCol = endCol
149+}
150+
151+// GetHighlight implements Highlightable interface.
152+func (b *BaseHighlightable) GetHighlight() (startLine, startCol, endLine, endCol int) {
153+ return b.highlightStartLine, b.highlightStartCol, b.highlightEndLine, b.highlightEndCol
154+}
155+
156+// HasHighlight returns true if a highlight region is set.
157+func (b *BaseHighlightable) HasHighlight() bool {
158+ return b.highlightStartLine >= 0 || b.highlightStartCol >= 0 ||
159+ b.highlightEndLine >= 0 || b.highlightEndCol >= 0
160+}
161+
162+// SetHighlightStyle sets the style function used for highlighting.
163+func (b *BaseHighlightable) SetHighlightStyle(style CellStyler) {
164+ b.highlightStyle = style
165+}
166+
167+// GetHighlightStyle returns the current highlight style function.
168+func (b *BaseHighlightable) GetHighlightStyle() CellStyler {
169+ return b.highlightStyle
170+}
171+
172+// InitHighlight initializes the highlight fields with default values.
173+func (b *BaseHighlightable) InitHighlight() {
174+ b.highlightStartLine = -1
175+ b.highlightStartCol = -1
176+ b.highlightEndLine = -1
177+ b.highlightEndCol = -1
178+ b.highlightStyle = LipglossStyleToCellStyler(lipgloss.NewStyle().Reverse(true))
179+}
180+
181+// ApplyHighlight applies highlighting to a screen buffer.
182+// This should be called after drawing content to the buffer.
183+func (b *BaseHighlightable) ApplyHighlight(buf *uv.ScreenBuffer, width, height int, style *lipgloss.Style) {
184+ if b.highlightStartLine < 0 {
185+ return
186+ }
187+
188+ var (
189+ topMargin, topBorder, topPadding int
190+ rightMargin, rightBorder, rightPadding int
191+ bottomMargin, bottomBorder, bottomPadding int
192+ leftMargin, leftBorder, leftPadding int
193+ )
194+ if style != nil {
195+ topMargin, rightMargin, bottomMargin, leftMargin = style.GetMargin()
196+ topBorder, rightBorder, bottomBorder, leftBorder = style.GetBorderTopSize(),
197+ style.GetBorderRightSize(),
198+ style.GetBorderBottomSize(),
199+ style.GetBorderLeftSize()
200+ topPadding, rightPadding, bottomPadding, leftPadding = style.GetPadding()
201+ }
202+
203+ // Calculate content area offsets
204+ contentArea := image.Rectangle{
205+ Min: image.Point{
206+ X: leftMargin + leftBorder + leftPadding,
207+ Y: topMargin + topBorder + topPadding,
208+ },
209+ Max: image.Point{
210+ X: width - (rightMargin + rightBorder + rightPadding),
211+ Y: height - (bottomMargin + bottomBorder + bottomPadding),
212+ },
213+ }
214+
215+ for y := b.highlightStartLine; y <= b.highlightEndLine && y < height; y++ {
216+ if y >= buf.Height() {
217+ break
218+ }
219+
220+ line := buf.Line(y)
221+
222+ // Determine column range for this line
223+ startCol := 0
224+ if y == b.highlightStartLine {
225+ startCol = min(b.highlightStartCol, len(line))
226+ }
227+
228+ endCol := len(line)
229+ if y == b.highlightEndLine {
230+ endCol = min(b.highlightEndCol, len(line))
231+ }
232+
233+ // Track last non-empty position as we go
234+ lastContentX := -1
235+
236+ // Single pass: check content and track last non-empty position
237+ for x := startCol; x < endCol; x++ {
238+ cell := line.At(x)
239+ if cell == nil {
240+ continue
241+ }
242+
243+ // Update last content position if non-empty
244+ if cell.Content != "" && cell.Content != " " {
245+ lastContentX = x
246+ }
247+ }
248+
249+ // Only apply highlight up to last content position
250+ highlightEnd := endCol
251+ if lastContentX >= 0 {
252+ highlightEnd = lastContentX + 1
253+ } else if lastContentX == -1 {
254+ highlightEnd = startCol // No content on this line
255+ }
256+
257+ // Apply highlight style only to cells with content
258+ for x := startCol; x < highlightEnd; x++ {
259+ if !image.Pt(x, y).In(contentArea) {
260+ continue
261+ }
262+ cell := line.At(x)
263+ cell.Style = b.highlightStyle(cell.Style)
264+ }
265+ }
266+}
267+
77268// StringItem is a simple string-based item with optional text wrapping.
78269// It caches rendered content by width for efficient repeated rendering.
79270// StringItem implements Focusable if focusStyle and blurStyle are set via WithFocusStyles.
271+// StringItem implements Highlightable for text selection/search highlighting.
80272type StringItem struct {
81273 BaseFocusable
274+ BaseHighlightable
82275 id string
83276 content string // Raw content string (may contain ANSI styles)
84277 wrap bool // Whether to wrap text
@@ -88,24 +281,51 @@ type StringItem struct {
88281 cache map[int]string
89282}
90283
284+// CellStyler is a function that applies styles to UV cells.
285+type CellStyler = func(s uv.Style) uv.Style
286+
287+var noColor = lipgloss.NoColor{}
288+
289+// LipglossStyleToCellStyler converts a Lip Gloss style to a CellStyler function.
290+func LipglossStyleToCellStyler(lgStyle lipgloss.Style) CellStyler {
291+ uvStyle := toUVStyle(lgStyle)
292+ return func(s uv.Style) uv.Style {
293+ if uvStyle.Fg != nil && lgStyle.GetForeground() != noColor {
294+ s.Fg = uvStyle.Fg
295+ }
296+ if uvStyle.Bg != nil && lgStyle.GetBackground() != noColor {
297+ s.Bg = uvStyle.Bg
298+ }
299+ s.Attrs |= uvStyle.Attrs
300+ if uvStyle.Underline != 0 {
301+ s.Underline = uvStyle.Underline
302+ }
303+ return s
304+ }
305+}
306+
91307// NewStringItem creates a new string item with the given ID and content.
92308func NewStringItem(id, content string) *StringItem {
93− return &StringItem{
309+ s := &StringItem{
94310 id: id,
95311 content: content,
96312 wrap: false,
97313 cache: make(map[int]string),
98314 }
315+ s.InitHighlight()
316+ return s
99317}
100318
101319// NewWrappingStringItem creates a new string item that wraps text to fit width.
102320func NewWrappingStringItem(id, content string) *StringItem {
103− return &StringItem{
321+ s := &StringItem{
104322 id: id,
105323 content: content,
106324 wrap: true,
107325 cache: make(map[int]string),
108326 }
327+ s.InitHighlight()
328+ return s
109329}
110330
111331// WithFocusStyles sets the focus and blur styles for the string item.
@@ -153,6 +373,7 @@ func (s *StringItem) Height(width int) int {
153373// Draw implements Item and uv.Drawable.
154374func (s *StringItem) Draw(scr uv.Screen, area uv.Rectangle) {
155375 width := area.Dx()
376+ height := area.Dy()
156377
157378 // Check cache first
158379 content, ok := s.cache[width]
@@ -167,21 +388,41 @@ func (s *StringItem) Draw(scr uv.Screen, area uv.Rectangle) {
167388 }
168389
169390 // Apply focus/blur styling if configured
170− if style := s.CurrentStyle(); style != nil {
391+ style := s.CurrentStyle()
392+ if style != nil {
171393 content = style.Width(width).Render(content)
172394 }
173395
174− // Draw the styled string
396+ // Create temp buffer to draw content with highlighting
397+ tempBuf := uv.NewScreenBuffer(width, height)
398+
399+ // Draw content to temp buffer first
175400 styled := uv.NewStyledString(content)
176− styled.Draw(scr, area)
401+ styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
402+
403+ // Apply highlighting if active
404+ s.ApplyHighlight(&tempBuf, width, height, style)
405+
406+ // Copy temp buffer to actual screen at the target area
407+ tempBuf.Draw(scr, area)
408+}
409+
410+// SetHighlight implements Highlightable and extends BaseHighlightable.
411+// Clears the cache when highlight changes.
412+func (s *StringItem) SetHighlight(startLine, startCol, endLine, endCol int) {
413+ s.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol)
414+ // Clear cache when highlight changes
415+ s.cache = make(map[int]string)
177416}
178417
179418// MarkdownItem renders markdown content using Glamour.
180419// It caches all rendered content by width for efficient repeated rendering.
181420// The wrap width is capped at 120 cells by default to ensure readable line lengths.
182421// MarkdownItem implements Focusable if focusStyle and blurStyle are set via WithFocusStyles.
422+// MarkdownItem implements Highlightable for text selection/search highlighting.
183423type MarkdownItem struct {
184424 BaseFocusable
425+ BaseHighlightable
185426 id string
186427 markdown string // Raw markdown content
187428 styleConfig *ansi.StyleConfig // Optional style configuration
@@ -204,7 +445,7 @@ func NewMarkdownItem(id, markdown string) *MarkdownItem {
204445 maxWidth: DefaultMarkdownMaxWidth,
205446 cache: make(map[int]string),
206447 }
207−
448+ m.InitHighlight()
208449 return m
209450}
210451
@@ -248,16 +489,27 @@ func (m *MarkdownItem) Height(width int) int {
248489// Draw implements Item and uv.Drawable.
249490func (m *MarkdownItem) Draw(scr uv.Screen, area uv.Rectangle) {
250491 width := area.Dx()
492+ height := area.Dy()
251493 rendered := m.renderMarkdown(width)
252494
253495 // Apply focus/blur styling if configured
254− if style := m.CurrentStyle(); style != nil {
496+ style := m.CurrentStyle()
497+ if style != nil {
255498 rendered = style.Render(rendered)
256499 }
257500
258− // Draw the rendered markdown
501+ // Create temp buffer to draw content with highlighting
502+ tempBuf := uv.NewScreenBuffer(width, height)
503+
504+ // Draw the rendered markdown to temp buffer
259505 styled := uv.NewStyledString(rendered)
260− styled.Draw(scr, area)
506+ styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
507+
508+ // Apply highlighting if active
509+ m.ApplyHighlight(&tempBuf, width, height, style)
510+
511+ // Copy temp buffer to actual screen at the target area
512+ tempBuf.Draw(scr, area)
261513}
262514
263515// renderMarkdown renders the markdown content at the given width, using cache if available.
@@ -302,6 +554,14 @@ func (m *MarkdownItem) renderMarkdown(width int) string {
302554 return rendered
303555}
304556
557+// SetHighlight implements Highlightable and extends BaseHighlightable.
558+// Clears the cache when highlight changes.
559+func (m *MarkdownItem) SetHighlight(startLine, startCol, endLine, endCol int) {
560+ m.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol)
561+ // Clear cache when highlight changes
562+ m.cache = make(map[int]string)
563+}
564+
305565// Gap is a 1-line spacer item used to add gaps between items.
306566var Gap = NewSpacerItem("spacer-gap", 1)
307567