1package selection
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/charmbracelet/bubbles/key"
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/charmbracelet/lipgloss"
10 "github.com/charmbracelet/soft-serve/config"
11 "github.com/charmbracelet/soft-serve/ui/common"
12 "github.com/charmbracelet/soft-serve/ui/components/code"
13 "github.com/charmbracelet/soft-serve/ui/components/selector"
14 "github.com/charmbracelet/soft-serve/ui/components/tabs"
15 "github.com/charmbracelet/soft-serve/ui/git"
16 wgit "github.com/charmbracelet/wish/git"
17 "github.com/gliderlabs/ssh"
18)
19
20type box int
21
22const (
23 selectorBox box = iota
24 readmeBox
25 lastBox
26)
27
28func (b box) String() string {
29 return []string{
30 "Repositories",
31 "About",
32 }[b]
33}
34
35// Selection is the model for the selection screen/page.
36type Selection struct {
37 cfg *config.Config
38 pk ssh.PublicKey
39 common common.Common
40 readme *code.Code
41 readmeHeight int
42 selector *selector.Selector
43 activeBox box
44 tabs *tabs.Tabs
45}
46
47// New creates a new selection model.
48func New(cfg *config.Config, pk ssh.PublicKey, common common.Common) *Selection {
49 ts := make([]string, lastBox)
50 for i, b := range []box{selectorBox, readmeBox} {
51 ts[i] = b.String()
52 }
53 t := tabs.New(common, ts)
54 t.TabSeparator = lipgloss.NewStyle()
55 t.TabInactive = lipgloss.NewStyle().
56 Bold(true).
57 UnsetBackground().
58 Foreground(common.Styles.InactiveBorderColor).
59 Padding(0, 1)
60 t.TabActive = t.TabInactive.Copy().
61 Background(lipgloss.Color("62")).
62 Foreground(lipgloss.Color("230"))
63 sel := &Selection{
64 cfg: cfg,
65 pk: pk,
66 common: common,
67 activeBox: selectorBox, // start with the selector focused
68 tabs: t,
69 }
70 readme := code.New(common, "", "")
71 readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.")
72 selector := selector.New(common,
73 []selector.IdentifiableItem{},
74 ItemDelegate{&common, &sel.activeBox})
75 selector.SetShowTitle(false)
76 selector.SetShowHelp(false)
77 selector.SetShowStatusBar(false)
78 selector.DisableQuitKeybindings()
79 sel.selector = selector
80 sel.readme = readme
81 return sel
82}
83
84func (s *Selection) getMargins() (wm, hm int) {
85 wm = 0
86 hm = s.common.Styles.Tabs.GetVerticalFrameSize() +
87 s.common.Styles.Tabs.GetHeight() +
88 2 // tabs margin see View()
89 switch s.activeBox {
90 case selectorBox:
91 hm += s.common.Styles.SelectorBox.GetVerticalFrameSize() +
92 s.common.Styles.SelectorBox.GetHeight()
93 case readmeBox:
94 hm += s.common.Styles.ReadmeBox.GetVerticalFrameSize() +
95 s.common.Styles.ReadmeBox.GetHeight() +
96 1 // readme statusbar
97 }
98 return
99}
100
101// SetSize implements common.Component.
102func (s *Selection) SetSize(width, height int) {
103 s.common.SetSize(width, height)
104 wm, hm := s.getMargins()
105 s.tabs.SetSize(width, height-hm)
106 s.selector.SetSize(width-wm, height-hm)
107 s.readme.SetSize(width-wm, height-hm)
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.activeBox == selectorBox {
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 switch s.activeBox {
134 case readmeBox:
135 k := s.readme.KeyMap
136 return [][]key.Binding{
137 {
138 k.PageDown,
139 k.PageUp,
140 },
141 {
142 k.HalfPageDown,
143 k.HalfPageUp,
144 },
145 {
146 k.Down,
147 k.Up,
148 },
149 }
150 case selectorBox:
151 copyKey := s.common.KeyMap.Copy
152 copyKey.SetHelp("c", "copy command")
153 k := s.selector.KeyMap
154 return [][]key.Binding{
155 {
156 s.common.KeyMap.Select,
157 copyKey,
158 k.CursorUp,
159 k.CursorDown,
160 },
161 {
162 k.NextPage,
163 k.PrevPage,
164 k.GoToStart,
165 k.GoToEnd,
166 },
167 {
168 k.Filter,
169 k.ClearFilter,
170 k.CancelWhileFiltering,
171 k.AcceptWhileFiltering,
172 },
173 }
174 }
175 return [][]key.Binding{}
176}
177
178// Init implements tea.Model.
179func (s *Selection) Init() tea.Cmd {
180 var readmeCmd tea.Cmd
181 items := make([]selector.IdentifiableItem, 0)
182 cfg := s.cfg
183 pk := s.pk
184 // Put configured repos first
185 for _, r := range cfg.Repos {
186 if r.Private && cfg.AuthRepo(r.Repo, pk) < wgit.AdminAccess {
187 continue
188 }
189 repo, err := cfg.Source.GetRepo(r.Repo)
190 if err != nil {
191 continue
192 }
193 items = append(items, Item{
194 repo: repo,
195 cmd: git.RepoURL(cfg.Host, cfg.Port, r.Repo),
196 })
197 }
198 for _, r := range cfg.Source.AllRepos() {
199 if r.Repo() == "config" {
200 rm, rp := r.Readme()
201 s.readmeHeight = strings.Count(rm, "\n")
202 readmeCmd = s.readme.SetContent(rm, rp)
203 }
204 if r.IsPrivate() && cfg.AuthRepo(r.Repo(), pk) < wgit.AdminAccess {
205 continue
206 }
207 exists := false
208 head, err := r.HEAD()
209 if err != nil {
210 return common.ErrorCmd(err)
211 }
212 lc, err := r.CommitsByPage(head, 1, 1)
213 if err != nil {
214 return common.ErrorCmd(err)
215 }
216 lastUpdate := lc[0].Committer.When
217 if lastUpdate.IsZero() {
218 lastUpdate = lc[0].Author.When
219 }
220 for i, item := range items {
221 item := item.(Item)
222 if item.repo.Repo() == r.Repo() {
223 exists = true
224 item.lastUpdate = lastUpdate
225 items[i] = item
226 break
227 }
228 }
229 if !exists {
230 items = append(items, Item{
231 repo: r,
232 lastUpdate: lastUpdate,
233 cmd: git.RepoURL(cfg.Host, cfg.Port, r.Name()),
234 })
235 }
236 }
237 return tea.Batch(
238 s.selector.Init(),
239 s.selector.SetItems(items),
240 readmeCmd,
241 )
242}
243
244// Update implements tea.Model.
245func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
246 cmds := make([]tea.Cmd, 0)
247 switch msg := msg.(type) {
248 case tea.WindowSizeMsg:
249 r, cmd := s.readme.Update(msg)
250 s.readme = r.(*code.Code)
251 if cmd != nil {
252 cmds = append(cmds, cmd)
253 }
254 m, cmd := s.selector.Update(msg)
255 s.selector = m.(*selector.Selector)
256 if cmd != nil {
257 cmds = append(cmds, cmd)
258 }
259 case tea.KeyMsg, tea.MouseMsg:
260 switch msg := msg.(type) {
261 case tea.KeyMsg:
262 switch {
263 case key.Matches(msg, s.common.KeyMap.Back):
264 cmds = append(cmds, s.selector.Init())
265 }
266 }
267 t, cmd := s.tabs.Update(msg)
268 s.tabs = t.(*tabs.Tabs)
269 if cmd != nil {
270 cmds = append(cmds, cmd)
271 }
272 case tabs.ActiveTabMsg:
273 s.activeBox = box(msg)
274 }
275 switch s.activeBox {
276 case readmeBox:
277 r, cmd := s.readme.Update(msg)
278 s.readme = r.(*code.Code)
279 if cmd != nil {
280 cmds = append(cmds, cmd)
281 }
282 case selectorBox:
283 m, cmd := s.selector.Update(msg)
284 s.selector = m.(*selector.Selector)
285 if cmd != nil {
286 cmds = append(cmds, cmd)
287 }
288 }
289 return s, tea.Batch(cmds...)
290}
291
292// View implements tea.Model.
293func (s *Selection) View() string {
294 var view string
295 wm, hm := s.getMargins()
296 hm++ // tabs margin
297 switch s.activeBox {
298 case selectorBox:
299 ss := s.common.Styles.SelectorBox.Copy().
300 Height(s.common.Height - hm)
301 view = ss.Render(s.selector.View())
302 case readmeBox:
303 rs := s.common.Styles.ReadmeBox.Copy().
304 Height(s.common.Height - hm)
305 status := fmt.Sprintf("☰ %.f%%", s.readme.ScrollPercent()*100)
306 readmeStatus := lipgloss.NewStyle().
307 Align(lipgloss.Right).
308 Width(s.common.Width - wm).
309 Foreground(s.common.Styles.InactiveBorderColor).
310 Render(status)
311 view = rs.Render(lipgloss.JoinVertical(lipgloss.Top,
312 s.readme.View(),
313 readmeStatus,
314 ))
315 }
316 ts := s.common.Styles.Tabs.Copy().
317 MarginBottom(1)
318 return lipgloss.JoinVertical(lipgloss.Top,
319 ts.Render(s.tabs.View()),
320 view,
321 )
322}