item.go

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