1package selection
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/ui/common"
14 "github.com/charmbracelet/soft-serve/ui/git"
15 "github.com/dustin/go-humanize"
16)
17
18// Item represents a single item in the selector.
19type Item struct {
20 repo git.GitRepo
21 lastUpdate time.Time
22 cmd string
23 copied time.Time
24}
25
26// ID implements selector.IdentifiableItem.
27func (i Item) ID() string {
28 return i.repo.Repo()
29}
30
31// Title returns the item title. Implements list.DefaultItem.
32func (i Item) Title() string { return i.repo.Name() }
33
34// Description returns the item description. Implements list.DefaultItem.
35func (i Item) Description() string { return i.repo.Description() }
36
37// FilterValue implements list.Item.
38func (i Item) FilterValue() string { return i.Title() }
39
40// Command returns the item Command view.
41func (i Item) Command() string {
42 return i.cmd
43}
44
45// ItemDelegate is the delegate for the item.
46type ItemDelegate struct {
47 common *common.Common
48 activeBox *box
49}
50
51// Width returns the item width.
52func (d ItemDelegate) Width() int {
53 width := d.common.Styles.MenuItem.GetHorizontalFrameSize() + d.common.Styles.MenuItem.GetWidth()
54 return width
55}
56
57// Height returns the item height. Implements list.ItemDelegate.
58func (d ItemDelegate) Height() int {
59 height := d.common.Styles.MenuItem.GetVerticalFrameSize() + d.common.Styles.MenuItem.GetHeight()
60 return height
61}
62
63// Spacing returns the spacing between items. Implements list.ItemDelegate.
64func (d ItemDelegate) Spacing() int { return 1 }
65
66// Update implements list.ItemDelegate.
67func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
68 idx := m.Index()
69 item, ok := m.SelectedItem().(Item)
70 if !ok {
71 return nil
72 }
73 switch msg := msg.(type) {
74 case tea.KeyMsg:
75 switch {
76 case key.Matches(msg, d.common.KeyMap.Copy):
77 item.copied = time.Now()
78 d.common.Copy.Copy(item.Command())
79 return m.SetItem(idx, item)
80 }
81 }
82 return nil
83}
84
85// Render implements list.ItemDelegate.
86func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
87 styles := d.common.Styles
88 i := listItem.(Item)
89 s := strings.Builder{}
90 var matchedRunes []int
91
92 // Conditions
93 var (
94 isSelected = index == m.Index()
95 isFiltered = m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied
96 )
97
98 itemStyle := styles.MenuItem.Copy()
99 if isSelected {
100 itemStyle = itemStyle.Copy().
101 BorderStyle(lipgloss.Border{
102 Left: "┃",
103 }).
104 BorderForeground(styles.ActiveBorderColor)
105 if d.activeBox != nil && *d.activeBox == readmeBox {
106 itemStyle = itemStyle.BorderForeground(styles.InactiveBorderColor)
107 }
108 }
109
110 title := i.Title()
111 if i.repo.IsPrivate() {
112 title += " 🔒"
113 }
114 if isSelected {
115 title += " "
116 }
117 updatedStr := fmt.Sprintf(" Updated %s", humanize.Time(i.lastUpdate))
118 updatedStyle := styles.MenuLastUpdate.Copy().
119 Align(lipgloss.Right).
120 Width(m.Width() - itemStyle.GetHorizontalFrameSize() - lipgloss.Width(title))
121 if isSelected {
122 updatedStyle = updatedStyle.Bold(true)
123 }
124 updated := updatedStyle.Render(updatedStr)
125
126 if isFiltered && index < len(m.VisibleItems()) {
127 // Get indices of matched characters
128 matchedRunes = m.MatchesForItem(index)
129 }
130
131 if isFiltered {
132 unmatched := lipgloss.NewStyle().Inline(true)
133 matched := unmatched.Copy().Underline(true)
134 if isSelected {
135 unmatched = unmatched.Bold(true)
136 matched = matched.Bold(true)
137 }
138 title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
139 }
140 titleStyle := lipgloss.NewStyle()
141 if isSelected {
142 titleStyle = titleStyle.Bold(true)
143 }
144 title = titleStyle.Render(title)
145 desc := lipgloss.NewStyle().
146 Faint(true).
147 Render(i.Description())
148
149 s.WriteString(lipgloss.JoinHorizontal(lipgloss.Bottom, title, updated))
150 s.WriteString("\n")
151 s.WriteString(desc)
152 s.WriteString("\n")
153 cmdStyle := styles.RepoCommand.Copy()
154 cmd := cmdStyle.Render(i.Command())
155 if !i.copied.IsZero() && i.copied.Add(time.Second).After(time.Now()) {
156 cmd = cmdStyle.Render("Copied!")
157 }
158 s.WriteString(cmd)
159 fmt.Fprint(w, itemStyle.Render(s.String()))
160}