selection.go

  1package selection
  2
  3import (
  4	"github.com/charmbracelet/bubbles/key"
  5	tea "github.com/charmbracelet/bubbletea"
  6	"github.com/charmbracelet/lipgloss"
  7	"github.com/charmbracelet/soft-serve/ui/common"
  8	"github.com/charmbracelet/soft-serve/ui/components/code"
  9	"github.com/charmbracelet/soft-serve/ui/components/selector"
 10	"github.com/charmbracelet/soft-serve/ui/git"
 11	"github.com/charmbracelet/soft-serve/ui/session"
 12	wgit "github.com/charmbracelet/wish/git"
 13)
 14
 15type box int
 16
 17const (
 18	readmeBox box = iota
 19	selectorBox
 20)
 21
 22// Selection is the model for the selection screen/page.
 23type Selection struct {
 24	s         session.Session
 25	common    common.Common
 26	readme    *code.Code
 27	selector  *selector.Selector
 28	activeBox box
 29}
 30
 31// New creates a new selection model.
 32func New(s session.Session, common common.Common) *Selection {
 33	sel := &Selection{
 34		s:         s,
 35		common:    common,
 36		activeBox: selectorBox, // start with the selector focused
 37	}
 38	readme := code.New(common, "", "")
 39	readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.")
 40	selector := selector.New(common,
 41		[]selector.IdentifiableItem{},
 42		ItemDelegate{&common, &sel.activeBox})
 43	selector.SetShowTitle(false)
 44	selector.SetShowHelp(false)
 45	selector.SetShowStatusBar(false)
 46	selector.DisableQuitKeybindings()
 47	sel.selector = selector
 48	sel.readme = readme
 49	return sel
 50}
 51
 52// SetSize implements common.Component.
 53func (s *Selection) SetSize(width, height int) {
 54	s.common.SetSize(width, height)
 55	sw := s.common.Styles.SelectorBox.GetWidth()
 56	wm := sw +
 57		s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
 58		s.common.Styles.ReadmeBox.GetHorizontalFrameSize() +
 59		// +1 to get wrapping to work.
 60		// This is needed because the readme box width has to be -1 from the
 61		// readme style in order for wrapping to not break.
 62		1
 63	hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
 64	s.readme.SetSize(width-wm, height-hm)
 65	s.selector.SetSize(sw, height)
 66}
 67
 68// ShortHelp implements help.KeyMap.
 69func (s *Selection) ShortHelp() []key.Binding {
 70	k := s.selector.KeyMap
 71	kb := make([]key.Binding, 0)
 72	kb = append(kb,
 73		s.common.KeyMap.UpDown,
 74		s.common.KeyMap.Section,
 75	)
 76	if s.activeBox == selectorBox {
 77		copyKey := s.common.KeyMap.Copy
 78		copyKey.SetHelp("c", "copy command")
 79		kb = append(kb,
 80			s.common.KeyMap.Select,
 81			k.Filter,
 82			k.ClearFilter,
 83			copyKey,
 84		)
 85	}
 86	return kb
 87}
 88
 89// FullHelp implements help.KeyMap.
 90func (s *Selection) FullHelp() [][]key.Binding {
 91	switch s.activeBox {
 92	case readmeBox:
 93		k := s.readme.KeyMap
 94		return [][]key.Binding{
 95			{
 96				k.PageDown,
 97				k.PageUp,
 98			},
 99			{
100				k.HalfPageDown,
101				k.HalfPageUp,
102			},
103			{
104				k.Down,
105				k.Up,
106			},
107		}
108	case selectorBox:
109		copyKey := s.common.KeyMap.Copy
110		copyKey.SetHelp("c", "copy command")
111		k := s.selector.KeyMap
112		return [][]key.Binding{
113			{
114				s.common.KeyMap.Select,
115				copyKey,
116			},
117			{
118				k.CursorUp,
119				k.CursorDown,
120			},
121			{
122				k.NextPage,
123				k.PrevPage,
124			},
125			{
126				k.GoToStart,
127				k.GoToEnd,
128			},
129			{
130				k.Filter,
131				k.ClearFilter,
132				k.CancelWhileFiltering,
133				k.AcceptWhileFiltering,
134			},
135		}
136	}
137	return [][]key.Binding{}
138}
139
140// Init implements tea.Model.
141func (s *Selection) Init() tea.Cmd {
142	items := make([]selector.IdentifiableItem, 0)
143	cfg := s.s.Config()
144	pk := s.s.PublicKey()
145	// Put configured repos first
146	for _, r := range cfg.Repos {
147		if r.Private && cfg.AuthRepo(r.Repo, pk) < wgit.AdminAccess {
148			continue
149		}
150		repo, err := cfg.Source.GetRepo(r.Repo)
151		if err != nil {
152			continue
153		}
154		items = append(items, Item{
155			repo: repo,
156			cmd:  git.RepoURL(cfg.Host, cfg.Port, r.Repo),
157		})
158	}
159	for _, r := range cfg.Source.AllRepos() {
160		if r.IsPrivate() && cfg.AuthRepo(r.Repo(), pk) < wgit.AdminAccess {
161			continue
162		}
163		exists := false
164		head, err := r.HEAD()
165		if err != nil {
166			return common.ErrorCmd(err)
167		}
168		lc, err := r.CommitsByPage(head, 1, 1)
169		if err != nil {
170			return common.ErrorCmd(err)
171		}
172		lastUpdate := lc[0].Committer.When
173		if lastUpdate.IsZero() {
174			lastUpdate = lc[0].Author.When
175		}
176		for i, item := range items {
177			item := item.(Item)
178			if item.repo.Repo() == r.Repo() {
179				exists = true
180				item.lastUpdate = lastUpdate
181				items[i] = item
182				break
183			}
184		}
185		if !exists {
186			items = append(items, Item{
187				repo:       r,
188				lastUpdate: lastUpdate,
189				cmd:        git.RepoURL(cfg.Host, cfg.Port, r.Name()),
190			})
191		}
192	}
193	return tea.Batch(
194		s.selector.Init(),
195		s.selector.SetItems(items),
196	)
197}
198
199// Update implements tea.Model.
200func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
201	cmds := make([]tea.Cmd, 0)
202	switch msg := msg.(type) {
203	case tea.WindowSizeMsg:
204		r, cmd := s.readme.Update(msg)
205		s.readme = r.(*code.Code)
206		if cmd != nil {
207			cmds = append(cmds, cmd)
208		}
209		m, cmd := s.selector.Update(msg)
210		s.selector = m.(*selector.Selector)
211		if cmd != nil {
212			cmds = append(cmds, cmd)
213		}
214	case selector.ActiveMsg:
215		cmds = append(cmds, s.changeActive(msg))
216		// reset readme position when active item change
217		s.readme.GotoTop()
218	case tea.KeyMsg:
219		switch {
220		case key.Matches(msg, s.common.KeyMap.Section):
221			s.activeBox = (s.activeBox + 1) % 2
222		case key.Matches(msg, s.common.KeyMap.Back):
223			cmds = append(cmds, s.selector.Init())
224		}
225	}
226	switch s.activeBox {
227	case readmeBox:
228		r, cmd := s.readme.Update(msg)
229		s.readme = r.(*code.Code)
230		if cmd != nil {
231			cmds = append(cmds, cmd)
232		}
233	case selectorBox:
234		m, cmd := s.selector.Update(msg)
235		s.selector = m.(*selector.Selector)
236		if cmd != nil {
237			cmds = append(cmds, cmd)
238		}
239	}
240	return s, tea.Batch(cmds...)
241}
242
243// View implements tea.Model.
244func (s *Selection) View() string {
245	wm := s.common.Styles.SelectorBox.GetWidth() +
246		s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
247		s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
248	hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
249	rs := s.common.Styles.ReadmeBox.Copy().
250		Width(s.common.Width - wm).
251		Height(s.common.Height - hm)
252	if s.activeBox == readmeBox {
253		rs.BorderForeground(s.common.Styles.ActiveBorderColor)
254	}
255	readme := rs.Render(s.readme.View())
256	return lipgloss.JoinHorizontal(
257		lipgloss.Top,
258		readme,
259		s.selector.View(),
260	)
261}
262
263func (s *Selection) changeActive(msg selector.ActiveMsg) tea.Cmd {
264	item, ok := msg.IdentifiableItem.(Item)
265	if !ok {
266		return nil
267	}
268	r := item.repo
269	rm, rp := r.Readme()
270	return s.readme.SetContent(rm, rp)
271}