1package selection
  2
  3import (
  4	"fmt"
  5	"io"
  6	"strings"
  7	"time"
  8
  9	"github.com/charmbracelet/bubbles/list"
 10	tea "github.com/charmbracelet/bubbletea"
 11	"github.com/charmbracelet/lipgloss"
 12	"github.com/charmbracelet/soft-serve/ui/components/yankable"
 13	"github.com/charmbracelet/soft-serve/ui/styles"
 14	"github.com/dustin/go-humanize"
 15)
 16
 17// Item represents a single item in the selector.
 18type Item struct {
 19	name       string
 20	repo       string
 21	desc       string
 22	lastUpdate time.Time
 23	url        *yankable.Yankable
 24}
 25
 26// ID implements selector.IdentifiableItem.
 27func (i Item) ID() string {
 28	return i.repo
 29}
 30
 31// Title returns the item title. Implements list.DefaultItem.
 32func (i Item) Title() string { return i.name }
 33
 34// Description returns the item description. Implements list.DefaultItem.
 35func (i Item) Description() string { return i.desc }
 36
 37// FilterValue implements list.Item.
 38func (i Item) FilterValue() string { return i.name }
 39
 40// URL returns the item URL view.
 41func (i Item) URL() string {
 42	return i.url.View()
 43}
 44
 45// ItemDelegate is the delegate for the item.
 46type ItemDelegate struct {
 47	styles    *styles.Styles
 48	activeBox *box
 49}
 50
 51// Width returns the item width.
 52func (d ItemDelegate) Width() int {
 53	width := d.styles.MenuItem.GetHorizontalFrameSize() + d.styles.MenuItem.GetWidth()
 54	return width
 55}
 56
 57// Height returns the item height. Implements list.ItemDelegate.
 58func (d ItemDelegate) Height() int {
 59	height := d.styles.MenuItem.GetVerticalFrameSize() + d.styles.MenuItem.GetHeight()
 60	return height
 61}
 62
 63// Spacing returns the spacing between items. Implements list.ItemDelegate.
 64func (d ItemDelegate) Spacing() int { return 0 }
 65
 66// Update implements list.ItemDelegate.
 67func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
 68	cmds := make([]tea.Cmd, 0)
 69	// if d.activeBox == nil || *d.activeBox != selectorBox {
 70	// 	return nil
 71	// }
 72	for i, item := range m.VisibleItems() {
 73		itm, ok := item.(Item)
 74		if !ok {
 75			continue
 76		}
 77		// FIXME check if X & Y are within the item box
 78		switch msg := msg.(type) {
 79		case tea.MouseMsg:
 80			// x := msg.X
 81			y := msg.Y
 82			// minX := (i * d.Width())
 83			// maxX := minX + d.Width()
 84			minY := (i * d.Height())
 85			maxY := minY + d.Height()
 86			// log.Printf("i: %d, x: %d, y: %d", i, x, y)
 87			if y < minY || y > maxY {
 88				continue
 89			}
 90		}
 91		y, cmd := itm.url.Update(msg)
 92		itm.url = y.(*yankable.Yankable)
 93		if cmd != nil {
 94			cmds = append(cmds, cmd)
 95		}
 96	}
 97	return tea.Batch(cmds...)
 98}
 99
100// Render implements list.ItemDelegate.
101func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
102	i := listItem.(Item)
103	s := strings.Builder{}
104	var matchedRunes []int
105
106	// Conditions
107	var (
108		isSelected = index == m.Index()
109		// emptyFilter = m.FilterState() == list.Filtering && m.FilterValue() == ""
110		isFiltered = m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied
111	)
112
113	itemStyle := d.styles.MenuItem.Copy()
114	if isSelected {
115		itemStyle = itemStyle.BorderForeground(d.styles.ActiveBorderColor)
116		if d.activeBox != nil && *d.activeBox == readmeBox {
117			// TODO make this into its own color
118			itemStyle = itemStyle.BorderForeground(lipgloss.Color("15"))
119		}
120	}
121
122	title := i.name
123	updatedStr := fmt.Sprintf(" Updated %s", humanize.Time(i.lastUpdate))
124	updated := d.styles.MenuLastUpdate.
125		Copy().
126		Width(m.Width() - itemStyle.GetHorizontalFrameSize() - lipgloss.Width(title)).
127		Render(updatedStr)
128	titleStyle := lipgloss.NewStyle().
129		Align(lipgloss.Left).
130		Width(m.Width() - itemStyle.GetHorizontalFrameSize() - lipgloss.Width(updated))
131
132	if isFiltered && index < len(m.VisibleItems()) {
133		// Get indices of matched characters
134		matchedRunes = m.MatchesForItem(index)
135	}
136
137	if isFiltered {
138		unmatched := lipgloss.NewStyle().Inline(true)
139		matched := unmatched.Copy().Underline(true)
140		title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
141	}
142	title = titleStyle.Render(title)
143
144	s.WriteString(lipgloss.JoinHorizontal(lipgloss.Bottom, title, updated))
145	s.WriteString("\n")
146	s.WriteString(i.desc)
147	s.WriteString("\n\n")
148	s.WriteString(i.url.View())
149	w.Write([]byte(itemStyle.Render(s.String())))
150}