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/soft-serve/server/backend"
12 "github.com/charmbracelet/soft-serve/server/ui/common"
13 "github.com/charmbracelet/soft-serve/server/ui/components/code"
14 "github.com/charmbracelet/soft-serve/server/ui/components/selector"
15 "github.com/charmbracelet/soft-serve/server/ui/components/tabs"
16)
17
18const (
19 defaultNoContent = "No readme found.\n\nCreate a `.soft-serve` repository and add a `README.md` file to display readme."
20)
21
22type pane int
23
24const (
25 selectorPane pane = iota
26 readmePane
27 lastPane
28)
29
30func (p pane) String() string {
31 return []string{
32 "Repositories",
33 "About",
34 }[p]
35}
36
37// Selection is the model for the selection screen/page.
38type Selection struct {
39 common common.Common
40 readme *code.Code
41 readmeHeight int
42 selector *selector.Selector
43 activePane pane
44 tabs *tabs.Tabs
45}
46
47// New creates a new selection model.
48func New(c common.Common) *Selection {
49 ts := make([]string, lastPane)
50 for i, b := range []pane{selectorPane, readmePane} {
51 ts[i] = b.String()
52 }
53 t := tabs.New(c, ts)
54 t.TabSeparator = lipgloss.NewStyle()
55 t.TabInactive = c.Styles.TopLevelNormalTab.Copy()
56 t.TabActive = c.Styles.TopLevelActiveTab.Copy()
57 t.TabDot = c.Styles.TopLevelActiveTabDot.Copy()
58 t.UseDot = true
59 sel := &Selection{
60 common: c,
61 activePane: selectorPane, // start with the selector focused
62 tabs: t,
63 }
64 readme := code.New(c, "", "")
65 readme.NoContentStyle = c.Styles.NoContent.Copy().
66 SetString(defaultNoContent)
67 selector := selector.New(c,
68 []selector.IdentifiableItem{},
69 NewItemDelegate(&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 if r.Name() == ".soft-serve" {
202 readme, path, err := backend.Readme(r)
203 if err != nil {
204 continue
205 }
206
207 readmeCmd = s.readme.SetContent(readme, path)
208 }
209
210 if r.IsHidden() {
211 continue
212 }
213 al := cfg.Backend.AccessLevelByPublicKey(r.Name(), pk)
214 if al >= backend.ReadOnlyAccess {
215 item, err := NewItem(r, cfg)
216 if err != nil {
217 s.common.Logger.Debugf("ui: failed to create item for %s: %v", r.Name(), err)
218 continue
219 }
220 sortedItems = append(sortedItems, item)
221 }
222 }
223 sort.Sort(sortedItems)
224 items := make([]selector.IdentifiableItem, len(sortedItems))
225 for i, it := range sortedItems {
226 items[i] = it
227 }
228 return tea.Batch(
229 s.selector.Init(),
230 s.selector.SetItems(items),
231 readmeCmd,
232 )
233}
234
235// Update implements tea.Model.
236func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
237 cmds := make([]tea.Cmd, 0)
238 switch msg := msg.(type) {
239 case tea.WindowSizeMsg:
240 r, cmd := s.readme.Update(msg)
241 s.readme = r.(*code.Code)
242 if cmd != nil {
243 cmds = append(cmds, cmd)
244 }
245 m, cmd := s.selector.Update(msg)
246 s.selector = m.(*selector.Selector)
247 if cmd != nil {
248 cmds = append(cmds, cmd)
249 }
250 case tea.KeyMsg, tea.MouseMsg:
251 switch msg := msg.(type) {
252 case tea.KeyMsg:
253 switch {
254 case key.Matches(msg, s.common.KeyMap.Back):
255 cmds = append(cmds, s.selector.Init())
256 }
257 }
258 t, cmd := s.tabs.Update(msg)
259 s.tabs = t.(*tabs.Tabs)
260 if cmd != nil {
261 cmds = append(cmds, cmd)
262 }
263 case tabs.ActiveTabMsg:
264 s.activePane = pane(msg)
265 }
266 switch s.activePane {
267 case readmePane:
268 r, cmd := s.readme.Update(msg)
269 s.readme = r.(*code.Code)
270 if cmd != nil {
271 cmds = append(cmds, cmd)
272 }
273 case selectorPane:
274 m, cmd := s.selector.Update(msg)
275 s.selector = m.(*selector.Selector)
276 if cmd != nil {
277 cmds = append(cmds, cmd)
278 }
279 }
280 return s, tea.Batch(cmds...)
281}
282
283// View implements tea.Model.
284func (s *Selection) View() string {
285 var view string
286 wm, hm := s.getMargins()
287 switch s.activePane {
288 case selectorPane:
289 ss := lipgloss.NewStyle().
290 Width(s.common.Width - wm).
291 Height(s.common.Height - hm)
292 view = ss.Render(s.selector.View())
293 case readmePane:
294 rs := lipgloss.NewStyle().
295 Height(s.common.Height - hm)
296 status := fmt.Sprintf("☰ %.f%%", s.readme.ScrollPercent()*100)
297 readmeStatus := lipgloss.NewStyle().
298 Align(lipgloss.Right).
299 Width(s.common.Width - wm).
300 Foreground(s.common.Styles.InactiveBorderColor).
301 Render(status)
302 view = rs.Render(lipgloss.JoinVertical(lipgloss.Left,
303 s.readme.View(),
304 readmeStatus,
305 ))
306 }
307 if s.activePane != selectorPane || s.FilterState() != list.Filtering {
308 tabs := s.common.Styles.Tabs.Render(s.tabs.View())
309 view = lipgloss.JoinVertical(lipgloss.Left,
310 tabs,
311 view,
312 )
313 }
314 return lipgloss.JoinVertical(
315 lipgloss.Left,
316 view,
317 )
318}