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.TopLevelNormalTab.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	return lipgloss.JoinVertical(
306		lipgloss.Left,
307		s.common.Styles.Tabs.Render(s.tabs.View()),
308		view,
309	)
310}