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