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