item.go

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