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}