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