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 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: selectorBox, // start with the selector focused
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,
46 []selector.IdentifiableItem{},
47 ItemDelegate{common.Styles, &sel.activeBox})
48 return sel
49}
50
51// SetSize implements common.Component.
52func (s *Selection) SetSize(width, height int) {
53 s.common.SetSize(width, height)
54 sw := s.common.Styles.SelectorBox.GetWidth()
55 wm := sw +
56 s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
57 s.common.Styles.ReadmeBox.GetHorizontalFrameSize() +
58 // +1 to get wrapping to work.
59 // This is needed because the readme box width has to be -1 from the
60 // readme style in order for wrapping to not break.
61 1
62 hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
63 s.readme.SetSize(width-wm, height-hm)
64 s.selector.SetSize(sw, height)
65}
66
67// ShortHelp implements help.KeyMap.
68func (s *Selection) ShortHelp() []key.Binding {
69 k := s.selector.KeyMap()
70 kb := make([]key.Binding, 0)
71 kb = append(kb,
72 s.common.Keymap.UpDown,
73 s.common.Keymap.Section,
74 )
75 if s.activeBox == selectorBox {
76 kb = append(kb,
77 s.common.Keymap.Select,
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 url: yank(repoUrl(cfg, r.Repo)),
147 })
148 }
149 for _, r := range cfg.Source.AllRepos() {
150 exists := false
151 head, err := r.HEAD()
152 if err != nil {
153 return common.ErrorCmd(err)
154 }
155 lc, err := r.CommitsByPage(head, 1, 1)
156 if err != nil {
157 return common.ErrorCmd(err)
158 }
159 lastUpdate := lc[0].Committer.When
160 for _, item := range items {
161 item := item.(Item)
162 if item.repo == r.Name() {
163 exists = true
164 item.lastUpdate = lastUpdate
165 break
166 }
167 }
168 if !exists {
169 items = append(items, Item{
170 name: r.Name(),
171 repo: r.Name(),
172 desc: "",
173 lastUpdate: lastUpdate,
174 url: yank(repoUrl(cfg, r.Name())),
175 })
176 }
177 }
178 return tea.Batch(
179 s.selector.Init(),
180 s.selector.SetItems(items),
181 )
182}
183
184// Update implements tea.Model.
185func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
186 cmds := make([]tea.Cmd, 0)
187 switch msg := msg.(type) {
188 case tea.WindowSizeMsg:
189 r, cmd := s.readme.Update(msg)
190 s.readme = r.(*code.Code)
191 if cmd != nil {
192 cmds = append(cmds, cmd)
193 }
194 m, cmd := s.selector.Update(msg)
195 s.selector = m.(*selector.Selector)
196 if cmd != nil {
197 cmds = append(cmds, cmd)
198 }
199 case selector.ActiveMsg:
200 cmds = append(cmds, s.changeActive(msg))
201 // reset readme position when active item change
202 s.readme.GotoTop()
203 case tea.KeyMsg:
204 switch {
205 case key.Matches(msg, s.common.Keymap.Section):
206 s.activeBox = (s.activeBox + 1) % 2
207 }
208 }
209 switch s.activeBox {
210 case readmeBox:
211 r, cmd := s.readme.Update(msg)
212 s.readme = r.(*code.Code)
213 if cmd != nil {
214 cmds = append(cmds, cmd)
215 }
216 case selectorBox:
217 m, cmd := s.selector.Update(msg)
218 s.selector = m.(*selector.Selector)
219 if cmd != nil {
220 cmds = append(cmds, cmd)
221 }
222 }
223 return s, tea.Batch(cmds...)
224}
225
226// View implements tea.Model.
227func (s *Selection) View() string {
228 wm := s.common.Styles.SelectorBox.GetWidth() +
229 s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
230 s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
231 hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
232 rs := s.common.Styles.ReadmeBox.Copy().
233 Width(s.common.Width - wm).
234 Height(s.common.Height - hm)
235 if s.activeBox == readmeBox {
236 rs.BorderForeground(s.common.Styles.ActiveBorderColor)
237 }
238 readme := rs.Render(s.readme.View())
239 return lipgloss.JoinHorizontal(
240 lipgloss.Top,
241 readme,
242 s.selector.View(),
243 )
244}
245
246func (s *Selection) changeActive(msg selector.ActiveMsg) tea.Cmd {
247 cfg := s.s.Config()
248 r, err := cfg.Source.GetRepo(string(msg))
249 if err != nil {
250 return common.ErrorCmd(err)
251 }
252 rm, rp := r.Readme()
253 return s.readme.SetContent(rm, rp)
254}
255
256func repoUrl(cfg *appCfg.Config, name string) string {
257 port := ""
258 if cfg.Port != 22 {
259 port += fmt.Sprintf(":%d", cfg.Port)
260 }
261 return fmt.Sprintf("git clone ssh://%s/%s", cfg.Host+port, name)
262}