selection.go

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