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 k.CursorUp,
117 k.CursorDown,
118 },
119 {
120 k.NextPage,
121 k.PrevPage,
122 k.GoToStart,
123 k.GoToEnd,
124 },
125 {
126 k.Filter,
127 k.ClearFilter,
128 k.CancelWhileFiltering,
129 k.AcceptWhileFiltering,
130 },
131 }
132 }
133 return [][]key.Binding{}
134}
135
136// Init implements tea.Model.
137func (s *Selection) Init() tea.Cmd {
138 items := make([]selector.IdentifiableItem, 0)
139 cfg := s.s.Config()
140 pk := s.s.PublicKey()
141 // Put configured repos first
142 for _, r := range cfg.Repos {
143 if r.Private && cfg.AuthRepo(r.Repo, pk) < wgit.AdminAccess {
144 continue
145 }
146 repo, err := cfg.Source.GetRepo(r.Repo)
147 if err != nil {
148 continue
149 }
150 items = append(items, Item{
151 repo: repo,
152 cmd: git.RepoURL(cfg.Host, cfg.Port, r.Repo),
153 })
154 }
155 for _, r := range cfg.Source.AllRepos() {
156 if r.IsPrivate() && cfg.AuthRepo(r.Repo(), pk) < wgit.AdminAccess {
157 continue
158 }
159 exists := false
160 head, err := r.HEAD()
161 if err != nil {
162 return common.ErrorCmd(err)
163 }
164 lc, err := r.CommitsByPage(head, 1, 1)
165 if err != nil {
166 return common.ErrorCmd(err)
167 }
168 lastUpdate := lc[0].Committer.When
169 if lastUpdate.IsZero() {
170 lastUpdate = lc[0].Author.When
171 }
172 for i, item := range items {
173 item := item.(Item)
174 if item.repo.Repo() == r.Repo() {
175 exists = true
176 item.lastUpdate = lastUpdate
177 items[i] = item
178 break
179 }
180 }
181 if !exists {
182 items = append(items, Item{
183 repo: r,
184 lastUpdate: lastUpdate,
185 cmd: git.RepoURL(cfg.Host, cfg.Port, r.Name()),
186 })
187 }
188 }
189 return tea.Batch(
190 s.selector.Init(),
191 s.selector.SetItems(items),
192 )
193}
194
195// Update implements tea.Model.
196func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
197 cmds := make([]tea.Cmd, 0)
198 switch msg := msg.(type) {
199 case tea.WindowSizeMsg:
200 r, cmd := s.readme.Update(msg)
201 s.readme = r.(*code.Code)
202 if cmd != nil {
203 cmds = append(cmds, cmd)
204 }
205 m, cmd := s.selector.Update(msg)
206 s.selector = m.(*selector.Selector)
207 if cmd != nil {
208 cmds = append(cmds, cmd)
209 }
210 case selector.ActiveMsg:
211 cmds = append(cmds, s.changeActive(msg))
212 // reset readme position when active item change
213 s.readme.GotoTop()
214 case tea.KeyMsg:
215 switch {
216 case key.Matches(msg, s.common.KeyMap.Section):
217 s.activeBox = (s.activeBox + 1) % 2
218 case key.Matches(msg, s.common.KeyMap.Back):
219 cmds = append(cmds, s.selector.Init())
220 }
221 }
222 switch s.activeBox {
223 case readmeBox:
224 r, cmd := s.readme.Update(msg)
225 s.readme = r.(*code.Code)
226 if cmd != nil {
227 cmds = append(cmds, cmd)
228 }
229 case selectorBox:
230 m, cmd := s.selector.Update(msg)
231 s.selector = m.(*selector.Selector)
232 if cmd != nil {
233 cmds = append(cmds, cmd)
234 }
235 }
236 return s, tea.Batch(cmds...)
237}
238
239// View implements tea.Model.
240func (s *Selection) View() string {
241 wm := s.common.Styles.SelectorBox.GetWidth() +
242 s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
243 s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
244 hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
245 rs := s.common.Styles.ReadmeBox.Copy().
246 Width(s.common.Width - wm).
247 Height(s.common.Height - hm)
248 if s.activeBox == readmeBox {
249 rs.BorderForeground(s.common.Styles.ActiveBorderColor)
250 }
251 readme := rs.Render(s.readme.View())
252 return lipgloss.JoinHorizontal(
253 lipgloss.Top,
254 readme,
255 s.selector.View(),
256 )
257}
258
259func (s *Selection) changeActive(msg selector.ActiveMsg) tea.Cmd {
260 item, ok := msg.IdentifiableItem.(Item)
261 if !ok {
262 return nil
263 }
264 r := item.repo
265 rm, rp := r.Readme()
266 return s.readme.SetContent(rm, rp)
267}