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