item.go

  1package selection
  2
  3import (
  4	"fmt"
  5	"io"
  6	"strings"
  7	"time"
  8
  9	"github.com/charmbracelet/bubbles/key"
 10	"github.com/charmbracelet/bubbles/list"
 11	tea "github.com/charmbracelet/bubbletea"
 12	"github.com/charmbracelet/lipgloss"
 13	"github.com/charmbracelet/soft-serve/ui/common"
 14	"github.com/charmbracelet/soft-serve/ui/git"
 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	cmd        string
 23	copied     time.Time
 24}
 25
 26// ID implements selector.IdentifiableItem.
 27func (i Item) ID() string {
 28	return i.repo.Repo()
 29}
 30
 31// Title returns the item title. Implements list.DefaultItem.
 32func (i Item) Title() string { return i.repo.Name() }
 33
 34// Description returns the item description. Implements list.DefaultItem.
 35func (i Item) Description() string { return i.repo.Description() }
 36
 37// FilterValue implements list.Item.
 38func (i Item) FilterValue() string { return i.Title() }
 39
 40// Command returns the item Command view.
 41func (i Item) Command() string {
 42	return i.cmd
 43}
 44
 45// ItemDelegate is the delegate for the item.
 46type ItemDelegate struct {
 47	common    *common.Common
 48	activeBox *box
 49}
 50
 51// Width returns the item width.
 52func (d ItemDelegate) Width() int {
 53	width := d.common.Styles.MenuItem.GetHorizontalFrameSize() + d.common.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.common.Styles.MenuItem.GetVerticalFrameSize() + d.common.Styles.MenuItem.GetHeight()
 60	return height
 61}
 62
 63// Spacing returns the spacing between items. Implements list.ItemDelegate.
 64func (d ItemDelegate) Spacing() int { return 1 }
 65
 66// Update implements list.ItemDelegate.
 67func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
 68	idx := m.Index()
 69	item, ok := m.SelectedItem().(Item)
 70	if !ok {
 71		return nil
 72	}
 73	switch msg := msg.(type) {
 74	case tea.KeyMsg:
 75		switch {
 76		case key.Matches(msg, d.common.KeyMap.Copy):
 77			item.copied = time.Now()
 78			d.common.Copy.Copy(item.Command())
 79			return m.SetItem(idx, item)
 80		}
 81	}
 82	return nil
 83}
 84
 85// Render implements list.ItemDelegate.
 86func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
 87	styles := d.common.Styles
 88	i := listItem.(Item)
 89	s := strings.Builder{}
 90	var matchedRunes []int
 91
 92	// Conditions
 93	var (
 94		isSelected = index == m.Index()
 95		isFiltered = m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied
 96	)
 97
 98	itemStyle := styles.MenuItem.Copy()
 99	if isSelected {
100		itemStyle = itemStyle.Copy().
101			BorderStyle(lipgloss.Border{
102				Left: "┃",
103			}).
104			BorderForeground(styles.ActiveBorderColor)
105		if d.activeBox != nil && *d.activeBox == readmeBox {
106			itemStyle = itemStyle.BorderForeground(styles.InactiveBorderColor)
107		}
108	}
109
110	title := i.Title()
111	if i.repo.IsPrivate() {
112		title += " 🔒"
113	}
114	if isSelected {
115		title += " "
116	}
117	updatedStr := fmt.Sprintf(" Updated %s", humanize.Time(i.lastUpdate))
118	updatedStyle := styles.MenuLastUpdate.Copy().
119		Align(lipgloss.Right).
120		Width(m.Width() - itemStyle.GetHorizontalFrameSize() - lipgloss.Width(title))
121	if isSelected {
122		updatedStyle = updatedStyle.Bold(true)
123	}
124	updated := updatedStyle.Render(updatedStr)
125
126	if isFiltered && index < len(m.VisibleItems()) {
127		// Get indices of matched characters
128		matchedRunes = m.MatchesForItem(index)
129	}
130
131	if isFiltered {
132		unmatched := lipgloss.NewStyle().Inline(true)
133		matched := unmatched.Copy().Underline(true)
134		if isSelected {
135			unmatched = unmatched.Bold(true)
136			matched = matched.Bold(true)
137		}
138		title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
139	}
140	titleStyle := lipgloss.NewStyle()
141	if isSelected {
142		titleStyle = titleStyle.Bold(true)
143	}
144	title = titleStyle.Render(title)
145	desc := lipgloss.NewStyle().
146		Faint(true).
147		Render(i.Description())
148
149	s.WriteString(lipgloss.JoinHorizontal(lipgloss.Bottom, title, updated))
150	s.WriteString("\n")
151	s.WriteString(desc)
152	s.WriteString("\n")
153	cmdStyle := styles.RepoCommand.Copy()
154	cmd := cmdStyle.Render(i.Command())
155	if !i.copied.IsZero() && i.copied.Add(time.Second).After(time.Now()) {
156		cmd = cmdStyle.Render("Copied!")
157	}
158	s.WriteString(cmd)
159	fmt.Fprint(w, itemStyle.Render(s.String()))
160}