1package selection
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/charmbracelet/bubbles/key"
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/charmbracelet/lipgloss"
10 "github.com/charmbracelet/soft-serve/config"
11 "github.com/charmbracelet/soft-serve/ui/common"
12 "github.com/charmbracelet/soft-serve/ui/components/code"
13 "github.com/charmbracelet/soft-serve/ui/components/selector"
14 "github.com/charmbracelet/soft-serve/ui/components/tabs"
15 "github.com/charmbracelet/soft-serve/ui/git"
16 wgit "github.com/charmbracelet/wish/git"
17 "github.com/gliderlabs/ssh"
18)
19
20type pane int
21
22const (
23 selectorPane pane = iota
24 readmePane
25 lastPane
26)
27
28func (p pane) String() string {
29 return []string{
30 "Repositories",
31 "About",
32 }[p]
33}
34
35// Selection is the model for the selection screen/page.
36type Selection struct {
37 cfg *config.Config
38 pk ssh.PublicKey
39 common common.Common
40 readme *code.Code
41 readmeHeight int
42 selector *selector.Selector
43 activePane pane
44 tabs *tabs.Tabs
45}
46
47// New creates a new selection model.
48func New(cfg *config.Config, pk ssh.PublicKey, common common.Common) *Selection {
49 ts := make([]string, lastPane)
50 for i, b := range []pane{selectorPane, readmePane} {
51 ts[i] = b.String()
52 }
53 t := tabs.New(common, ts)
54 t.TabSeparator = lipgloss.NewStyle()
55 t.TabInactive = common.Styles.TopLevelTabNormal.Copy()
56 t.TabActive = common.Styles.TopLevelActiveTab.Copy()
57 t.TabDot = common.Styles.TopLevelActiveTabDot.Copy()
58 t.UseDot = true
59 sel := &Selection{
60 cfg: cfg,
61 pk: pk,
62 common: common,
63 activePane: selectorPane, // start with the selector focused
64 tabs: t,
65 }
66 readme := code.New(common, "", "")
67 readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.")
68 selector := selector.New(common,
69 []selector.IdentifiableItem{},
70 ItemDelegate{&common, &sel.activePane})
71 selector.SetShowTitle(false)
72 selector.SetShowHelp(false)
73 selector.SetShowStatusBar(false)
74 selector.DisableQuitKeybindings()
75 sel.selector = selector
76 sel.readme = readme
77 return sel
78}
79
80func (s *Selection) getMargins() (wm, hm int) {
81 wm = 0
82 hm = s.common.Styles.Tabs.GetVerticalFrameSize() +
83 s.common.Styles.Tabs.GetHeight() +
84 2 // tabs margin see View()
85 if s.activePane == readmePane {
86 hm += 1 // readme statusbar
87 }
88 return
89}
90
91// SetSize implements common.Component.
92func (s *Selection) SetSize(width, height int) {
93 s.common.SetSize(width, height)
94 wm, hm := s.getMargins()
95 s.tabs.SetSize(width, height-hm)
96 s.selector.SetSize(width-wm, height-hm)
97 s.readme.SetSize(width-wm, height-hm)
98}
99
100// ShortHelp implements help.KeyMap.
101func (s *Selection) ShortHelp() []key.Binding {
102 k := s.selector.KeyMap
103 kb := make([]key.Binding, 0)
104 kb = append(kb,
105 s.common.KeyMap.UpDown,
106 s.common.KeyMap.Section,
107 )
108 if s.activePane == selectorPane {
109 copyKey := s.common.KeyMap.Copy
110 copyKey.SetHelp("c", "copy command")
111 kb = append(kb,
112 s.common.KeyMap.Select,
113 k.Filter,
114 k.ClearFilter,
115 copyKey,
116 )
117 }
118 return kb
119}
120
121// FullHelp implements help.KeyMap.
122func (s *Selection) FullHelp() [][]key.Binding {
123 switch s.activePane {
124 case readmePane:
125 k := s.readme.KeyMap
126 return [][]key.Binding{
127 {
128 k.PageDown,
129 k.PageUp,
130 },
131 {
132 k.HalfPageDown,
133 k.HalfPageUp,
134 },
135 {
136 k.Down,
137 k.Up,
138 },
139 }
140 case selectorPane:
141 copyKey := s.common.KeyMap.Copy
142 copyKey.SetHelp("c", "copy command")
143 k := s.selector.KeyMap
144 return [][]key.Binding{
145 {
146 s.common.KeyMap.Select,
147 copyKey,
148 k.CursorUp,
149 k.CursorDown,
150 },
151 {
152 k.NextPage,
153 k.PrevPage,
154 k.GoToStart,
155 k.GoToEnd,
156 },
157 {
158 k.Filter,
159 k.ClearFilter,
160 k.CancelWhileFiltering,
161 k.AcceptWhileFiltering,
162 },
163 }
164 }
165 return [][]key.Binding{}
166}
167
168// Init implements tea.Model.
169func (s *Selection) Init() tea.Cmd {
170 var readmeCmd tea.Cmd
171 items := make([]selector.IdentifiableItem, 0)
172 cfg := s.cfg
173 pk := s.pk
174 // Put configured repos first
175 for _, r := range cfg.Repos {
176 acc := cfg.AuthRepo(r.Repo, pk)
177 if r.Private && acc < wgit.ReadOnlyAccess {
178 continue
179 }
180 repo, err := cfg.Source.GetRepo(r.Repo)
181 if err != nil {
182 continue
183 }
184 items = append(items, Item{
185 repo: repo,
186 cmd: git.RepoURL(cfg.Host, cfg.Port, r.Repo),
187 })
188 }
189 for _, r := range cfg.Source.AllRepos() {
190 if r.Repo() == "config" {
191 rm, rp := r.Readme()
192 s.readmeHeight = strings.Count(rm, "\n")
193 readmeCmd = s.readme.SetContent(rm, rp)
194 }
195 acc := cfg.AuthRepo(r.Repo(), pk)
196 if r.IsPrivate() && acc < wgit.ReadOnlyAccess {
197 continue
198 }
199 exists := false
200 lc, err := r.Commit("HEAD")
201 if err != nil {
202 return common.ErrorCmd(err)
203 }
204 lastUpdate := lc.Committer.When
205 if lastUpdate.IsZero() {
206 lastUpdate = lc.Author.When
207 }
208 for i, item := range items {
209 item := item.(Item)
210 if item.repo.Repo() == r.Repo() {
211 exists = true
212 item.lastUpdate = lastUpdate
213 items[i] = item
214 break
215 }
216 }
217 if !exists {
218 items = append(items, Item{
219 repo: r,
220 lastUpdate: lastUpdate,
221 cmd: git.RepoURL(cfg.Host, cfg.Port, r.Name()),
222 })
223 }
224 }
225 return tea.Batch(
226 s.selector.Init(),
227 s.selector.SetItems(items),
228 readmeCmd,
229 )
230}
231
232// Update implements tea.Model.
233func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
234 cmds := make([]tea.Cmd, 0)
235 switch msg := msg.(type) {
236 case tea.WindowSizeMsg:
237 r, cmd := s.readme.Update(msg)
238 s.readme = r.(*code.Code)
239 if cmd != nil {
240 cmds = append(cmds, cmd)
241 }
242 m, cmd := s.selector.Update(msg)
243 s.selector = m.(*selector.Selector)
244 if cmd != nil {
245 cmds = append(cmds, cmd)
246 }
247 case tea.KeyMsg, tea.MouseMsg:
248 switch msg := msg.(type) {
249 case tea.KeyMsg:
250 switch {
251 case key.Matches(msg, s.common.KeyMap.Back):
252 cmds = append(cmds, s.selector.Init())
253 }
254 }
255 t, cmd := s.tabs.Update(msg)
256 s.tabs = t.(*tabs.Tabs)
257 if cmd != nil {
258 cmds = append(cmds, cmd)
259 }
260 case tabs.ActiveTabMsg:
261 s.activePane = pane(msg)
262 }
263 switch s.activePane {
264 case readmePane:
265 r, cmd := s.readme.Update(msg)
266 s.readme = r.(*code.Code)
267 if cmd != nil {
268 cmds = append(cmds, cmd)
269 }
270 case selectorPane:
271 m, cmd := s.selector.Update(msg)
272 s.selector = m.(*selector.Selector)
273 if cmd != nil {
274 cmds = append(cmds, cmd)
275 }
276 }
277 return s, tea.Batch(cmds...)
278}
279
280// View implements tea.Model.
281func (s *Selection) View() string {
282 var view string
283 wm, hm := s.getMargins()
284 hm++ // tabs margin
285 switch s.activePane {
286 case selectorPane:
287 ss := lipgloss.NewStyle().
288 Width(s.common.Width - wm).
289 Height(s.common.Height - hm)
290 view = ss.Render(s.selector.View())
291 case readmePane:
292 rs := lipgloss.NewStyle().
293 Height(s.common.Height - hm)
294 status := fmt.Sprintf("☰ %.f%%", s.readme.ScrollPercent()*100)
295 readmeStatus := lipgloss.NewStyle().
296 Align(lipgloss.Right).
297 Width(s.common.Width - wm).
298 Foreground(s.common.Styles.InactiveBorderColor).
299 Render(status)
300 view = rs.Render(lipgloss.JoinVertical(lipgloss.Left,
301 s.readme.View(),
302 readmeStatus,
303 ))
304 }
305 ts := s.common.Styles.Tabs.Copy()
306 return lipgloss.JoinVertical(lipgloss.Left, ts.Render(s.tabs.View()), view)
307}