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