1package selection
  2
  3import (
  4	"fmt"
  5	"io"
  6	"sort"
  7	"strings"
  8	"time"
  9
 10	"github.com/charmbracelet/bubbles/key"
 11	"github.com/charmbracelet/bubbles/list"
 12	tea "github.com/charmbracelet/bubbletea"
 13	"github.com/charmbracelet/lipgloss"
 14	"github.com/charmbracelet/soft-serve/pkg/proto"
 15	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 16	"github.com/dustin/go-humanize"
 17)
 18
 19var _ sort.Interface = Items{}
 20
 21// Items is a list of Item.
 22type Items []Item
 23
 24// Len implements sort.Interface.
 25func (it Items) Len() int {
 26	return len(it)
 27}
 28
 29// Less implements sort.Interface.
 30func (it Items) Less(i int, j int) bool {
 31	if it[i].lastUpdate == nil && it[j].lastUpdate != nil {
 32		return false
 33	}
 34	if it[i].lastUpdate != nil && it[j].lastUpdate == nil {
 35		return true
 36	}
 37	if it[i].lastUpdate == nil && it[j].lastUpdate == nil {
 38		return it[i].repo.Name() < it[j].repo.Name()
 39	}
 40	return it[i].lastUpdate.After(*it[j].lastUpdate)
 41}
 42
 43// Swap implements sort.Interface.
 44func (it Items) Swap(i int, j int) {
 45	it[i], it[j] = it[j], it[i]
 46}
 47
 48// Item represents a single item in the selector.
 49type Item struct {
 50	repo       proto.Repository
 51	lastUpdate *time.Time
 52	cmd        string
 53}
 54
 55// New creates a new Item.
 56func NewItem(c common.Common, repo proto.Repository) (Item, error) {
 57	var lastUpdate *time.Time
 58	lu := repo.UpdatedAt()
 59	if !lu.IsZero() {
 60		lastUpdate = &lu
 61	}
 62	var cmd string
 63	if cfg := c.Config(); cfg != nil {
 64		cmd = c.CloneCmd(cfg.SSH.PublicURL, repo.Name())
 65	}
 66	return Item{
 67		repo:       repo,
 68		lastUpdate: lastUpdate,
 69		cmd:        cmd,
 70	}, nil
 71}
 72
 73// ID implements selector.IdentifiableItem.
 74func (i Item) ID() string {
 75	return i.repo.Name()
 76}
 77
 78// Title returns the item title. Implements list.DefaultItem.
 79func (i Item) Title() string {
 80	name := i.repo.ProjectName()
 81	if name == "" {
 82		name = i.repo.Name()
 83	}
 84
 85	return name
 86}
 87
 88// Description returns the item description. Implements list.DefaultItem.
 89func (i Item) Description() string { return strings.TrimSpace(i.repo.Description()) }
 90
 91// FilterValue implements list.Item.
 92func (i Item) FilterValue() string { return i.Title() }
 93
 94// Command returns the item Command view.
 95func (i Item) Command() string {
 96	return i.cmd
 97}
 98
 99// ItemDelegate is the delegate for the item.
100type ItemDelegate struct {
101	common     *common.Common
102	activePane *pane
103	copiedIdx  int
104}
105
106// NewItemDelegate creates a new ItemDelegate.
107func NewItemDelegate(common *common.Common, activePane *pane) *ItemDelegate {
108	return &ItemDelegate{
109		common:     common,
110		activePane: activePane,
111		copiedIdx:  -1,
112	}
113}
114
115// Width returns the item width.
116func (d ItemDelegate) Width() int {
117	width := d.common.Styles.MenuItem.GetHorizontalFrameSize() + d.common.Styles.MenuItem.GetWidth()
118	return width
119}
120
121// Height returns the item height. Implements list.ItemDelegate.
122func (d *ItemDelegate) Height() int {
123	height := d.common.Styles.MenuItem.GetVerticalFrameSize() + d.common.Styles.MenuItem.GetHeight()
124	return height
125}
126
127// Spacing returns the spacing between items. Implements list.ItemDelegate.
128func (d *ItemDelegate) Spacing() int { return 1 }
129
130// Update implements list.ItemDelegate.
131func (d *ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
132	idx := m.Index()
133	item, ok := m.SelectedItem().(Item)
134	if !ok {
135		return nil
136	}
137	switch msg := msg.(type) {
138	case tea.KeyMsg:
139		switch {
140		case key.Matches(msg, d.common.KeyMap.Copy):
141			d.copiedIdx = idx
142			d.common.Output.Copy(item.Command())
143			return m.SetItem(idx, item)
144		}
145	}
146	return nil
147}
148
149// Render implements list.ItemDelegate.
150func (d *ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
151	i := listItem.(Item)
152	s := strings.Builder{}
153	var matchedRunes []int
154
155	// Conditions
156	var (
157		isSelected = index == m.Index()
158		isFiltered = m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied
159	)
160
161	styles := d.common.Styles.RepoSelector.Normal
162	if isSelected {
163		styles = d.common.Styles.RepoSelector.Active
164	}
165
166	title := i.Title()
167	title = common.TruncateString(title, m.Width()-styles.Base.GetHorizontalFrameSize())
168	if i.repo.IsPrivate() {
169		title += " 🔒"
170	}
171	if isSelected {
172		title += " "
173	}
174	var updatedStr string
175	if i.lastUpdate != nil {
176		updatedStr = fmt.Sprintf(" Updated %s", humanize.Time(*i.lastUpdate))
177	}
178	if m.Width()-styles.Base.GetHorizontalFrameSize()-lipgloss.Width(updatedStr)-lipgloss.Width(title) <= 0 {
179		updatedStr = ""
180	}
181	updatedStyle := styles.Updated.Copy().
182		Align(lipgloss.Right).
183		Width(m.Width() - styles.Base.GetHorizontalFrameSize() - lipgloss.Width(title))
184	updated := updatedStyle.Render(updatedStr)
185
186	if isFiltered && index < len(m.VisibleItems()) {
187		// Get indices of matched characters
188		matchedRunes = m.MatchesForItem(index)
189	}
190
191	if isFiltered {
192		unmatched := styles.Title.Copy().Inline(true)
193		matched := unmatched.Copy().Underline(true)
194		title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
195	}
196	title = styles.Title.Render(title)
197	desc := i.Description()
198	desc = common.TruncateString(desc, m.Width()-styles.Base.GetHorizontalFrameSize())
199	desc = styles.Desc.Render(desc)
200
201	s.WriteString(lipgloss.JoinHorizontal(lipgloss.Bottom, title, updated))
202	s.WriteRune('\n')
203	s.WriteString(desc)
204	s.WriteRune('\n')
205
206	cmd := i.Command()
207	cmdStyler := styles.Command.Render
208	if d.copiedIdx == index {
209		cmd = "(copied to clipboard)"
210		cmdStyler = styles.Desc.Render
211		d.copiedIdx = -1
212	}
213	cmd = common.TruncateString(cmd, m.Width()-styles.Base.GetHorizontalFrameSize())
214	s.WriteString(cmdStyler(cmd))
215	fmt.Fprint(w,
216		d.common.Zone.Mark(i.ID(),
217			styles.Base.Render(s.String()),
218		),
219	)
220}