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