item.go

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