1package repo
2
3import (
4 "fmt"
5 "io"
6 "strings"
7 "time"
8
9 "github.com/charmbracelet/bubbles/key"
10 "github.com/charmbracelet/bubbles/list"
11 tea "github.com/charmbracelet/bubbletea"
12 "github.com/charmbracelet/lipgloss"
13 "github.com/charmbracelet/soft-serve/git"
14 "github.com/charmbracelet/soft-serve/server/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].Commit.Author.When.After(cl[j].Commit.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.KeyMsg:
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.Reference.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 if isTag {
134 if c != nil {
135 date := c.Committer.When.Format("Jan 02")
136 if c.Committer.When.Year() != time.Now().Year() {
137 date += fmt.Sprintf(" %d", c.Committer.When.Year())
138 }
139 desc += " " + st.ItemDesc.Render(date)
140 }
141
142 t := i.Tag
143 if t != nil {
144 msgSt := st.ItemDesc.Copy().Faint(false)
145 msg := t.Message()
146 nl := strings.Index(msg, "\n")
147 if nl > 0 {
148 msg = msg[:nl]
149 }
150 msg = strings.TrimSpace(msg)
151 if msg != "" {
152 msgMargin := m.Width() -
153 horizontalFrameSize -
154 lipgloss.Width(selector) -
155 lipgloss.Width(ref) -
156 lipgloss.Width(desc) -
157 lipgloss.Width(sha) -
158 3 // 3 is for the paddings and truncation symbol
159 if msgMargin >= 0 {
160 msg = common.TruncateString(msg, msgMargin)
161 desc = " " + msgSt.Render(msg) + desc
162 }
163 }
164 }
165 } else if c != nil {
166 onMargin := m.Width() -
167 horizontalFrameSize -
168 lipgloss.Width(selector) -
169 lipgloss.Width(ref) -
170 lipgloss.Width(desc) -
171 lipgloss.Width(sha) -
172 2 // 2 is for the padding and truncation symbol
173 if onMargin >= 0 {
174 on := common.TruncateString("updated "+humanize.Time(c.Committer.When), onMargin)
175 desc += " " + st.ItemDesc.Render(on)
176 }
177 }
178
179 var hash string
180 ref = itemSt.Render(ref)
181 hashMargin := m.Width() -
182 horizontalFrameSize -
183 lipgloss.Width(selector) -
184 lipgloss.Width(ref) -
185 lipgloss.Width(desc) -
186 lipgloss.Width(sha) -
187 1 // 1 is for the left padding
188 if hashMargin >= 0 {
189 hash = strings.Repeat(" ", hashMargin) + st.ItemHash.Copy().
190 Align(lipgloss.Right).
191 PaddingLeft(1).
192 Render(sha)
193 }
194 fmt.Fprint(w,
195 d.common.Zone.Mark(
196 i.ID(),
197 st.Base.Render(
198 lipgloss.JoinHorizontal(lipgloss.Left,
199 truncate.String(selector+ref+desc+hash,
200 uint(m.Width()-horizontalFrameSize)),
201 ),
202 ),
203 ),
204 )
205}