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