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}