1package selection
2
3import (
4 "github.com/charmbracelet/bubbles/key"
5 tea "github.com/charmbracelet/bubbletea"
6 "github.com/charmbracelet/lipgloss"
7 "github.com/charmbracelet/soft-serve/ui/common"
8 "github.com/charmbracelet/soft-serve/ui/components/code"
9 "github.com/charmbracelet/soft-serve/ui/components/selector"
10 "github.com/charmbracelet/soft-serve/ui/git"
11 "github.com/charmbracelet/soft-serve/ui/session"
12)
13
14type box int
15
16const (
17 readmeBox box = iota
18 selectorBox
19)
20
21// Selection is the model for the selection screen/page.
22type Selection struct {
23 s session.Session
24 common common.Common
25 readme *code.Code
26 selector *selector.Selector
27 activeBox box
28}
29
30// New creates a new selection model.
31func New(s session.Session, common common.Common) *Selection {
32 sel := &Selection{
33 s: s,
34 common: common,
35 activeBox: selectorBox, // start with the selector focused
36 }
37 readme := code.New(common, "", "")
38 readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.")
39 selector := selector.New(common,
40 []selector.IdentifiableItem{},
41 ItemDelegate{&common, &sel.activeBox})
42 selector.SetShowTitle(false)
43 selector.SetShowHelp(false)
44 selector.SetShowStatusBar(false)
45 selector.DisableQuitKeybindings()
46 sel.selector = selector
47 sel.readme = readme
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 copyKey := s.common.KeyMap.Copy
77 copyKey.SetHelp("c", "copy command")
78 kb = append(kb,
79 s.common.KeyMap.Select,
80 k.Filter,
81 k.ClearFilter,
82 copyKey,
83 )
84 }
85 return kb
86}
87
88// FullHelp implements help.KeyMap.
89func (s *Selection) FullHelp() [][]key.Binding {
90 switch s.activeBox {
91 case readmeBox:
92 k := s.readme.KeyMap
93 return [][]key.Binding{
94 {
95 k.PageDown,
96 k.PageUp,
97 },
98 {
99 k.HalfPageDown,
100 k.HalfPageUp,
101 },
102 {
103 k.Down,
104 k.Up,
105 },
106 }
107 case selectorBox:
108 copyKey := s.common.KeyMap.Copy
109 copyKey.SetHelp("c", "copy command")
110 k := s.selector.KeyMap
111 return [][]key.Binding{
112 {
113 s.common.KeyMap.Select,
114 copyKey,
115 },
116 {
117 k.CursorUp,
118 k.CursorDown,
119 },
120 {
121 k.NextPage,
122 k.PrevPage,
123 },
124 {
125 k.GoToStart,
126 k.GoToEnd,
127 },
128 {
129 k.Filter,
130 k.ClearFilter,
131 k.CancelWhileFiltering,
132 k.AcceptWhileFiltering,
133 },
134 }
135 }
136 return [][]key.Binding{}
137}
138
139// Init implements tea.Model.
140func (s *Selection) Init() tea.Cmd {
141 items := make([]selector.IdentifiableItem, 0)
142 cfg := s.s.Config()
143 // Put configured repos first
144 for _, r := range cfg.Repos {
145 repo, err := cfg.Source.GetRepo(r.Repo)
146 if err != nil {
147 continue
148 }
149 items = append(items, Item{
150 repo: repo,
151 cmd: git.RepoURL(cfg.Host, cfg.Port, 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.Repo() == r.Repo() {
168 exists = true
169 item.lastUpdate = lastUpdate
170 break
171 }
172 }
173 if !exists {
174 items = append(items, Item{
175 repo: r,
176 lastUpdate: lastUpdate,
177 cmd: git.RepoURL(cfg.Host, cfg.Port, r.Name()),
178 })
179 }
180 }
181 return tea.Batch(
182 s.selector.Init(),
183 s.selector.SetItems(items),
184 )
185}
186
187// Update implements tea.Model.
188func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
189 cmds := make([]tea.Cmd, 0)
190 switch msg := msg.(type) {
191 case tea.WindowSizeMsg:
192 r, cmd := s.readme.Update(msg)
193 s.readme = r.(*code.Code)
194 if cmd != nil {
195 cmds = append(cmds, cmd)
196 }
197 m, cmd := s.selector.Update(msg)
198 s.selector = m.(*selector.Selector)
199 if cmd != nil {
200 cmds = append(cmds, cmd)
201 }
202 case selector.ActiveMsg:
203 cmds = append(cmds, s.changeActive(msg))
204 // reset readme position when active item change
205 s.readme.GotoTop()
206 case tea.KeyMsg:
207 switch {
208 case key.Matches(msg, s.common.KeyMap.Section):
209 s.activeBox = (s.activeBox + 1) % 2
210 case key.Matches(msg, s.common.KeyMap.Back):
211 cmds = append(cmds, s.selector.Init())
212 }
213 }
214 switch s.activeBox {
215 case readmeBox:
216 r, cmd := s.readme.Update(msg)
217 s.readme = r.(*code.Code)
218 if cmd != nil {
219 cmds = append(cmds, cmd)
220 }
221 case selectorBox:
222 m, cmd := s.selector.Update(msg)
223 s.selector = m.(*selector.Selector)
224 if cmd != nil {
225 cmds = append(cmds, cmd)
226 }
227 }
228 return s, tea.Batch(cmds...)
229}
230
231// View implements tea.Model.
232func (s *Selection) View() string {
233 wm := s.common.Styles.SelectorBox.GetWidth() +
234 s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
235 s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
236 hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
237 rs := s.common.Styles.ReadmeBox.Copy().
238 Width(s.common.Width - wm).
239 Height(s.common.Height - hm)
240 if s.activeBox == readmeBox {
241 rs.BorderForeground(s.common.Styles.ActiveBorderColor)
242 }
243 readme := rs.Render(s.readme.View())
244 return lipgloss.JoinHorizontal(
245 lipgloss.Top,
246 readme,
247 s.selector.View(),
248 )
249}
250
251func (s *Selection) changeActive(msg selector.ActiveMsg) tea.Cmd {
252 r := msg.IdentifiableItem.(Item).repo
253 rm, rp := r.Readme()
254 return s.readme.SetContent(rm, rp)
255}