selection.go

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