package list import ( "fmt" "io" "strings" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" ) // DefaultItemStyles defines styling for a default list item. // See DefaultItemView for when these come into play. type DefaultItemStyles struct { // The Normal state. NormalTitle lipgloss.Style NormalDesc lipgloss.Style // The selected item state. SelectedTitle lipgloss.Style SelectedDesc lipgloss.Style // The dimmed state, for when the filter input is initially activated. DimmedTitle lipgloss.Style DimmedDesc lipgloss.Style // Characters matching the current filter, if any. FilterMatch lipgloss.Style } // NewDefaultItemStyles returns style definitions for a default item. See // DefaultItemView for when these come into play. func NewDefaultItemStyles(isDark bool) (s DefaultItemStyles) { normalTitleColor := "#1a1a1a" normalDescColor := "#A49FA5" selectedTitleBorderFgColor := "#F793FF" selectedTitleFgColor := "#EE6FF8" selectedDescColor := "#F793FF" dimmedTitleColor := "#A49FA5" dimmedDescColor := "#C2B8C2" if isDark { normalTitleColor = "#dddddd" normalDescColor = "#777777" selectedTitleBorderFgColor = "#AD58B4" selectedTitleFgColor = "#EE6FF8" selectedDescColor = "#AD58B4" dimmedTitleColor = "#777777" dimmedDescColor = "#4D4D4D" } s.NormalTitle = lipgloss.NewStyle(). Foreground(lipgloss.Color(normalTitleColor)). Padding(0, 0, 0, 2) s.NormalDesc = s.NormalTitle. Foreground(lipgloss.Color(normalDescColor)) s.SelectedTitle = lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), false, false, false, true). BorderForeground(lipgloss.Color(selectedTitleBorderFgColor)). Foreground(lipgloss.Color(selectedTitleFgColor)). Padding(0, 0, 0, 1) s.SelectedDesc = s.SelectedTitle. Foreground(lipgloss.Color(selectedDescColor)) s.DimmedTitle = lipgloss.NewStyle(). Foreground(lipgloss.Color(dimmedTitleColor)). Padding(0, 0, 0, 2) s.DimmedDesc = s.DimmedTitle. Foreground(lipgloss.Color(dimmedDescColor)) s.FilterMatch = lipgloss.NewStyle().Underline(true) return s } // DefaultItem describes an items designed to work with DefaultDelegate. type DefaultItem interface { Item Title() string Description() string } // DefaultDelegate is a standard delegate designed to work in lists. It's // styled by DefaultItemStyles, which can be customized as you like. // // The description line can be hidden by setting Description to false, which // renders the list as single-line-items. The spacing between items can be set // with the SetSpacing method. // // Setting UpdateFunc is optional. If it's set it will be called when the // ItemDelegate called, which is called when the list's Update function is // invoked. // // Settings ShortHelpFunc and FullHelpFunc is optional. They can be set to // include items in the list's default short and full help menus. type DefaultDelegate struct { ShowDescription bool Styles DefaultItemStyles UpdateFunc func(tea.Msg, *Model) tea.Cmd ShortHelpFunc func() []key.Binding FullHelpFunc func() [][]key.Binding height int spacing int } // NewDefaultDelegate creates a new delegate with default styles. func NewDefaultDelegate() DefaultDelegate { return DefaultDelegate{ ShowDescription: true, Styles: NewDefaultItemStyles(true), height: 2, spacing: 1, } } // SetHeight sets delegate's preferred height. func (d *DefaultDelegate) 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 DefaultDelegate) Height() int { if d.ShowDescription { return d.height } return 1 } // SetSpacing sets the delegate's spacing. func (d *DefaultDelegate) SetSpacing(i int) { d.spacing = i } // Spacing returns the delegate's spacing. func (d DefaultDelegate) Spacing() int { return d.spacing } // Update checks whether the delegate's UpdateFunc is set and calls it. func (d DefaultDelegate) Update(msg tea.Msg, m *Model) tea.Cmd { if d.UpdateFunc == nil { return nil } return d.UpdateFunc(msg, m) } // Render prints an item. func (d DefaultDelegate) Render(w io.Writer, m Model, index int, item Item) { var ( title, desc string matchedRunes []int s = &d.Styles ) if i, ok := item.(DefaultItem); ok { title = i.Title() desc = i.Description() } else { return } if m.width <= 0 { // short-circuit return } // Prevent text from exceeding list width textwidth := m.width - s.NormalTitle.GetPaddingLeft() - s.NormalTitle.GetPaddingRight() title = ansi.Truncate(title, textwidth, ellipsis) if d.ShowDescription { var lines []string for i, line := range strings.Split(desc, "\n") { if i >= d.height-1 { break } lines = append(lines, ansi.Truncate(line, textwidth, ellipsis)) } desc = strings.Join(lines, "\n") } // Conditions var ( isSelected = index == m.Index() emptyFilter = m.FilterState() == Filtering && m.FilterValue() == "" isFiltered = m.FilterState() == Filtering || m.FilterState() == FilterApplied ) if isFiltered && index < len(m.filteredItems) { // Get indices of matched characters matchedRunes = m.MatchesForItem(index) } if emptyFilter { title = s.DimmedTitle.Render(title) desc = s.DimmedDesc.Render(desc) } else if isSelected && m.FilterState() != Filtering { if isFiltered { // Highlight matches unmatched := s.SelectedTitle.Inline(true) matched := unmatched.Inherit(s.FilterMatch) title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched) } title = s.SelectedTitle.Render(title) desc = s.SelectedDesc.Render(desc) } else { if isFiltered { // Highlight matches unmatched := s.NormalTitle.Inline(true) matched := unmatched.Inherit(s.FilterMatch) title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched) } title = s.NormalTitle.Render(title) desc = s.NormalDesc.Render(desc) } 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 DefaultDelegate) ShortHelp() []key.Binding { if d.ShortHelpFunc != nil { return d.ShortHelpFunc() } return nil } // FullHelp returns the delegate's full help. func (d DefaultDelegate) FullHelp() [][]key.Binding { if d.FullHelpFunc != nil { return d.FullHelpFunc() } return nil }