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