item.go

  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/server/backend"
 15	"github.com/charmbracelet/soft-serve/server/config"
 16	"github.com/charmbracelet/soft-serve/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       backend.Repository
 52	lastUpdate *time.Time
 53	cmd        string
 54}
 55
 56// New creates a new Item.
 57func NewItem(repo backend.Repository, cfg *config.Config) (Item, error) {
 58	var lastUpdate *time.Time
 59	lu := repo.UpdatedAt()
 60	if !lu.IsZero() {
 61		lastUpdate = &lu
 62	}
 63	return Item{
 64		repo:       repo,
 65		lastUpdate: lastUpdate,
 66		cmd:        common.CloneCmd(cfg.SSH.PublicURL, repo.Name()),
 67	}, nil
 68}
 69
 70// ID implements selector.IdentifiableItem.
 71func (i Item) ID() string {
 72	return i.repo.Name()
 73}
 74
 75// Title returns the item title. Implements list.DefaultItem.
 76func (i Item) Title() string {
 77	name := i.repo.ProjectName()
 78	if name == "" {
 79		name = i.repo.Name()
 80	}
 81
 82	return name
 83}
 84
 85// Description returns the item description. Implements list.DefaultItem.
 86func (i Item) Description() string { return i.repo.Description() }
 87
 88// FilterValue implements list.Item.
 89func (i Item) FilterValue() string { return i.Title() }
 90
 91// Command returns the item Command view.
 92func (i Item) Command() string {
 93	return i.cmd
 94}
 95
 96// ItemDelegate is the delegate for the item.
 97type ItemDelegate struct {
 98	common     *common.Common
 99	activePane *pane
100	copiedIdx  int
101}
102
103// NewItemDelegate creates a new ItemDelegate.
104func NewItemDelegate(common *common.Common, activePane *pane) *ItemDelegate {
105	return &ItemDelegate{
106		common:     common,
107		activePane: activePane,
108		copiedIdx:  -1,
109	}
110}
111
112// Width returns the item width.
113func (d ItemDelegate) Width() int {
114	width := d.common.Styles.MenuItem.GetHorizontalFrameSize() + d.common.Styles.MenuItem.GetWidth()
115	return width
116}
117
118// Height returns the item height. Implements list.ItemDelegate.
119func (d *ItemDelegate) Height() int {
120	height := d.common.Styles.MenuItem.GetVerticalFrameSize() + d.common.Styles.MenuItem.GetHeight()
121	return height
122}
123
124// Spacing returns the spacing between items. Implements list.ItemDelegate.
125func (d *ItemDelegate) Spacing() int { return 1 }
126
127// Update implements list.ItemDelegate.
128func (d *ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
129	idx := m.Index()
130	item, ok := m.SelectedItem().(Item)
131	if !ok {
132		return nil
133	}
134	switch msg := msg.(type) {
135	case tea.KeyMsg:
136		switch {
137		case key.Matches(msg, d.common.KeyMap.Copy):
138			d.copiedIdx = idx
139			d.common.Copy.Copy(item.Command())
140			return m.SetItem(idx, item)
141		}
142	}
143	return nil
144}
145
146// Render implements list.ItemDelegate.
147func (d *ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
148	i := listItem.(Item)
149	s := strings.Builder{}
150	var matchedRunes []int
151
152	// Conditions
153	var (
154		isSelected = index == m.Index()
155		isFiltered = m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied
156	)
157
158	styles := d.common.Styles.RepoSelector.Normal
159	if isSelected {
160		styles = d.common.Styles.RepoSelector.Active
161	}
162
163	title := i.Title()
164	title = common.TruncateString(title, m.Width()-styles.Base.GetHorizontalFrameSize())
165	if i.repo.IsPrivate() {
166		title += " 🔒"
167	}
168	if isSelected {
169		title += " "
170	}
171	var updatedStr string
172	if i.lastUpdate != nil {
173		updatedStr = fmt.Sprintf(" Updated %s", humanize.Time(*i.lastUpdate))
174	}
175	if m.Width()-styles.Base.GetHorizontalFrameSize()-lipgloss.Width(updatedStr)-lipgloss.Width(title) <= 0 {
176		updatedStr = ""
177	}
178	updatedStyle := styles.Updated.Copy().
179		Align(lipgloss.Right).
180		Width(m.Width() - styles.Base.GetHorizontalFrameSize() - lipgloss.Width(title))
181	updated := updatedStyle.Render(updatedStr)
182
183	if isFiltered && index < len(m.VisibleItems()) {
184		// Get indices of matched characters
185		matchedRunes = m.MatchesForItem(index)
186	}
187
188	if isFiltered {
189		unmatched := styles.Title.Copy().Inline(true)
190		matched := unmatched.Copy().Underline(true)
191		title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
192	}
193	title = styles.Title.Render(title)
194	desc := i.Description()
195	desc = common.TruncateString(desc, m.Width()-styles.Base.GetHorizontalFrameSize())
196	desc = styles.Desc.Render(desc)
197
198	s.WriteString(lipgloss.JoinHorizontal(lipgloss.Bottom, title, updated))
199	s.WriteRune('\n')
200	s.WriteString(desc)
201	s.WriteRune('\n')
202	cmd := common.TruncateString(i.Command(), m.Width()-styles.Base.GetHorizontalFrameSize())
203	cmd = styles.Command.Render(cmd)
204	if d.copiedIdx == index {
205		cmd += " " + styles.Desc.Render("(copied to clipboard)")
206		d.copiedIdx = -1
207	}
208	s.WriteString(cmd)
209	fmt.Fprint(w,
210		d.common.Zone.Mark(i.ID(),
211			styles.Base.Render(s.String()),
212		),
213	)
214}