mirror of
https://github.com/zoriya/bubbles.git
synced 2026-08-05 04:36:07 +00:00
251 lines
6.8 KiB
Go
251 lines
6.8 KiB
Go
package list
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/v2/key"
|
|
tea "github.com/charmbracelet/bubbletea/v2"
|
|
"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
|
|
}
|
|
|
|
// BaseItemStyles returns a set of base styles for a default item. No color
|
|
// will be applied. You can use this as a starting point for your own styles,
|
|
// if you like.
|
|
func BaseItemStyles() (s DefaultItemStyles) {
|
|
s.NormalTitle = lipgloss.NewStyle().
|
|
Padding(0, 0, 0, 2) //nolint:mnd
|
|
s.NormalDesc = s.NormalTitle
|
|
s.SelectedTitle = lipgloss.NewStyle().
|
|
Border(lipgloss.NormalBorder(), false, false, false, true).
|
|
Padding(0, 0, 0, 1)
|
|
s.SelectedDesc = s.SelectedTitle
|
|
s.DimmedTitle = lipgloss.NewStyle().
|
|
Padding(0, 0, 0, 2) //nolint:mnd
|
|
s.DimmedDesc = s.DimmedTitle
|
|
s.FilterMatch = lipgloss.NewStyle().
|
|
Underline(true)
|
|
return s
|
|
}
|
|
|
|
func newItemStyles(isDark bool) DefaultItemStyles {
|
|
lightDark := lipgloss.LightDark(isDark)
|
|
s := BaseItemStyles()
|
|
|
|
s.NormalTitle = s.NormalTitle.
|
|
Foreground(lightDark("#1a1a1a", "#dddddd"))
|
|
s.NormalDesc = s.NormalDesc.
|
|
Foreground(lightDark("#A49FA5", "#777777"))
|
|
s.SelectedTitle = s.SelectedTitle.
|
|
BorderForeground(lightDark("#F793FF", "#AD58B4")).
|
|
Foreground(lightDark("#EE6FF8", "#EE6FF8"))
|
|
s.SelectedDesc = s.SelectedDesc.
|
|
Foreground(lightDark("#F793FF", "#AD58B4"))
|
|
s.DimmedTitle = s.DimmedTitle.
|
|
Foreground(lightDark("#A49FA5", "#777777"))
|
|
s.DimmedDesc = s.DimmedDesc.
|
|
Foreground(lightDark("#C2B8C2", "#4D4D4D"))
|
|
|
|
return s
|
|
}
|
|
|
|
// NewDefaultItemStyles returns light style definitions for a default item. See
|
|
// [DefaultItemView] for when these come into play.
|
|
func NewLightItemStyles() DefaultItemStyles {
|
|
return newItemStyles(false)
|
|
}
|
|
|
|
// NewDefaultItemStyles returns default dark style definitions for a default
|
|
// item. See [DefaultItemView] for when these come into play.
|
|
func NewDefaultItemStyles() DefaultItemStyles {
|
|
return newItemStyles(true)
|
|
}
|
|
|
|
// DefaultItem describes an item 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 {
|
|
const defaultHeight = 2
|
|
const defaultSpacing = 1
|
|
return DefaultDelegate{
|
|
ShowDescription: true,
|
|
Styles: NewDefaultItemStyles(),
|
|
height: defaultHeight,
|
|
spacing: defaultSpacing,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|