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 fix yankable component
110	yank := func(text string) *yankable.Yankable {
111		return yankable.New(
112			lipgloss.NewStyle().Foreground(lipgloss.Color("168")),
113			lipgloss.NewStyle().Foreground(lipgloss.Color("168")).SetString("Copied!"),
114			text,
115		)
116	}
117	// Put configured repos first
118	for _, r := range cfg.Repos {
119		items = append(items, Item{
120			Title:       r.Name,
121			Name:        r.Repo,
122			Description: r.Note,
123			LastUpdate:  time.Now(),
124			URL:         yank(repoUrl(cfg, r.Name)),
125		})
126	}
127	for _, r := range cfg.Source.AllRepos() {
128		exists := false
129		for _, item := range items {
130			item := item.(Item)
131			if item.Name == r.Name() {
132				exists = true
133				break
134			}
135		}
136		if !exists {
137			items = append(items, Item{
138				Title:       r.Name(),
139				Name:        r.Name(),
140				Description: "",
141				LastUpdate:  time.Now(),
142				URL:         yank(repoUrl(cfg, r.Name())),
143			})
144		}
145	}
146	return tea.Batch(
147		s.selector.Init(),
148		s.selector.SetItems(items),
149	)
150}
151
152// Update implements tea.Model.
153func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
154	cmds := make([]tea.Cmd, 0)
155	switch msg := msg.(type) {
156	case tea.WindowSizeMsg:
157		r, cmd := s.readme.Update(msg)
158		s.readme = r.(*code.Code)
159		if cmd != nil {
160			cmds = append(cmds, cmd)
161		}
162		m, cmd := s.selector.Update(msg)
163		s.selector = m.(*selector.Selector)
164		if cmd != nil {
165			cmds = append(cmds, cmd)
166		}
167	case selector.ActiveMsg:
168		cmds = append(cmds, s.changeActive(msg))
169		// reset readme position
170		s.readme.GotoTop()
171	case tea.KeyMsg:
172		switch {
173		case key.Matches(msg, s.common.Keymap.Section):
174			s.activeBox = (s.activeBox + 1) % 2
175		}
176	}
177	switch s.activeBox {
178	case readmeBox:
179		r, cmd := s.readme.Update(msg)
180		s.readme = r.(*code.Code)
181		if cmd != nil {
182			cmds = append(cmds, cmd)
183		}
184	case selectorBox:
185		m, cmd := s.selector.Update(msg)
186		s.selector = m.(*selector.Selector)
187		if cmd != nil {
188			cmds = append(cmds, cmd)
189		}
190	}
191	return s, tea.Batch(cmds...)
192}
193
194// View implements tea.Model.
195func (s *Selection) View() string {
196	wm := s.common.Styles.SelectorBox.GetWidth() +
197		s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
198		s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
199	hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
200	rs := s.common.Styles.ReadmeBox.Copy().
201		Width(s.common.Width - wm).
202		Height(s.common.Height - hm)
203	if s.activeBox == readmeBox {
204		rs.BorderForeground(s.common.Styles.ActiveBorderColor)
205	}
206	readme := rs.Render(s.readme.View())
207	return lipgloss.JoinHorizontal(
208		lipgloss.Top,
209		readme,
210		s.selector.View(),
211	)
212}
213
214func (s *Selection) changeActive(msg selector.ActiveMsg) tea.Cmd {
215	cfg := s.s.Config()
216	r, err := cfg.Source.GetRepo(string(msg))
217	if err != nil {
218		return common.ErrorCmd(err)
219	}
220	rm, rp := r.Readme()
221	return s.readme.SetContent(rm, rp)
222}
223
224func repoUrl(cfg *appCfg.Config, name string) string {
225	port := ""
226	if cfg.Port != 22 {
227		port += fmt.Sprintf(":%d", cfg.Port)
228	}
229	return fmt.Sprintf("git clone ssh://%s/%s", cfg.Host+port, name)
230}