@@ -1,6 +1,7 @@
1package list
1package list
6 "charm.land/lipgloss/v2"
7 "charm.land/lipgloss/v2"
@@ -10,6 +11,50 @@ import (
10 "github.com/charmbracelet/ultraviolet/screen"
11 "github.com/charmbracelet/ultraviolet/screen"
14+// toUVStyle converts a lipgloss.Style to a uv.Style, stripping multiline attributes.
15+func toUVStyle(lgStyle lipgloss.Style) uv.Style {
18+ // Colors are already color.Color
19+ uvStyle.Fg = lgStyle.GetForeground()
20+ uvStyle.Bg = lgStyle.GetBackground()
22+ // Build attributes using bitwise OR
25+ if lgStyle.GetBold() {
29+ if lgStyle.GetItalic() {
30+ attrs |= uv.AttrItalic
33+ if lgStyle.GetUnderline() {
34+ uvStyle.Underline = uv.UnderlineSingle
37+ if lgStyle.GetStrikethrough() {
38+ attrs |= uv.AttrStrikethrough
41+ if lgStyle.GetFaint() {
42+ attrs |= uv.AttrFaint
45+ if lgStyle.GetBlink() {
46+ attrs |= uv.AttrBlink
49+ if lgStyle.GetReverse() {
50+ attrs |= uv.AttrReverse
53+ uvStyle.Attrs = attrs
13// Item represents a list item that can draw itself to a UV buffer.
58// Item represents a list item that can draw itself to a UV buffer.
14// Items implement the uv.Drawable interface.
59// Items implement the uv.Drawable interface.
15type Item interface {
60type Item interface {
@@ -31,6 +76,17 @@ type Focusable interface {
31 IsFocused() bool
76 IsFocused() bool
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)
86+ // GetHighlight returns the current highlight region.
87+ GetHighlight() (startLine, startCol, endLine, endCol int)
34// BaseFocusable provides common focus state and styling for items.
90// BaseFocusable provides common focus state and styling for items.
35// Embed this type to add focus behavior to any item.
91// Embed this type to add focus behavior to any item.
36type BaseFocusable struct {
92type BaseFocusable struct {
@@ -74,11 +130,148 @@ func (b *BaseFocusable) SetFocusStyles(focusStyle, blurStyle *lipgloss.Style) {
74 b.blurStyle = blurStyle
130 b.blurStyle = blurStyle
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
140+ highlightStyle CellStyler
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
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
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
162+// SetHighlightStyle sets the style function used for highlighting.
163+func (b *BaseHighlightable) SetHighlightStyle(style CellStyler) {
164+ b.highlightStyle = style
167+// GetHighlightStyle returns the current highlight style function.
168+func (b *BaseHighlightable) GetHighlightStyle() CellStyler {
169+ return b.highlightStyle
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))
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 {
189+ topMargin, topBorder, topPadding int
190+ rightMargin, rightBorder, rightPadding int
191+ bottomMargin, bottomBorder, bottomPadding int
192+ leftMargin, leftBorder, leftPadding int
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()
203+ // Calculate content area offsets
204+ contentArea := image.Rectangle{
206+ X: leftMargin + leftBorder + leftPadding,
207+ Y: topMargin + topBorder + topPadding,
210+ X: width - (rightMargin + rightBorder + rightPadding),
211+ Y: height - (bottomMargin + bottomBorder + bottomPadding),
215+ for y := b.highlightStartLine; y <= b.highlightEndLine && y < height; y++ {
216+ if y >= buf.Height() {
222+ // Determine column range for this line
224+ if y == b.highlightStartLine {
225+ startCol = min(b.highlightStartCol, len(line))
229+ if y == b.highlightEndLine {
230+ endCol = min(b.highlightEndCol, len(line))
233+ // Track last non-empty position as we go
236+ // Single pass: check content and track last non-empty position
237+ for x := startCol; x < endCol; x++ {
243+ // Update last content position if non-empty
244+ if cell.Content != "" && cell.Content != " " {
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
257+ // Apply highlight style only to cells with content
258+ for x := startCol; x < highlightEnd; x++ {
259+ if !image.Pt(x, y).In(contentArea) {
263+ cell.Style = b.highlightStyle(cell.Style)
77// StringItem is a simple string-based item with optional text wrapping.
268// StringItem is a simple string-based item with optional text wrapping.
78// It caches rendered content by width for efficient repeated rendering.
269// It caches rendered content by width for efficient repeated rendering.
79// StringItem implements Focusable if focusStyle and blurStyle are set via WithFocusStyles.
270// StringItem implements Focusable if focusStyle and blurStyle are set via WithFocusStyles.
271+// StringItem implements Highlightable for text selection/search highlighting.
80type StringItem struct {
272type StringItem struct {
81 BaseFocusable
273 BaseFocusable
82 id string
275 id string
83 content string // Raw content string (may contain ANSI styles)
276 content string // Raw content string (may contain ANSI styles)
84 wrap bool // Whether to wrap text
277 wrap bool // Whether to wrap text
@@ -88,24 +281,51 @@ type StringItem struct {
88 cache map[int]string
281 cache map[int]string
284+// CellStyler is a function that applies styles to UV cells.
285+type CellStyler = func(s uv.Style) uv.Style
287+var noColor = lipgloss.NoColor{}
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 {
296+ if uvStyle.Bg != nil && lgStyle.GetBackground() != noColor {
299+ s.Attrs |= uvStyle.Attrs
300+ if uvStyle.Underline != 0 {
301+ s.Underline = uvStyle.Underline
91// NewStringItem creates a new string item with the given ID and content.
307// NewStringItem creates a new string item with the given ID and content.
92func NewStringItem(id, content string) *StringItem {
308func NewStringItem(id, content string) *StringItem {
93− return &StringItem{
309+ s := &StringItem{
95 content: content,
311 content: content,
96 wrap: false,
312 wrap: false,
97 cache: make(map[int]string),
313 cache: make(map[int]string),
101// NewWrappingStringItem creates a new string item that wraps text to fit width.
319// NewWrappingStringItem creates a new string item that wraps text to fit width.
102func NewWrappingStringItem(id, content string) *StringItem {
320func NewWrappingStringItem(id, content string) *StringItem {
103− return &StringItem{
321+ s := &StringItem{
105 content: content,
323 content: content,
106 wrap: true,
324 wrap: true,
107 cache: make(map[int]string),
325 cache: make(map[int]string),
111// WithFocusStyles sets the focus and blur styles for the string item.
331// WithFocusStyles sets the focus and blur styles for the string item.
@@ -153,6 +373,7 @@ func (s *StringItem) Height(width int) int {
153// Draw implements Item and uv.Drawable.
373// Draw implements Item and uv.Drawable.
154func (s *StringItem) Draw(scr uv.Screen, area uv.Rectangle) {
374func (s *StringItem) Draw(scr uv.Screen, area uv.Rectangle) {
155 width := area.Dx()
375 width := area.Dx()
157 // Check cache first
378 // Check cache first
158 content, ok := s.cache[width]
379 content, ok := s.cache[width]
@@ -167,21 +388,41 @@ func (s *StringItem) Draw(scr uv.Screen, area uv.Rectangle) {
169 // Apply focus/blur styling if configured
390 // Apply focus/blur styling if configured
170− if style := s.CurrentStyle(); style != nil {
391+ style := s.CurrentStyle()
171 content = style.Width(width).Render(content)
393 content = style.Width(width).Render(content)
174− // Draw the styled string
396+ // Create temp buffer to draw content with highlighting
397+ tempBuf := uv.NewScreenBuffer(width, height)
399+ // Draw content to temp buffer first
175 styled := uv.NewStyledString(content)
400 styled := uv.NewStyledString(content)
401+ styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
403+ // Apply highlighting if active
404+ s.ApplyHighlight(&tempBuf, width, height, style)
406+ // Copy temp buffer to actual screen at the target area
176− styled.Draw(scr, area)
407+ tempBuf.Draw(scr, area)
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)
179// MarkdownItem renders markdown content using Glamour.
418// MarkdownItem renders markdown content using Glamour.
180// It caches all rendered content by width for efficient repeated rendering.
419// It caches all rendered content by width for efficient repeated rendering.
181// The wrap width is capped at 120 cells by default to ensure readable line lengths.
420// The wrap width is capped at 120 cells by default to ensure readable line lengths.
182// MarkdownItem implements Focusable if focusStyle and blurStyle are set via WithFocusStyles.
421// MarkdownItem implements Focusable if focusStyle and blurStyle are set via WithFocusStyles.
422+// MarkdownItem implements Highlightable for text selection/search highlighting.
183type MarkdownItem struct {
423type MarkdownItem struct {
184 BaseFocusable
424 BaseFocusable
185 id string
426 id string
186 markdown string // Raw markdown content
427 markdown string // Raw markdown content
187 styleConfig *ansi.StyleConfig // Optional style configuration
428 styleConfig *ansi.StyleConfig // Optional style configuration
@@ -204,7 +445,7 @@ func NewMarkdownItem(id, markdown string) *MarkdownItem {
204 maxWidth: DefaultMarkdownMaxWidth,
445 maxWidth: DefaultMarkdownMaxWidth,
205 cache: make(map[int]string),
446 cache: make(map[int]string),
207−
448+ m.InitHighlight()
@@ -248,16 +489,27 @@ func (m *MarkdownItem) Height(width int) int {
248// Draw implements Item and uv.Drawable.
489// Draw implements Item and uv.Drawable.
249func (m *MarkdownItem) Draw(scr uv.Screen, area uv.Rectangle) {
490func (m *MarkdownItem) Draw(scr uv.Screen, area uv.Rectangle) {
250 width := area.Dx()
491 width := area.Dx()
251 rendered := m.renderMarkdown(width)
493 rendered := m.renderMarkdown(width)
253 // Apply focus/blur styling if configured
495 // Apply focus/blur styling if configured
254− if style := m.CurrentStyle(); style != nil {
496+ style := m.CurrentStyle()
255 rendered = style.Render(rendered)
498 rendered = style.Render(rendered)
501+ // Create temp buffer to draw content with highlighting
502+ tempBuf := uv.NewScreenBuffer(width, height)
258− // Draw the rendered markdown
504+ // Draw the rendered markdown to temp buffer
259 styled := uv.NewStyledString(rendered)
505 styled := uv.NewStyledString(rendered)
506+ styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
508+ // Apply highlighting if active
509+ m.ApplyHighlight(&tempBuf, width, height, style)
511+ // Copy temp buffer to actual screen at the target area
260− styled.Draw(scr, area)
512+ tempBuf.Draw(scr, area)
263// renderMarkdown renders the markdown content at the given width, using cache if available.
515// renderMarkdown renders the markdown content at the given width, using cache if available.
@@ -302,6 +554,14 @@ func (m *MarkdownItem) renderMarkdown(width int) string {
302 return rendered
554 return rendered
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)
305// Gap is a 1-line spacer item used to add gaps between items.
565// Gap is a 1-line spacer item used to add gaps between items.
306var Gap = NewSpacerItem("spacer-gap", 1)
566var Gap = NewSpacerItem("spacer-gap", 1)