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/soft-serve/server/access"
 12	"github.com/charmbracelet/soft-serve/server/backend"
 13	"github.com/charmbracelet/soft-serve/server/ui/common"
 14	"github.com/charmbracelet/soft-serve/server/ui/components/code"
 15	"github.com/charmbracelet/soft-serve/server/ui/components/selector"
 16	"github.com/charmbracelet/soft-serve/server/ui/components/tabs"
 17)
 18
 19const (
 20	defaultNoContent = "No readme found.\n\nCreate a `.soft-serve` repository and add a `README.md` file to display readme."
 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	selector   *selector.Selector
 43	activePane pane
 44	tabs       *tabs.Tabs
 45}
 46
 47// New creates a new selection model.
 48func New(c 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(c, ts)
 54	t.TabSeparator = lipgloss.NewStyle()
 55	t.TabInactive = c.Styles.TopLevelNormalTab.Copy()
 56	t.TabActive = c.Styles.TopLevelActiveTab.Copy()
 57	t.TabDot = c.Styles.TopLevelActiveTabDot.Copy()
 58	t.UseDot = true
 59	sel := &Selection{
 60		common:     c,
 61		activePane: selectorPane, // start with the selector focused
 62		tabs:       t,
 63	}
 64	readme := code.New(c, "", "")
 65	readme.NoContentStyle = c.Styles.NoContent.Copy().
 66		SetString(defaultNoContent)
 67	selector := selector.New(c,
 68		[]selector.IdentifiableItem{},
 69		NewItemDelegate(&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	ctx := s.common.Context()
191	be := s.common.Backend()
192	pk := s.common.PublicKey()
193	if pk == nil && !be.AllowKeyless(ctx) {
194		return nil
195	}
196
197	repos, err := be.Repositories(ctx)
198	if err != nil {
199		return common.ErrorCmd(err)
200	}
201	sortedItems := make(Items, 0)
202	for _, r := range repos {
203		if r.Name() == ".soft-serve" {
204			readme, path, err := backend.Readme(r)
205			if err != nil {
206				continue
207			}
208
209			readmeCmd = s.readme.SetContent(readme, path)
210		}
211
212		if r.IsHidden() {
213			continue
214		}
215		al := be.AccessLevelByPublicKey(ctx, r.Name(), pk)
216		if al >= access.ReadOnlyAccess {
217			item, err := NewItem(r, cfg)
218			if err != nil {
219				s.common.Logger.Debugf("ui: failed to create item for %s: %v", r.Name(), err)
220				continue
221			}
222			sortedItems = append(sortedItems, item)
223		}
224	}
225	sort.Sort(sortedItems)
226	items := make([]selector.IdentifiableItem, len(sortedItems))
227	for i, it := range sortedItems {
228		items[i] = it
229	}
230	return tea.Batch(
231		s.selector.Init(),
232		s.selector.SetItems(items),
233		readmeCmd,
234	)
235}
236
237// Update implements tea.Model.
238func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
239	cmds := make([]tea.Cmd, 0)
240	switch msg := msg.(type) {
241	case tea.WindowSizeMsg:
242		r, cmd := s.readme.Update(msg)
243		s.readme = r.(*code.Code)
244		if cmd != nil {
245			cmds = append(cmds, cmd)
246		}
247		m, cmd := s.selector.Update(msg)
248		s.selector = m.(*selector.Selector)
249		if cmd != nil {
250			cmds = append(cmds, cmd)
251		}
252	case tea.KeyMsg, tea.MouseMsg:
253		switch msg := msg.(type) {
254		case tea.KeyMsg:
255			switch {
256			case key.Matches(msg, s.common.KeyMap.Back):
257				cmds = append(cmds, s.selector.Init())
258			}
259		}
260		t, cmd := s.tabs.Update(msg)
261		s.tabs = t.(*tabs.Tabs)
262		if cmd != nil {
263			cmds = append(cmds, cmd)
264		}
265	case tabs.ActiveTabMsg:
266		s.activePane = pane(msg)
267	}
268	switch s.activePane {
269	case readmePane:
270		r, cmd := s.readme.Update(msg)
271		s.readme = r.(*code.Code)
272		if cmd != nil {
273			cmds = append(cmds, cmd)
274		}
275	case selectorPane:
276		m, cmd := s.selector.Update(msg)
277		s.selector = m.(*selector.Selector)
278		if cmd != nil {
279			cmds = append(cmds, cmd)
280		}
281	}
282	return s, tea.Batch(cmds...)
283}
284
285// View implements tea.Model.
286func (s *Selection) View() string {
287	var view string
288	wm, hm := s.getMargins()
289	switch s.activePane {
290	case selectorPane:
291		ss := lipgloss.NewStyle().
292			Width(s.common.Width - wm).
293			Height(s.common.Height - hm)
294		view = ss.Render(s.selector.View())
295	case readmePane:
296		rs := lipgloss.NewStyle().
297			Height(s.common.Height - hm)
298		status := fmt.Sprintf("☰ %.f%%", s.readme.ScrollPercent()*100)
299		readmeStatus := lipgloss.NewStyle().
300			Align(lipgloss.Right).
301			Width(s.common.Width - wm).
302			Foreground(s.common.Styles.InactiveBorderColor).
303			Render(status)
304		view = rs.Render(lipgloss.JoinVertical(lipgloss.Left,
305			s.readme.View(),
306			readmeStatus,
307		))
308	}
309	if s.activePane != selectorPane || s.FilterState() != list.Filtering {
310		tabs := s.common.Styles.Tabs.Render(s.tabs.View())
311		view = lipgloss.JoinVertical(lipgloss.Left,
312			tabs,
313			view,
314		)
315	}
316	return lipgloss.JoinVertical(
317		lipgloss.Left,
318		view,
319	)
320}