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 acc := cfg.AuthRepo(r.Repo, pk)
187 if r.Private && acc < wgit.ReadOnlyAccess {
188 continue
189 }
190 repo, err := cfg.Source.GetRepo(r.Repo)
191 if err != nil {
192 continue
193 }
194 items = append(items, Item{
195 repo: repo,
196 cmd: git.RepoURL(cfg.Host, cfg.Port, r.Repo),
197 })
198 }
199 for _, r := range cfg.Source.AllRepos() {
200 if r.Repo() == "config" {
201 rm, rp := r.Readme()
202 s.readmeHeight = strings.Count(rm, "\n")
203 readmeCmd = s.readme.SetContent(rm, rp)
204 }
205 acc := cfg.AuthRepo(r.Repo(), pk)
206 if r.IsPrivate() && acc < wgit.ReadOnlyAccess {
207 continue
208 }
209 exists := false
210 lc, err := r.Commit("HEAD")
211 if err != nil {
212 return common.ErrorCmd(err)
213 }
214 lastUpdate := lc.Committer.When
215 if lastUpdate.IsZero() {
216 lastUpdate = lc.Author.When
217 }
218 for i, item := range items {
219 item := item.(Item)
220 if item.repo.Repo() == r.Repo() {
221 exists = true
222 item.lastUpdate = lastUpdate
223 items[i] = item
224 break
225 }
226 }
227 if !exists {
228 items = append(items, Item{
229 repo: r,
230 lastUpdate: lastUpdate,
231 cmd: git.RepoURL(cfg.Host, cfg.Port, r.Name()),
232 })
233 }
234 }
235 return tea.Batch(
236 s.selector.Init(),
237 s.selector.SetItems(items),
238 readmeCmd,
239 )
240}
241
242// Update implements tea.Model.
243func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
244 cmds := make([]tea.Cmd, 0)
245 switch msg := msg.(type) {
246 case tea.WindowSizeMsg:
247 r, cmd := s.readme.Update(msg)
248 s.readme = r.(*code.Code)
249 if cmd != nil {
250 cmds = append(cmds, cmd)
251 }
252 m, cmd := s.selector.Update(msg)
253 s.selector = m.(*selector.Selector)
254 if cmd != nil {
255 cmds = append(cmds, cmd)
256 }
257 case tea.KeyMsg, tea.MouseMsg:
258 switch msg := msg.(type) {
259 case tea.KeyMsg:
260 switch {
261 case key.Matches(msg, s.common.KeyMap.Back):
262 cmds = append(cmds, s.selector.Init())
263 }
264 }
265 t, cmd := s.tabs.Update(msg)
266 s.tabs = t.(*tabs.Tabs)
267 if cmd != nil {
268 cmds = append(cmds, cmd)
269 }
270 case tabs.ActiveTabMsg:
271 s.activeBox = box(msg)
272 }
273 switch s.activeBox {
274 case readmeBox:
275 r, cmd := s.readme.Update(msg)
276 s.readme = r.(*code.Code)
277 if cmd != nil {
278 cmds = append(cmds, cmd)
279 }
280 case selectorBox:
281 m, cmd := s.selector.Update(msg)
282 s.selector = m.(*selector.Selector)
283 if cmd != nil {
284 cmds = append(cmds, cmd)
285 }
286 }
287 return s, tea.Batch(cmds...)
288}
289
290// View implements tea.Model.
291func (s *Selection) View() string {
292 var view string
293 wm, hm := s.getMargins()
294 hm++ // tabs margin
295 switch s.activeBox {
296 case selectorBox:
297 ss := s.common.Styles.SelectorBox.Copy().
298 Width(s.common.Width - wm).
299 Height(s.common.Height - hm)
300 view = ss.Render(s.selector.View())
301 case readmeBox:
302 rs := s.common.Styles.ReadmeBox.Copy().
303 Height(s.common.Height - hm)
304 status := fmt.Sprintf("☰ %.f%%", s.readme.ScrollPercent()*100)
305 readmeStatus := lipgloss.NewStyle().
306 Align(lipgloss.Right).
307 Width(s.common.Width - wm).
308 Foreground(s.common.Styles.InactiveBorderColor).
309 Render(status)
310 view = rs.Render(lipgloss.JoinVertical(lipgloss.Top,
311 s.readme.View(),
312 readmeStatus,
313 ))
314 }
315 ts := s.common.Styles.Tabs.Copy().
316 MarginBottom(1)
317 return lipgloss.JoinVertical(lipgloss.Top,
318 ts.Render(s.tabs.View()),
319 view,
320 )
321}