selection.go

  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	// Add TERM using pty.Term if it's not already set.
118	for _, env := range environ {
119		if strings.HasPrefix(env, "TERM=") {
120			termExists = true
121			break
122		}
123	}
124	if !termExists {
125		pty, _, _ := session.Pty()
126		environ = append(environ, fmt.Sprintf("TERM=%s", pty.Term))
127	}
128	items := make([]list.Item, 0)
129	cfg := s.s.Config()
130	// TODO clean up this
131	yank := func(text string) *yankable.Yankable {
132		return yankable.New(
133			session,
134			environ,
135			lipgloss.NewStyle().Foreground(lipgloss.Color("168")),
136			lipgloss.NewStyle().Foreground(lipgloss.Color("168")).SetString("Copied!"),
137			text,
138		)
139	}
140	// Put configured repos first
141	for _, r := range cfg.Repos {
142		items = append(items, Item{
143			name:       r.Name,
144			repo:       r.Repo,
145			desc:       r.Note,
146			lastUpdate: time.Now(), // TODO get repo last update
147			url:        yank(repoUrl(cfg, r.Repo)),
148		})
149	}
150	for _, r := range cfg.Source.AllRepos() {
151		exists := false
152		for _, item := range items {
153			item := item.(Item)
154			if item.repo == r.Name() {
155				exists = true
156				break
157			}
158		}
159		if !exists {
160			items = append(items, Item{
161				name:       r.Name(),
162				repo:       r.Name(),
163				desc:       "",
164				lastUpdate: time.Now(), // TODO get repo last update
165				url:        yank(repoUrl(cfg, r.Name())),
166			})
167		}
168	}
169	return tea.Batch(
170		s.selector.Init(),
171		s.selector.SetItems(items),
172	)
173}
174
175// Update implements tea.Model.
176func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
177	cmds := make([]tea.Cmd, 0)
178	switch msg := msg.(type) {
179	case tea.WindowSizeMsg:
180		r, cmd := s.readme.Update(msg)
181		s.readme = r.(*code.Code)
182		if cmd != nil {
183			cmds = append(cmds, cmd)
184		}
185		m, cmd := s.selector.Update(msg)
186		s.selector = m.(*selector.Selector)
187		if cmd != nil {
188			cmds = append(cmds, cmd)
189		}
190	case selector.ActiveMsg:
191		cmds = append(cmds, s.changeActive(msg))
192		// reset readme position when active item change
193		s.readme.GotoTop()
194	case tea.KeyMsg:
195		switch {
196		case key.Matches(msg, s.common.Keymap.Section):
197			s.activeBox = (s.activeBox + 1) % 2
198		}
199	}
200	switch s.activeBox {
201	case readmeBox:
202		r, cmd := s.readme.Update(msg)
203		s.readme = r.(*code.Code)
204		if cmd != nil {
205			cmds = append(cmds, cmd)
206		}
207	case selectorBox:
208		m, cmd := s.selector.Update(msg)
209		s.selector = m.(*selector.Selector)
210		if cmd != nil {
211			cmds = append(cmds, cmd)
212		}
213	}
214	return s, tea.Batch(cmds...)
215}
216
217// View implements tea.Model.
218func (s *Selection) View() string {
219	wm := s.common.Styles.SelectorBox.GetWidth() +
220		s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
221		s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
222	hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
223	rs := s.common.Styles.ReadmeBox.Copy().
224		Width(s.common.Width - wm).
225		Height(s.common.Height - hm)
226	if s.activeBox == readmeBox {
227		rs.BorderForeground(s.common.Styles.ActiveBorderColor)
228	}
229	readme := rs.Render(s.readme.View())
230	return lipgloss.JoinHorizontal(
231		lipgloss.Top,
232		readme,
233		s.selector.View(),
234	)
235}
236
237func (s *Selection) changeActive(msg selector.ActiveMsg) tea.Cmd {
238	cfg := s.s.Config()
239	r, err := cfg.Source.GetRepo(string(msg))
240	if err != nil {
241		return common.ErrorCmd(err)
242	}
243	rm, rp := r.Readme()
244	return s.readme.SetContent(rm, rp)
245}
246
247func repoUrl(cfg *appCfg.Config, name string) string {
248	port := ""
249	if cfg.Port != 22 {
250		port += fmt.Sprintf(":%d", cfg.Port)
251	}
252	return fmt.Sprintf("git clone ssh://%s/%s", cfg.Host+port, name)
253}