item.go

  1package selection
  2
  3import (
  4	"fmt"
  5	"io"
  6	"sort"
  7	"strings"
  8	"time"
  9
 10	"github.com/charmbracelet/bubbles/v2/key"
 11	"github.com/charmbracelet/bubbles/v2/list"
 12	tea "github.com/charmbracelet/bubbletea/v2"
 13	lipgloss "github.com/charmbracelet/lipgloss/v2"
 14	"github.com/charmbracelet/soft-serve/pkg/proto"
 15	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 16	humanize "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.KeyPressMsg:
139		switch {
140		case key.Matches(msg, d.common.KeyMap.Copy):
141			d.copiedIdx = idx
142			return tea.Batch(
143				tea.SetClipboard(item.Command()),
144				m.SetItem(idx, item),
145			)
146		}
147	}
148	return nil
149}
150
151// Render implements list.ItemDelegate.
152func (d *ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
153	i := listItem.(Item)
154	s := strings.Builder{}
155	var matchedRunes []int
156
157	// Conditions
158	var (
159		isSelected = index == m.Index()
160		isFiltered = m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied
161	)
162
163	styles := d.common.Styles.RepoSelector.Normal
164	if isSelected {
165		styles = d.common.Styles.RepoSelector.Active
166	}
167
168	title := i.Title()
169	title = common.TruncateString(title, m.Width()-styles.Base.GetHorizontalFrameSize())
170	if i.repo.IsPrivate() {
171		title += " 🔒"
172	}
173	if isSelected {
174		title += " "
175	}
176	var updatedStr string
177	if i.lastUpdate != nil {
178		updatedStr = fmt.Sprintf(" Updated %s", humanize.Time(*i.lastUpdate))
179	}
180	if m.Width()-styles.Base.GetHorizontalFrameSize()-lipgloss.Width(updatedStr)-lipgloss.Width(title) <= 0 {
181		updatedStr = ""
182	}
183	updatedStyle := styles.Updated.
184		Align(lipgloss.Right).
185		Width(m.Width() - styles.Base.GetHorizontalFrameSize() - lipgloss.Width(title))
186	updated := updatedStyle.Render(updatedStr)
187
188	if isFiltered && index < len(m.VisibleItems()) {
189		// Get indices of matched characters
190		matchedRunes = m.MatchesForItem(index)
191	}
192
193	if isFiltered {
194		unmatched := styles.Title.Inline(true)
195		matched := unmatched.Underline(true)
196		title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
197	}
198	title = styles.Title.Render(title)
199	desc := i.Description()
200	desc = common.TruncateString(desc, m.Width()-styles.Base.GetHorizontalFrameSize())
201	desc = styles.Desc.Render(desc)
202
203	s.WriteString(lipgloss.JoinHorizontal(lipgloss.Bottom, title, updated))
204	s.WriteRune('\n')
205	s.WriteString(desc)
206	s.WriteRune('\n')
207
208	cmd := i.Command()
209	cmdStyler := styles.Command.Render
210	if d.copiedIdx == index {
211		cmd = "(copied to clipboard)"
212		cmdStyler = styles.Desc.Render
213		d.copiedIdx = -1
214	}
215	cmd = common.TruncateString(cmd, m.Width()-styles.Base.GetHorizontalFrameSize())
216	s.WriteString(cmdStyler(cmd))
217	fmt.Fprint(w,
218		d.common.Zone.Mark(i.ID(),
219			styles.Base.Render(s.String()),
220		),
221	)
222}