1package selection
2
3import (
4 "fmt"
5 "io"
6 "strings"
7 "time"
8
9 "github.com/charmbracelet/bubbles/list"
10 tea "github.com/charmbracelet/bubbletea"
11 "github.com/charmbracelet/lipgloss"
12 "github.com/charmbracelet/soft-serve/ui/components/yankable"
13 "github.com/charmbracelet/soft-serve/ui/git"
14 "github.com/charmbracelet/soft-serve/ui/styles"
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 url *yankable.Yankable
23}
24
25// ID implements selector.IdentifiableItem.
26func (i Item) ID() string {
27 return i.repo.Repo()
28}
29
30// Title returns the item title. Implements list.DefaultItem.
31func (i Item) Title() string { return i.repo.Name() }
32
33// Description returns the item description. Implements list.DefaultItem.
34func (i Item) Description() string { return i.repo.Description() }
35
36// FilterValue implements list.Item.
37func (i Item) FilterValue() string { return i.Title() }
38
39// URL returns the item URL view.
40func (i Item) URL() string {
41 return i.url.View()
42}
43
44// ItemDelegate is the delegate for the item.
45type ItemDelegate struct {
46 styles *styles.Styles
47 activeBox *box
48}
49
50// Width returns the item width.
51func (d ItemDelegate) Width() int {
52 width := d.styles.MenuItem.GetHorizontalFrameSize() + d.styles.MenuItem.GetWidth()
53 return width
54}
55
56// Height returns the item height. Implements list.ItemDelegate.
57func (d ItemDelegate) Height() int {
58 height := d.styles.MenuItem.GetVerticalFrameSize() + d.styles.MenuItem.GetHeight()
59 return height
60}
61
62// Spacing returns the spacing between items. Implements list.ItemDelegate.
63func (d ItemDelegate) Spacing() int { return 0 }
64
65// Update implements list.ItemDelegate.
66func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
67 cmds := make([]tea.Cmd, 0)
68 // if d.activeBox == nil || *d.activeBox != selectorBox {
69 // return nil
70 // }
71 for i, item := range m.VisibleItems() {
72 itm, ok := item.(Item)
73 if !ok {
74 continue
75 }
76 // FIXME check if X & Y are within the item box
77 switch msg := msg.(type) {
78 case tea.MouseMsg:
79 // x := msg.X
80 y := msg.Y
81 // minX := (i * d.Width())
82 // maxX := minX + d.Width()
83 minY := (i * d.Height())
84 maxY := minY + d.Height()
85 // log.Printf("i: %d, x: %d, y: %d", i, x, y)
86 if y < minY || y > maxY {
87 continue
88 }
89 }
90 y, cmd := itm.url.Update(msg)
91 itm.url = y.(*yankable.Yankable)
92 if cmd != nil {
93 cmds = append(cmds, cmd)
94 }
95 }
96 return tea.Batch(cmds...)
97}
98
99// Render implements list.ItemDelegate.
100func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
101 i := listItem.(Item)
102 s := strings.Builder{}
103 var matchedRunes []int
104
105 // Conditions
106 var (
107 isSelected = index == m.Index()
108 // emptyFilter = m.FilterState() == list.Filtering && m.FilterValue() == ""
109 isFiltered = m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied
110 )
111
112 itemStyle := d.styles.MenuItem.Copy()
113 if isSelected {
114 itemStyle = itemStyle.BorderForeground(d.styles.ActiveBorderColor)
115 if d.activeBox != nil && *d.activeBox == readmeBox {
116 // TODO make this into its own color
117 itemStyle = itemStyle.BorderForeground(lipgloss.Color("15"))
118 }
119 }
120
121 title := i.Title()
122 updatedStr := fmt.Sprintf(" Updated %s", humanize.Time(i.lastUpdate))
123 updated := d.styles.MenuLastUpdate.
124 Copy().
125 Width(m.Width() - itemStyle.GetHorizontalFrameSize() - lipgloss.Width(title)).
126 Render(updatedStr)
127 titleStyle := lipgloss.NewStyle().
128 Align(lipgloss.Left).
129 Width(m.Width() - itemStyle.GetHorizontalFrameSize() - lipgloss.Width(updated))
130
131 if isFiltered && index < len(m.VisibleItems()) {
132 // Get indices of matched characters
133 matchedRunes = m.MatchesForItem(index)
134 }
135
136 if isFiltered {
137 unmatched := lipgloss.NewStyle().Inline(true)
138 matched := unmatched.Copy().Underline(true)
139 title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
140 }
141 title = titleStyle.Render(title)
142
143 s.WriteString(lipgloss.JoinHorizontal(lipgloss.Bottom, title, updated))
144 s.WriteString("\n")
145 s.WriteString(i.Description())
146 s.WriteString("\n\n")
147 s.WriteString(i.url.View())
148 w.Write([]byte(itemStyle.Render(s.String())))
149}