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