1package selection
2
3import (
4 "strings"
5
6 "github.com/charmbracelet/bubbles/key"
7 tea "github.com/charmbracelet/bubbletea"
8 "github.com/charmbracelet/lipgloss"
9 "github.com/charmbracelet/soft-serve/config"
10 "github.com/charmbracelet/soft-serve/ui/common"
11 "github.com/charmbracelet/soft-serve/ui/components/code"
12 "github.com/charmbracelet/soft-serve/ui/components/selector"
13 "github.com/charmbracelet/soft-serve/ui/git"
14 wgit "github.com/charmbracelet/wish/git"
15 "github.com/gliderlabs/ssh"
16)
17
18type box int
19
20const (
21 readmeBox box = iota
22 selectorBox
23)
24
25// Selection is the model for the selection screen/page.
26type Selection struct {
27 cfg *config.Config
28 pk ssh.PublicKey
29 common common.Common
30 readme *code.Code
31 readmeHeight int
32 selector *selector.Selector
33 activeBox box
34}
35
36// New creates a new selection model.
37func New(cfg *config.Config, pk ssh.PublicKey, common common.Common) *Selection {
38 sel := &Selection{
39 cfg: cfg,
40 pk: pk,
41 common: common,
42 activeBox: selectorBox, // start with the selector focused
43 }
44 readme := code.New(common, "", "")
45 readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.")
46 selector := selector.New(common,
47 []selector.IdentifiableItem{},
48 ItemDelegate{&common, &sel.activeBox})
49 selector.SetShowTitle(true)
50 selector.Title = "Repositories"
51 selector.SetShowHelp(false)
52 selector.SetShowStatusBar(false)
53 selector.DisableQuitKeybindings()
54 sel.selector = selector
55 sel.readme = readme
56 return sel
57}
58
59func (s *Selection) getReadmeHeight() int {
60 rh := s.readmeHeight
61 if rh > s.common.Height/3 {
62 rh = s.common.Height / 3
63 }
64 return rh
65}
66
67func (s *Selection) getMargins() (wm, hm int) {
68 wm = 0
69 hm = s.common.Styles.SelectorBox.GetVerticalFrameSize() +
70 s.common.Styles.SelectorBox.GetHeight()
71 if rh := s.getReadmeHeight(); rh > 0 {
72 hm += s.common.Styles.ReadmeBox.GetVerticalFrameSize() +
73 rh
74 }
75 return
76}
77
78// SetSize implements common.Component.
79func (s *Selection) SetSize(width, height int) {
80 s.common.SetSize(width, height)
81 wm, hm := s.getMargins()
82 s.readme.SetSize(width-wm, s.getReadmeHeight())
83 s.selector.SetSize(width-wm, height-hm)
84}
85
86// ShortHelp implements help.KeyMap.
87func (s *Selection) ShortHelp() []key.Binding {
88 k := s.selector.KeyMap
89 kb := make([]key.Binding, 0)
90 kb = append(kb,
91 s.common.KeyMap.UpDown,
92 s.common.KeyMap.Section,
93 )
94 if s.activeBox == selectorBox {
95 copyKey := s.common.KeyMap.Copy
96 copyKey.SetHelp("c", "copy command")
97 kb = append(kb,
98 s.common.KeyMap.Select,
99 k.Filter,
100 k.ClearFilter,
101 copyKey,
102 )
103 }
104 return kb
105}
106
107// FullHelp implements help.KeyMap.
108func (s *Selection) FullHelp() [][]key.Binding {
109 switch s.activeBox {
110 case readmeBox:
111 k := s.readme.KeyMap
112 return [][]key.Binding{
113 {
114 k.PageDown,
115 k.PageUp,
116 },
117 {
118 k.HalfPageDown,
119 k.HalfPageUp,
120 },
121 {
122 k.Down,
123 k.Up,
124 },
125 }
126 case selectorBox:
127 copyKey := s.common.KeyMap.Copy
128 copyKey.SetHelp("c", "copy command")
129 k := s.selector.KeyMap
130 return [][]key.Binding{
131 {
132 s.common.KeyMap.Select,
133 copyKey,
134 k.CursorUp,
135 k.CursorDown,
136 },
137 {
138 k.NextPage,
139 k.PrevPage,
140 k.GoToStart,
141 k.GoToEnd,
142 },
143 {
144 k.Filter,
145 k.ClearFilter,
146 k.CancelWhileFiltering,
147 k.AcceptWhileFiltering,
148 },
149 }
150 }
151 return [][]key.Binding{}
152}
153
154// Init implements tea.Model.
155func (s *Selection) Init() tea.Cmd {
156 var readmeCmd tea.Cmd
157 items := make([]selector.IdentifiableItem, 0)
158 cfg := s.cfg
159 pk := s.pk
160 // Put configured repos first
161 for _, r := range cfg.Repos {
162 if r.Private && cfg.AuthRepo(r.Repo, pk) < wgit.AdminAccess {
163 continue
164 }
165 repo, err := cfg.Source.GetRepo(r.Repo)
166 if err != nil {
167 continue
168 }
169 items = append(items, Item{
170 repo: repo,
171 cmd: git.RepoURL(cfg.Host, cfg.Port, r.Repo),
172 })
173 }
174 for _, r := range cfg.Source.AllRepos() {
175 if r.Repo() == "config" {
176 rm, rp := r.Readme()
177 s.readmeHeight = strings.Count(rm, "\n")
178 readmeCmd = s.readme.SetContent(rm, rp)
179 }
180 if r.IsPrivate() && cfg.AuthRepo(r.Repo(), pk) < wgit.AdminAccess {
181 continue
182 }
183 exists := false
184 head, err := r.HEAD()
185 if err != nil {
186 return common.ErrorCmd(err)
187 }
188 lc, err := r.CommitsByPage(head, 1, 1)
189 if err != nil {
190 return common.ErrorCmd(err)
191 }
192 lastUpdate := lc[0].Committer.When
193 if lastUpdate.IsZero() {
194 lastUpdate = lc[0].Author.When
195 }
196 for i, item := range items {
197 item := item.(Item)
198 if item.repo.Repo() == r.Repo() {
199 exists = true
200 item.lastUpdate = lastUpdate
201 items[i] = item
202 break
203 }
204 }
205 if !exists {
206 items = append(items, Item{
207 repo: r,
208 lastUpdate: lastUpdate,
209 cmd: git.RepoURL(cfg.Host, cfg.Port, r.Name()),
210 })
211 }
212 }
213 return tea.Batch(
214 s.selector.Init(),
215 s.selector.SetItems(items),
216 readmeCmd,
217 )
218}
219
220// Update implements tea.Model.
221func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
222 cmds := make([]tea.Cmd, 0)
223 switch msg := msg.(type) {
224 case tea.WindowSizeMsg:
225 r, cmd := s.readme.Update(msg)
226 s.readme = r.(*code.Code)
227 if cmd != nil {
228 cmds = append(cmds, cmd)
229 }
230 m, cmd := s.selector.Update(msg)
231 s.selector = m.(*selector.Selector)
232 if cmd != nil {
233 cmds = append(cmds, cmd)
234 }
235 case tea.KeyMsg:
236 switch {
237 case key.Matches(msg, s.common.KeyMap.Section):
238 s.activeBox = (s.activeBox + 1) % 2
239 case key.Matches(msg, s.common.KeyMap.Back):
240 cmds = append(cmds, s.selector.Init())
241 }
242 }
243 switch s.activeBox {
244 case readmeBox:
245 r, cmd := s.readme.Update(msg)
246 s.readme = r.(*code.Code)
247 if cmd != nil {
248 cmds = append(cmds, cmd)
249 }
250 case selectorBox:
251 m, cmd := s.selector.Update(msg)
252 s.selector = m.(*selector.Selector)
253 if cmd != nil {
254 cmds = append(cmds, cmd)
255 }
256 }
257 return s, tea.Batch(cmds...)
258}
259
260// View implements tea.Model.
261func (s *Selection) View() string {
262 rh := s.getReadmeHeight()
263 rs := s.common.Styles.ReadmeBox.Copy().
264 Width(s.common.Width).
265 Height(rh)
266 if s.activeBox == readmeBox {
267 rs.BorderForeground(s.common.Styles.ActiveBorderColor)
268 }
269 view := s.selector.View()
270 if rh > 0 {
271 readme := rs.Render(s.readme.View())
272 view = lipgloss.JoinVertical(lipgloss.Top,
273 readme,
274 view,
275 )
276 }
277 return view
278}