selection.go

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