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