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