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 activePane *pane
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 i := listItem.(Item)
88 s := strings.Builder{}
89 var matchedRunes []int
90
91 // Conditions
92 var (
93 isSelected = index == m.Index()
94 isFiltered = m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied
95 )
96
97 styles := d.common.Styles.RepoSelector.Normal
98 if isSelected {
99 styles = d.common.Styles.RepoSelector.Active
100 }
101
102 title := i.Title()
103 title = common.TruncateString(title, m.Width()-styles.Base.GetHorizontalFrameSize())
104 if i.repo.IsPrivate() {
105 title += " 🔒"
106 }
107 if isSelected {
108 title += " "
109 }
110 updatedStr := fmt.Sprintf(" Updated %s", humanize.Time(i.lastUpdate))
111 if m.Width()-styles.Base.GetHorizontalFrameSize()-lipgloss.Width(updatedStr)-lipgloss.Width(title) <= 0 {
112 updatedStr = ""
113 }
114 updatedStyle := styles.Updated.Copy().
115 Align(lipgloss.Right).
116 Width(m.Width() - styles.Base.GetHorizontalFrameSize() - lipgloss.Width(title))
117 updated := updatedStyle.Render(updatedStr)
118
119 if isFiltered && index < len(m.VisibleItems()) {
120 // Get indices of matched characters
121 matchedRunes = m.MatchesForItem(index)
122 }
123
124 if isFiltered {
125 unmatched := lipgloss.NewStyle().Inline(true)
126 matched := unmatched.Copy().Underline(true)
127 if isSelected {
128 unmatched = unmatched.Bold(true)
129 matched = matched.Bold(true)
130 }
131 title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
132 }
133 title = styles.Title.Render(title)
134 desc := i.Description()
135 desc = common.TruncateString(desc, m.Width()-styles.Base.GetHorizontalFrameSize())
136 desc = styles.Desc.Render(desc)
137
138 s.WriteString(lipgloss.JoinHorizontal(lipgloss.Bottom, title, updated))
139 s.WriteRune('\n')
140 s.WriteString(desc)
141 s.WriteRune('\n')
142 cmd := common.TruncateString(i.Command(), m.Width()-styles.Base.GetHorizontalFrameSize())
143 cmd = styles.Command.Render(cmd)
144 if !i.copied.IsZero() && i.copied.Add(time.Second).After(time.Now()) {
145 cmd = styles.Command.Render("Copied!")
146 }
147 s.WriteString(cmd)
148 fmt.Fprint(w, styles.Base.Render(s.String()))
149}