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