selection.go

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