selection.go

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