mirror of
https://github.com/Maks1mS/bubbles.git
synced 2024-12-24 14:44:38 +03:00
cd2593cfb7
* feat: allow to set the height of the item The user might want to show more than 2 lines, and, right now, if they try to do so, things break. Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com> * fix: short-circuit if width <= 0 Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com> * fix: height check Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
228 lines
6.1 KiB
Go
228 lines
6.1 KiB
Go
package list
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/key"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/muesli/reflow/truncate"
|
|
)
|
|
|
|
// 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
|
|
|
|
// Charcters 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() (s DefaultItemStyles) {
|
|
s.NormalTitle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"}).
|
|
Padding(0, 0, 0, 2)
|
|
|
|
s.NormalDesc = s.NormalTitle.Copy().
|
|
Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"})
|
|
|
|
s.SelectedTitle = lipgloss.NewStyle().
|
|
Border(lipgloss.NormalBorder(), false, false, false, true).
|
|
BorderForeground(lipgloss.AdaptiveColor{Light: "#F793FF", Dark: "#AD58B4"}).
|
|
Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}).
|
|
Padding(0, 0, 0, 1)
|
|
|
|
s.SelectedDesc = s.SelectedTitle.Copy().
|
|
Foreground(lipgloss.AdaptiveColor{Light: "#F793FF", Dark: "#AD58B4"})
|
|
|
|
s.DimmedTitle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}).
|
|
Padding(0, 0, 0, 2)
|
|
|
|
s.DimmedDesc = s.DimmedTitle.Copy().
|
|
Foreground(lipgloss.AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"})
|
|
|
|
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 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(),
|
|
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 set 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 := uint(m.width - s.NormalTitle.GetPaddingLeft() - s.NormalTitle.GetPaddingRight())
|
|
title = truncate.StringWithTail(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, truncate.StringWithTail(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.Copy().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.Copy().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)
|
|
return
|
|
}
|
|
fmt.Fprintf(w, "%s", title)
|
|
}
|
|
|
|
// 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
|
|
}
|