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