refsitem.go

  1package repo
  2
  3import (
  4	"fmt"
  5	"io"
  6	"strings"
  7	"time"
  8
  9	"github.com/charmbracelet/bubbles/v2/key"
 10	"github.com/charmbracelet/bubbles/v2/list"
 11	tea "github.com/charmbracelet/bubbletea/v2"
 12	"github.com/charmbracelet/lipgloss/v2"
 13	"github.com/charmbracelet/soft-serve/git"
 14	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 15	"github.com/dustin/go-humanize"
 16	"github.com/muesli/reflow/truncate"
 17)
 18
 19// RefItem is a git reference item.
 20type RefItem struct {
 21	*git.Reference
 22	*git.Tag
 23	*git.Commit
 24}
 25
 26// ID implements selector.IdentifiableItem.
 27func (i RefItem) ID() string {
 28	return i.Reference.Name().String()
 29}
 30
 31// Title implements list.DefaultItem.
 32func (i RefItem) Title() string {
 33	return i.Reference.Name().Short()
 34}
 35
 36// Description implements list.DefaultItem.
 37func (i RefItem) Description() string {
 38	return ""
 39}
 40
 41// Short returns the short name of the reference.
 42func (i RefItem) Short() string {
 43	return i.Reference.Name().Short()
 44}
 45
 46// FilterValue implements list.Item.
 47func (i RefItem) FilterValue() string { return i.Short() }
 48
 49// RefItems is a list of git references.
 50type RefItems []RefItem
 51
 52// Len implements sort.Interface.
 53func (cl RefItems) Len() int { return len(cl) }
 54
 55// Swap implements sort.Interface.
 56func (cl RefItems) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
 57
 58// Less implements sort.Interface.
 59func (cl RefItems) Less(i, j int) bool {
 60	if cl[i].Commit != nil && cl[j].Commit != nil {
 61		return cl[i].Author.When.After(cl[j].Author.When)
 62	} else if cl[i].Commit != nil && cl[j].Commit == nil {
 63		return true
 64	}
 65	return false
 66}
 67
 68// RefItemDelegate is the delegate for the ref item.
 69type RefItemDelegate struct {
 70	common *common.Common
 71}
 72
 73// Height implements list.ItemDelegate.
 74func (d RefItemDelegate) Height() int { return 1 }
 75
 76// Spacing implements list.ItemDelegate.
 77func (d RefItemDelegate) Spacing() int { return 0 }
 78
 79// Update implements list.ItemDelegate.
 80func (d RefItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
 81	item, ok := m.SelectedItem().(RefItem)
 82	if !ok {
 83		return nil
 84	}
 85	switch msg := msg.(type) {
 86	case tea.KeyPressMsg:
 87		switch {
 88		case key.Matches(msg, d.common.KeyMap.Copy):
 89			return copyCmd(item.ID(), fmt.Sprintf("Reference %q copied to clipboard", item.ID()))
 90		}
 91	}
 92	return nil
 93}
 94
 95// Render implements list.ItemDelegate.
 96func (d RefItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
 97	i, ok := listItem.(RefItem)
 98	if !ok {
 99		return
100	}
101
102	isTag := i.IsTag()
103	isActive := index == m.Index()
104	s := d.common.Styles.Ref
105	st := s.Normal
106	selector := "  "
107	if isActive {
108		st = s.Active
109		selector = s.ItemSelector.String()
110	}
111
112	horizontalFrameSize := st.Base.GetHorizontalFrameSize()
113	var itemSt lipgloss.Style
114	if isTag && isActive {
115		itemSt = st.ItemTag
116	} else if isTag {
117		itemSt = st.ItemTag
118	} else if isActive {
119		itemSt = st.Item
120	} else {
121		itemSt = st.Item
122	}
123
124	var sha string
125	c := i.Commit
126	if c != nil {
127		sha = c.ID.String()[:7]
128	}
129
130	ref := i.Short()
131
132	var desc string
133	//nolint:nestif // Complex UI logic requires nested conditions
134	if isTag {
135		if c != nil {
136			date := c.Committer.When.Format("Jan 02")
137			if c.Committer.When.Year() != time.Now().Year() {
138				date += fmt.Sprintf(" %d", c.Committer.When.Year())
139			}
140			desc += " " + st.ItemDesc.Render(date)
141		}
142
143		t := i.Tag
144		if t != nil {
145			msgSt := st.ItemDesc.Faint(false)
146			msg := t.Message()
147			nl := strings.Index(msg, "\n")
148			if nl > 0 {
149				msg = msg[:nl]
150			}
151			msg = strings.TrimSpace(msg)
152			if msg != "" {
153				msgMargin := m.Width() -
154					horizontalFrameSize -
155					lipgloss.Width(selector) -
156					lipgloss.Width(ref) -
157					lipgloss.Width(desc) -
158					lipgloss.Width(sha) -
159					3 // 3 is for the paddings and truncation symbol
160				if msgMargin >= 0 {
161					msg = common.TruncateString(msg, msgMargin)
162					desc = " " + msgSt.Render(msg) + desc
163				}
164			}
165		}
166	} else if c != nil {
167		onMargin := m.Width() -
168			horizontalFrameSize -
169			lipgloss.Width(selector) -
170			lipgloss.Width(ref) -
171			lipgloss.Width(desc) -
172			lipgloss.Width(sha) -
173			2 // 2 is for the padding and truncation symbol
174		if onMargin >= 0 {
175			on := common.TruncateString("updated "+humanize.Time(c.Committer.When), onMargin)
176			desc += " " + st.ItemDesc.Render(on)
177		}
178	}
179
180	var hash string
181	ref = itemSt.Render(ref)
182	hashMargin := m.Width() -
183		horizontalFrameSize -
184		lipgloss.Width(selector) -
185		lipgloss.Width(ref) -
186		lipgloss.Width(desc) -
187		lipgloss.Width(sha) -
188		1 // 1 is for the left padding
189	if hashMargin >= 0 {
190		hash = strings.Repeat(" ", hashMargin) + st.ItemHash.
191			Align(lipgloss.Right).
192			PaddingLeft(1).
193			Render(sha)
194	}
195	fmt.Fprint(w, //nolint:errcheck
196		d.common.Zone.Mark(
197			i.ID(),
198			st.Base.Render(
199				lipgloss.JoinHorizontal(lipgloss.Top,
200					truncate.String(selector+ref+desc+hash,
201						uint(m.Width()-horizontalFrameSize)), //nolint:gosec
202				),
203			),
204		),
205	)
206}