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