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 for _, item := range items {
174 item := item.(Item)
175 if item.repo.Repo() == r.Repo() {
176 exists = true
177 item.lastUpdate = lastUpdate
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 r := msg.IdentifiableItem.(Item).repo
261 rm, rp := r.Readme()
262 return s.readme.SetContent(rm, rp)
263}