1package selection
2
3import (
4 "fmt"
5 "log"
6 "sort"
7
8 "github.com/charmbracelet/bubbles/key"
9 "github.com/charmbracelet/bubbles/list"
10 tea "github.com/charmbracelet/bubbletea"
11 "github.com/charmbracelet/lipgloss"
12 "github.com/charmbracelet/soft-serve/ui/common"
13 "github.com/charmbracelet/soft-serve/ui/components/code"
14 "github.com/charmbracelet/soft-serve/ui/components/selector"
15 "github.com/charmbracelet/soft-serve/ui/components/tabs"
16)
17
18type pane int
19
20const (
21 selectorPane pane = iota
22 readmePane
23 lastPane
24)
25
26func (p pane) String() string {
27 return []string{
28 "Repositories",
29 "About",
30 }[p]
31}
32
33// Selection is the model for the selection screen/page.
34type Selection struct {
35 common common.Common
36 readme *code.Code
37 readmeHeight int
38 selector *selector.Selector
39 activePane pane
40 tabs *tabs.Tabs
41}
42
43// New creates a new selection model.
44func New(c common.Common) *Selection {
45 ts := make([]string, lastPane)
46 for i, b := range []pane{selectorPane, readmePane} {
47 ts[i] = b.String()
48 }
49 t := tabs.New(c, ts)
50 t.TabSeparator = lipgloss.NewStyle()
51 t.TabInactive = c.Styles.TopLevelNormalTab.Copy()
52 t.TabActive = c.Styles.TopLevelActiveTab.Copy()
53 t.TabDot = c.Styles.TopLevelActiveTabDot.Copy()
54 t.UseDot = true
55 sel := &Selection{
56 common: c,
57 activePane: selectorPane, // start with the selector focused
58 tabs: t,
59 }
60 readme := code.New(c, "", "")
61 readme.NoContentStyle = c.Styles.NoContent.Copy().SetString("No readme found.")
62 selector := selector.New(c,
63 []selector.IdentifiableItem{},
64 ItemDelegate{&c, &sel.activePane})
65 selector.SetShowTitle(false)
66 selector.SetShowHelp(false)
67 selector.SetShowStatusBar(false)
68 selector.DisableQuitKeybindings()
69 sel.selector = selector
70 sel.readme = readme
71 return sel
72}
73
74func (s *Selection) getMargins() (wm, hm int) {
75 wm = 0
76 hm = s.common.Styles.Tabs.GetVerticalFrameSize() +
77 s.common.Styles.Tabs.GetHeight()
78 if s.activePane == selectorPane && s.IsFiltering() {
79 // hide tabs when filtering
80 hm = 0
81 }
82 return
83}
84
85// FilterState returns the current filter state.
86func (s *Selection) FilterState() list.FilterState {
87 return s.selector.FilterState()
88}
89
90// SetSize implements common.Component.
91func (s *Selection) SetSize(width, height int) {
92 s.common.SetSize(width, height)
93 wm, hm := s.getMargins()
94 s.tabs.SetSize(width, height-hm)
95 s.selector.SetSize(width-wm, height-hm)
96 s.readme.SetSize(width-wm, height-hm-1) // -1 for readme status line
97}
98
99// IsFiltering returns true if the selector is currently filtering.
100func (s *Selection) IsFiltering() bool {
101 return s.FilterState() == list.Filtering
102}
103
104// ShortHelp implements help.KeyMap.
105func (s *Selection) ShortHelp() []key.Binding {
106 k := s.selector.KeyMap
107 kb := make([]key.Binding, 0)
108 kb = append(kb,
109 s.common.KeyMap.UpDown,
110 s.common.KeyMap.Section,
111 )
112 if s.activePane == selectorPane {
113 copyKey := s.common.KeyMap.Copy
114 copyKey.SetHelp("c", "copy command")
115 kb = append(kb,
116 s.common.KeyMap.Select,
117 k.Filter,
118 k.ClearFilter,
119 copyKey,
120 )
121 }
122 return kb
123}
124
125// FullHelp implements help.KeyMap.
126func (s *Selection) FullHelp() [][]key.Binding {
127 b := [][]key.Binding{
128 {
129 s.common.KeyMap.Section,
130 },
131 }
132 switch s.activePane {
133 case readmePane:
134 k := s.readme.KeyMap
135 b = append(b, []key.Binding{
136 k.PageDown,
137 k.PageUp,
138 })
139 b = append(b, []key.Binding{
140 k.HalfPageDown,
141 k.HalfPageUp,
142 })
143 b = append(b, []key.Binding{
144 k.Down,
145 k.Up,
146 })
147 case selectorPane:
148 copyKey := s.common.KeyMap.Copy
149 copyKey.SetHelp("c", "copy command")
150 k := s.selector.KeyMap
151 if !s.IsFiltering() {
152 b[0] = append(b[0],
153 s.common.KeyMap.Select,
154 copyKey,
155 )
156 }
157 b = append(b, []key.Binding{
158 k.CursorUp,
159 k.CursorDown,
160 })
161 b = append(b, []key.Binding{
162 k.NextPage,
163 k.PrevPage,
164 k.GoToStart,
165 k.GoToEnd,
166 })
167 b = append(b, []key.Binding{
168 k.Filter,
169 k.ClearFilter,
170 k.CancelWhileFiltering,
171 k.AcceptWhileFiltering,
172 })
173 }
174 return b
175}
176
177// Init implements tea.Model.
178func (s *Selection) Init() tea.Cmd {
179 var readmeCmd tea.Cmd
180 cfg := s.common.Config()
181 pk := s.common.PublicKey()
182 if cfg == nil || pk == nil {
183 return nil
184 }
185 repos, err := cfg.Backend.Repositories()
186 if err != nil {
187 return common.ErrorCmd(err)
188 }
189 sortedItems := make(Items, 0)
190 // Put configured repos first
191 for _, r := range repos {
192 item, err := NewItem(r, cfg)
193 if err != nil {
194 log.Printf("ui: failed to create item for %s: %v", r.Name(), err)
195 continue
196 }
197 sortedItems = append(sortedItems, item)
198 }
199 sort.Sort(sortedItems)
200 items := make([]selector.IdentifiableItem, len(sortedItems))
201 for i, it := range sortedItems {
202 items[i] = it
203 }
204 return tea.Batch(
205 s.selector.Init(),
206 s.selector.SetItems(items),
207 readmeCmd,
208 )
209}
210
211// Update implements tea.Model.
212func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
213 cmds := make([]tea.Cmd, 0)
214 switch msg := msg.(type) {
215 case tea.WindowSizeMsg:
216 r, cmd := s.readme.Update(msg)
217 s.readme = r.(*code.Code)
218 if cmd != nil {
219 cmds = append(cmds, cmd)
220 }
221 m, cmd := s.selector.Update(msg)
222 s.selector = m.(*selector.Selector)
223 if cmd != nil {
224 cmds = append(cmds, cmd)
225 }
226 case tea.KeyMsg, tea.MouseMsg:
227 switch msg := msg.(type) {
228 case tea.KeyMsg:
229 switch {
230 case key.Matches(msg, s.common.KeyMap.Back):
231 cmds = append(cmds, s.selector.Init())
232 }
233 }
234 t, cmd := s.tabs.Update(msg)
235 s.tabs = t.(*tabs.Tabs)
236 if cmd != nil {
237 cmds = append(cmds, cmd)
238 }
239 case tabs.ActiveTabMsg:
240 s.activePane = pane(msg)
241 }
242 switch s.activePane {
243 case readmePane:
244 r, cmd := s.readme.Update(msg)
245 s.readme = r.(*code.Code)
246 if cmd != nil {
247 cmds = append(cmds, cmd)
248 }
249 case selectorPane:
250 m, cmd := s.selector.Update(msg)
251 s.selector = m.(*selector.Selector)
252 if cmd != nil {
253 cmds = append(cmds, cmd)
254 }
255 }
256 return s, tea.Batch(cmds...)
257}
258
259// View implements tea.Model.
260func (s *Selection) View() string {
261 var view string
262 wm, hm := s.getMargins()
263 switch s.activePane {
264 case selectorPane:
265 ss := lipgloss.NewStyle().
266 Width(s.common.Width - wm).
267 Height(s.common.Height - hm)
268 view = ss.Render(s.selector.View())
269 case readmePane:
270 rs := lipgloss.NewStyle().
271 Height(s.common.Height - hm)
272 status := fmt.Sprintf("☰ %.f%%", s.readme.ScrollPercent()*100)
273 readmeStatus := lipgloss.NewStyle().
274 Align(lipgloss.Right).
275 Width(s.common.Width - wm).
276 Foreground(s.common.Styles.InactiveBorderColor).
277 Render(status)
278 view = rs.Render(lipgloss.JoinVertical(lipgloss.Left,
279 s.readme.View(),
280 readmeStatus,
281 ))
282 }
283 if s.activePane != selectorPane || s.FilterState() != list.Filtering {
284 tabs := s.common.Styles.Tabs.Render(s.tabs.View())
285 view = lipgloss.JoinVertical(lipgloss.Left,
286 tabs,
287 view,
288 )
289 }
290 return lipgloss.JoinVertical(
291 lipgloss.Left,
292 view,
293 )
294}