selection.go

  1package selection
  2
  3import (
  4	"fmt"
  5	"time"
  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	appCfg "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/yankable"
 16	"github.com/charmbracelet/soft-serve/ui/session"
 17)
 18
 19type box int
 20
 21const (
 22	readmeBox box = iota
 23	selectorBox
 24)
 25
 26// Selection is the model for the selection screen/page.
 27type Selection struct {
 28	s         session.Session
 29	common    common.Common
 30	readme    *code.Code
 31	selector  *selector.Selector
 32	activeBox box
 33}
 34
 35// New creates a new selection model.
 36func New(s session.Session, common common.Common) *Selection {
 37	sel := &Selection{
 38		s:         s,
 39		common:    common,
 40		activeBox: 1,
 41	}
 42	readme := code.New(common, "", "")
 43	readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.")
 44	sel.readme = readme
 45	sel.selector = selector.New(common, []list.Item{}, ItemDelegate{common.Styles, &sel.activeBox})
 46	return sel
 47}
 48
 49// SetSize implements common.Component.
 50func (s *Selection) SetSize(width, height int) {
 51	s.common.SetSize(width, height)
 52	sw := s.common.Styles.SelectorBox.GetWidth()
 53	wm := sw +
 54		s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
 55		s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
 56	hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
 57	s.readme.SetSize(width-wm, height-hm)
 58	s.selector.SetSize(sw, height)
 59}
 60
 61// ShortHelp implements help.KeyMap.
 62func (s *Selection) ShortHelp() []key.Binding {
 63	k := s.selector.KeyMap()
 64	kb := make([]key.Binding, 0)
 65	kb = append(kb,
 66		s.common.Keymap.UpDown,
 67		s.common.Keymap.Select,
 68	)
 69	if s.activeBox == selectorBox {
 70		kb = append(kb,
 71			k.Filter,
 72			k.ClearFilter,
 73		)
 74	}
 75	return kb
 76}
 77
 78// FullHelp implements help.KeyMap.
 79// TODO implement full help on ?
 80func (s *Selection) FullHelp() [][]key.Binding {
 81	k := s.selector.KeyMap()
 82	return [][]key.Binding{
 83		{
 84			k.CursorUp,
 85			k.CursorDown,
 86			k.NextPage,
 87			k.PrevPage,
 88			k.GoToStart,
 89			k.GoToEnd,
 90		},
 91		{
 92			k.Filter,
 93			k.ClearFilter,
 94			k.CancelWhileFiltering,
 95			k.AcceptWhileFiltering,
 96			k.ShowFullHelp,
 97			k.CloseFullHelp,
 98		},
 99		// Ignore the following keys:
100		// k.Quit,
101		// k.ForceQuit,
102	}
103}
104
105// Init implements tea.Model.
106func (s *Selection) Init() tea.Cmd {
107	items := make([]list.Item, 0)
108	cfg := s.s.Config()
109	// TODO clean up this
110	yank := func(text string) *yankable.Yankable {
111		return yankable.New(
112			s.s.Session(),
113			lipgloss.NewStyle().Foreground(lipgloss.Color("168")),
114			lipgloss.NewStyle().Foreground(lipgloss.Color("168")).SetString("Copied!"),
115			text,
116		)
117	}
118	// Put configured repos first
119	for _, r := range cfg.Repos {
120		items = append(items, Item{
121			Title:       r.Name,
122			Name:        r.Repo,
123			Description: r.Note,
124			LastUpdate:  time.Now(),
125			URL:         yank(repoUrl(cfg, r.Name)),
126		})
127	}
128	for _, r := range cfg.Source.AllRepos() {
129		exists := false
130		for _, item := range items {
131			item := item.(Item)
132			if item.Name == r.Name() {
133				exists = true
134				break
135			}
136		}
137		if !exists {
138			items = append(items, Item{
139				Title:       r.Name(),
140				Name:        r.Name(),
141				Description: "",
142				LastUpdate:  time.Now(),
143				URL:         yank(repoUrl(cfg, r.Name())),
144			})
145		}
146	}
147	return tea.Batch(
148		s.selector.Init(),
149		s.selector.SetItems(items),
150	)
151}
152
153// Update implements tea.Model.
154func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
155	cmds := make([]tea.Cmd, 0)
156	switch msg := msg.(type) {
157	case tea.WindowSizeMsg:
158		r, cmd := s.readme.Update(msg)
159		s.readme = r.(*code.Code)
160		if cmd != nil {
161			cmds = append(cmds, cmd)
162		}
163		m, cmd := s.selector.Update(msg)
164		s.selector = m.(*selector.Selector)
165		if cmd != nil {
166			cmds = append(cmds, cmd)
167		}
168	case selector.ActiveMsg:
169		cmds = append(cmds, s.changeActive(msg))
170		// reset readme position
171		s.readme.GotoTop()
172	case tea.KeyMsg:
173		switch {
174		case key.Matches(msg, s.common.Keymap.Section):
175			s.activeBox = (s.activeBox + 1) % 2
176		}
177	}
178	switch s.activeBox {
179	case readmeBox:
180		r, cmd := s.readme.Update(msg)
181		s.readme = r.(*code.Code)
182		if cmd != nil {
183			cmds = append(cmds, cmd)
184		}
185	case selectorBox:
186		m, cmd := s.selector.Update(msg)
187		s.selector = m.(*selector.Selector)
188		if cmd != nil {
189			cmds = append(cmds, cmd)
190		}
191	}
192	return s, tea.Batch(cmds...)
193}
194
195// View implements tea.Model.
196func (s *Selection) View() string {
197	wm := s.common.Styles.SelectorBox.GetWidth() +
198		s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
199		s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
200	hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
201	rs := s.common.Styles.ReadmeBox.Copy().
202		Width(s.common.Width - wm).
203		Height(s.common.Height - hm)
204	if s.activeBox == readmeBox {
205		rs.BorderForeground(s.common.Styles.ActiveBorderColor)
206	}
207	readme := rs.Render(s.readme.View())
208	return lipgloss.JoinHorizontal(
209		lipgloss.Top,
210		readme,
211		s.selector.View(),
212	)
213}
214
215func (s *Selection) changeActive(msg selector.ActiveMsg) tea.Cmd {
216	cfg := s.s.Config()
217	r, err := cfg.Source.GetRepo(string(msg))
218	if err != nil {
219		return common.ErrorCmd(err)
220	}
221	rm, rp := r.Readme()
222	return s.readme.SetContent(rm, rp)
223}
224
225func repoUrl(cfg *appCfg.Config, name string) string {
226	port := ""
227	if cfg.Port != 22 {
228		port += fmt.Sprintf(":%d", cfg.Port)
229	}
230	return fmt.Sprintf("git clone ssh://%s/%s", cfg.Host+port, name)
231}