package main import ( "fmt" "io" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // WrappingDelegate is a delegate that wraps text instead of truncating it. // It's based on the DefaultDelegate but modified to use text wrapping. type WrappingDelegate struct { ShowDescription bool Styles list.DefaultItemStyles UpdateFunc func(tea.Msg, *list.Model) tea.Cmd ShortHelpFunc func() []key.Binding FullHelpFunc func() [][]key.Binding height int spacing int } // NewWrappingDelegate creates a new delegate with wrapping behavior and default styles. func NewWrappingDelegate() WrappingDelegate { const defaultHeight = 2 const defaultSpacing = 1 return WrappingDelegate{ ShowDescription: true, Styles: list.NewDefaultItemStyles(), height: defaultHeight, spacing: defaultSpacing, } } // SetHeight sets delegate's preferred height. func (d *WrappingDelegate) SetHeight(i int) { d.height = i } // Height returns the delegate's preferred height. // This has effect only if ShowDescription is true, // otherwise height is always 1. func (d WrappingDelegate) Height() int { if d.ShowDescription { return d.height } return 1 } // SetSpacing sets the delegate's spacing. func (d *WrappingDelegate) SetSpacing(i int) { d.spacing = i } // Spacing returns the delegate's spacing. func (d WrappingDelegate) Spacing() int { return d.spacing } // Update checks whether the delegate's UpdateFunc is set and calls it. func (d WrappingDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { if d.UpdateFunc == nil { return nil } return d.UpdateFunc(msg, m) } // wrapText wraps text to the specified width using lipgloss func wrapText(text string, width int, style lipgloss.Style) string { if width <= 0 { return text } return style.Width(width).Render(text) } // Render prints an item with text wrapping instead of truncation. func (d WrappingDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) { var ( title, desc string matchedRunes []int s = &d.Styles ) if i, ok := item.(list.DefaultItem); ok { title = i.Title() desc = i.Description() } else { return } if m.Width() <= 0 { // short-circuit return } // Calculate available text width textwidth := m.Width() - s.NormalTitle.GetPaddingLeft() - s.NormalTitle.GetPaddingRight() // Conditions var ( isSelected = index == m.Index() emptyFilter = m.FilterState() == list.Filtering && m.FilterValue() == "" isFiltered = m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied ) if isFiltered && index < len(m.VisibleItems()) { // Get indices of matched characters matchedRunes = m.MatchesForItem(index) } // Apply styling and wrapping based on state if emptyFilter { title = wrapText(title, textwidth, s.DimmedTitle) if d.ShowDescription { desc = wrapText(desc, textwidth, s.DimmedDesc) } } else if isSelected && m.FilterState() != list.Filtering { if isFiltered { // Highlight matches unmatched := s.SelectedTitle.Inline(true) matched := unmatched.Inherit(s.FilterMatch) title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched) } title = wrapText(title, textwidth, s.SelectedTitle) if d.ShowDescription { desc = wrapText(desc, textwidth, s.SelectedDesc) } } else { if isFiltered { // Highlight matches unmatched := s.NormalTitle.Inline(true) matched := unmatched.Inherit(s.FilterMatch) title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched) } title = wrapText(title, textwidth, s.NormalTitle) if d.ShowDescription { desc = wrapText(desc, textwidth, s.NormalDesc) } } if d.ShowDescription { fmt.Fprintf(w, "%s\n%s", title, desc) //nolint: errcheck return } fmt.Fprintf(w, "%s", title) //nolint: errcheck } // ShortHelp returns the delegate's short help. func (d WrappingDelegate) ShortHelp() []key.Binding { if d.ShortHelpFunc != nil { return d.ShortHelpFunc() } return nil } // FullHelp returns the delegate's full help. func (d WrappingDelegate) FullHelp() [][]key.Binding { if d.FullHelpFunc != nil { return d.FullHelpFunc() } return nil }