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/access"
12 "github.com/charmbracelet/soft-serve/server/backend"
13 "github.com/charmbracelet/soft-serve/server/ui/common"
14 "github.com/charmbracelet/soft-serve/server/ui/components/code"
15 "github.com/charmbracelet/soft-serve/server/ui/components/selector"
16 "github.com/charmbracelet/soft-serve/server/ui/components/tabs"
17)
18
19const (
20 defaultNoContent = "No readme found.\n\nCreate a `.soft-serve` repository and add a `README.md` file to display readme."
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 = c.Renderer.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().
67 SetString(defaultNoContent)
68 selector := selector.New(c,
69 []selector.IdentifiableItem{},
70 NewItemDelegate(&c, &sel.activePane))
71 selector.SetShowTitle(false)
72 selector.SetShowHelp(false)
73 selector.SetShowStatusBar(false)
74 selector.DisableQuitKeybindings()
75 sel.selector = selector
76 sel.readme = readme
77 return sel
78}
79
80func (s *Selection) getMargins() (wm, hm int) {
81 wm = 0
82 hm = s.common.Styles.Tabs.GetVerticalFrameSize() +
83 s.common.Styles.Tabs.GetHeight()
84 if s.activePane == selectorPane && s.IsFiltering() {
85 // hide tabs when filtering
86 hm = 0
87 }
88 return
89}
90
91// FilterState returns the current filter state.
92func (s *Selection) FilterState() list.FilterState {
93 return s.selector.FilterState()
94}
95
96// SetSize implements common.Component.
97func (s *Selection) SetSize(width, height int) {
98 s.common.SetSize(width, height)
99 wm, hm := s.getMargins()
100 s.tabs.SetSize(width, height-hm)
101 s.selector.SetSize(width-wm, height-hm)
102 s.readme.SetSize(width-wm, height-hm-1) // -1 for readme status line
103}
104
105// IsFiltering returns true if the selector is currently filtering.
106func (s *Selection) IsFiltering() bool {
107 return s.FilterState() == list.Filtering
108}
109
110// ShortHelp implements help.KeyMap.
111func (s *Selection) ShortHelp() []key.Binding {
112 k := s.selector.KeyMap
113 kb := make([]key.Binding, 0)
114 kb = append(kb,
115 s.common.KeyMap.UpDown,
116 s.common.KeyMap.Section,
117 )
118 if s.activePane == selectorPane {
119 copyKey := s.common.KeyMap.Copy
120 copyKey.SetHelp("c", "copy command")
121 kb = append(kb,
122 s.common.KeyMap.Select,
123 k.Filter,
124 k.ClearFilter,
125 copyKey,
126 )
127 }
128 return kb
129}
130
131// FullHelp implements help.KeyMap.
132func (s *Selection) FullHelp() [][]key.Binding {
133 b := [][]key.Binding{
134 {
135 s.common.KeyMap.Section,
136 },
137 }
138 switch s.activePane {
139 case readmePane:
140 k := s.readme.KeyMap
141 b = append(b, []key.Binding{
142 k.PageDown,
143 k.PageUp,
144 })
145 b = append(b, []key.Binding{
146 k.HalfPageDown,
147 k.HalfPageUp,
148 })
149 b = append(b, []key.Binding{
150 k.Down,
151 k.Up,
152 })
153 case selectorPane:
154 copyKey := s.common.KeyMap.Copy
155 copyKey.SetHelp("c", "copy command")
156 k := s.selector.KeyMap
157 if !s.IsFiltering() {
158 b[0] = append(b[0],
159 s.common.KeyMap.Select,
160 copyKey,
161 )
162 }
163 b = append(b, []key.Binding{
164 k.CursorUp,
165 k.CursorDown,
166 })
167 b = append(b, []key.Binding{
168 k.NextPage,
169 k.PrevPage,
170 k.GoToStart,
171 k.GoToEnd,
172 })
173 b = append(b, []key.Binding{
174 k.Filter,
175 k.ClearFilter,
176 k.CancelWhileFiltering,
177 k.AcceptWhileFiltering,
178 })
179 }
180 return b
181}
182
183// Init implements tea.Model.
184func (s *Selection) Init() tea.Cmd {
185 ctx := s.common.Context()
186 cfg := s.common.Config()
187 var readmeCmd tea.Cmd
188 be := s.common.Backend()
189 if be == nil {
190 return nil
191 }
192
193 pk := s.common.PublicKey()
194 if pk == nil && !be.AllowKeyless(ctx) {
195 return nil
196 }
197
198 // TODO: fixme
199 repos, _ := be.Repositories(ctx, 1, 10)
200 user := s.common.User()
201
202 sortedItems := make(Items, 0)
203 for _, r := range repos {
204 if r.Name() == ".soft-serve" {
205 readme, path, err := backend.Readme(r)
206 if err != nil {
207 continue
208 }
209
210 readmeCmd = s.readme.SetContent(readme, path)
211 }
212
213 if r.IsHidden() {
214 continue
215 }
216
217 al, _ := be.AccessLevel(ctx, r.Name(), user)
218 if al >= access.ReadOnlyAccess {
219 item, err := NewItem(cfg, r)
220 if err != nil {
221 s.common.Logger.Debugf("ui: failed to create item for %s: %v", r.Name(), err)
222 continue
223 }
224 sortedItems = append(sortedItems, item)
225 }
226 }
227 sort.Sort(sortedItems)
228 items := make([]selector.IdentifiableItem, len(sortedItems))
229 for i, it := range sortedItems {
230 items[i] = it
231 }
232 return tea.Batch(
233 s.selector.Init(),
234 s.selector.SetItems(items),
235 readmeCmd,
236 )
237}
238
239// Update implements tea.Model.
240func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
241 cmds := make([]tea.Cmd, 0)
242 switch msg := msg.(type) {
243 case tea.WindowSizeMsg:
244 r, cmd := s.readme.Update(msg)
245 s.readme = r.(*code.Code)
246 if cmd != nil {
247 cmds = append(cmds, cmd)
248 }
249 m, cmd := s.selector.Update(msg)
250 s.selector = m.(*selector.Selector)
251 if cmd != nil {
252 cmds = append(cmds, cmd)
253 }
254 case tea.KeyMsg, tea.MouseMsg:
255 switch msg := msg.(type) {
256 case tea.KeyMsg:
257 switch {
258 case key.Matches(msg, s.common.KeyMap.Back):
259 cmds = append(cmds, s.selector.Init())
260 }
261 }
262 t, cmd := s.tabs.Update(msg)
263 s.tabs = t.(*tabs.Tabs)
264 if cmd != nil {
265 cmds = append(cmds, cmd)
266 }
267 case tabs.ActiveTabMsg:
268 s.activePane = pane(msg)
269 }
270 switch s.activePane {
271 case readmePane:
272 r, cmd := s.readme.Update(msg)
273 s.readme = r.(*code.Code)
274 if cmd != nil {
275 cmds = append(cmds, cmd)
276 }
277 case selectorPane:
278 m, cmd := s.selector.Update(msg)
279 s.selector = m.(*selector.Selector)
280 if cmd != nil {
281 cmds = append(cmds, cmd)
282 }
283 }
284 return s, tea.Batch(cmds...)
285}
286
287// View implements tea.Model.
288func (s *Selection) View() string {
289 var view string
290 wm, hm := s.getMargins()
291 switch s.activePane {
292 case selectorPane:
293 ss := s.common.Renderer.NewStyle().
294 Width(s.common.Width - wm).
295 Height(s.common.Height - hm)
296 view = ss.Render(s.selector.View())
297 case readmePane:
298 rs := s.common.Renderer.NewStyle().
299 Height(s.common.Height - hm)
300 status := fmt.Sprintf("☰ %.f%%", s.readme.ScrollPercent()*100)
301 readmeStatus := s.common.Renderer.NewStyle().
302 Align(lipgloss.Right).
303 Width(s.common.Width - wm).
304 Foreground(s.common.Styles.InactiveBorderColor).
305 Render(status)
306 view = rs.Render(lipgloss.JoinVertical(lipgloss.Left,
307 s.readme.View(),
308 readmeStatus,
309 ))
310 }
311 if s.activePane != selectorPane || s.FilterState() != list.Filtering {
312 tabs := s.common.Styles.Tabs.Render(s.tabs.View())
313 view = lipgloss.JoinVertical(lipgloss.Left,
314 tabs,
315 view,
316 )
317 }
318 return lipgloss.JoinVertical(
319 lipgloss.Left,
320 view,
321 )
322}