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