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 title = common.TruncateString(title, m.Width()-itemStyle.GetHorizontalFrameSize())
112 if i.repo.IsPrivate() {
113 title += " 🔒"
114 }
115 if isSelected {
116 title += " "
117 }
118 updatedStr := fmt.Sprintf(" Updated %s", humanize.Time(i.lastUpdate))
119 if m.Width()-itemStyle.GetHorizontalFrameSize()-lipgloss.Width(updatedStr)-lipgloss.Width(title) <= 0 {
120 updatedStr = ""
121 }
122 updatedStyle := styles.MenuLastUpdate.Copy().
123 Align(lipgloss.Right).
124 Width(m.Width() - itemStyle.GetHorizontalFrameSize() - lipgloss.Width(title))
125 if isSelected {
126 updatedStyle = updatedStyle.Bold(true)
127 }
128 updated := updatedStyle.Render(updatedStr)
129
130 if isFiltered && index < len(m.VisibleItems()) {
131 // Get indices of matched characters
132 matchedRunes = m.MatchesForItem(index)
133 }
134
135 if isFiltered {
136 unmatched := lipgloss.NewStyle().Inline(true)
137 matched := unmatched.Copy().Underline(true)
138 if isSelected {
139 unmatched = unmatched.Bold(true)
140 matched = matched.Bold(true)
141 }
142 title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
143 }
144 titleStyle := lipgloss.NewStyle()
145 if isSelected {
146 titleStyle = titleStyle.Bold(true)
147 }
148 title = titleStyle.Render(title)
149 desc := i.Description()
150 descStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
151 if desc == "" {
152 desc = "No description"
153 descStyle = descStyle.Faint(true)
154 }
155 desc = common.TruncateString(desc, m.Width()-itemStyle.GetHorizontalFrameSize())
156 desc = descStyle.Render(desc)
157
158 s.WriteString(lipgloss.JoinHorizontal(lipgloss.Bottom, title, updated))
159 s.WriteString("\n")
160 s.WriteString(desc)
161 s.WriteString("\n")
162 cmdStyle := styles.Repo.Command.Copy()
163 cmd := common.TruncateString(i.Command(), m.Width()-itemStyle.GetHorizontalFrameSize())
164 cmd = cmdStyle.Render(cmd)
165 if !i.copied.IsZero() && i.copied.Add(time.Second).After(time.Now()) {
166 cmd = cmdStyle.Render("Copied!")
167 }
168 s.WriteString(cmd)
169 fmt.Fprint(w, itemStyle.Render(s.String()))
170}