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