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