1package selection
2
3import (
4 "fmt"
5 "time"
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: 1,
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, []list.Item{}, ItemDelegate{common.Styles, &sel.activeBox})
46 return sel
47}
48
49// SetSize implements common.Component.
50func (s *Selection) SetSize(width, height int) {
51 s.common.SetSize(width, height)
52 sw := s.common.Styles.SelectorBox.GetWidth()
53 wm := sw +
54 s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
55 s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
56 hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
57 s.readme.SetSize(width-wm, height-hm)
58 s.selector.SetSize(sw, height)
59}
60
61// ShortHelp implements help.KeyMap.
62func (s *Selection) ShortHelp() []key.Binding {
63 k := s.selector.KeyMap()
64 kb := make([]key.Binding, 0)
65 kb = append(kb,
66 s.common.Keymap.UpDown,
67 s.common.Keymap.Select,
68 )
69 if s.activeBox == selectorBox {
70 kb = append(kb,
71 k.Filter,
72 k.ClearFilter,
73 )
74 }
75 return kb
76}
77
78// FullHelp implements help.KeyMap.
79// TODO implement full help on ?
80func (s *Selection) FullHelp() [][]key.Binding {
81 k := s.selector.KeyMap()
82 return [][]key.Binding{
83 {
84 k.CursorUp,
85 k.CursorDown,
86 k.NextPage,
87 k.PrevPage,
88 k.GoToStart,
89 k.GoToEnd,
90 },
91 {
92 k.Filter,
93 k.ClearFilter,
94 k.CancelWhileFiltering,
95 k.AcceptWhileFiltering,
96 k.ShowFullHelp,
97 k.CloseFullHelp,
98 },
99 // Ignore the following keys:
100 // k.Quit,
101 // k.ForceQuit,
102 }
103}
104
105// Init implements tea.Model.
106func (s *Selection) Init() tea.Cmd {
107 items := make([]list.Item, 0)
108 cfg := s.s.Config()
109 // TODO clean up this
110 yank := func(text string) *yankable.Yankable {
111 return yankable.New(
112 s.s.Session(),
113 lipgloss.NewStyle().Foreground(lipgloss.Color("168")),
114 lipgloss.NewStyle().Foreground(lipgloss.Color("168")).SetString("Copied!"),
115 text,
116 )
117 }
118 // Put configured repos first
119 for _, r := range cfg.Repos {
120 items = append(items, Item{
121 Title: r.Name,
122 Name: r.Repo,
123 Description: r.Note,
124 LastUpdate: time.Now(),
125 URL: yank(repoUrl(cfg, r.Name)),
126 })
127 }
128 for _, r := range cfg.Source.AllRepos() {
129 exists := false
130 for _, item := range items {
131 item := item.(Item)
132 if item.Name == r.Name() {
133 exists = true
134 break
135 }
136 }
137 if !exists {
138 items = append(items, Item{
139 Title: r.Name(),
140 Name: r.Name(),
141 Description: "",
142 LastUpdate: time.Now(),
143 URL: yank(repoUrl(cfg, r.Name())),
144 })
145 }
146 }
147 return tea.Batch(
148 s.selector.Init(),
149 s.selector.SetItems(items),
150 )
151}
152
153// Update implements tea.Model.
154func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
155 cmds := make([]tea.Cmd, 0)
156 switch msg := msg.(type) {
157 case tea.WindowSizeMsg:
158 r, cmd := s.readme.Update(msg)
159 s.readme = r.(*code.Code)
160 if cmd != nil {
161 cmds = append(cmds, cmd)
162 }
163 m, cmd := s.selector.Update(msg)
164 s.selector = m.(*selector.Selector)
165 if cmd != nil {
166 cmds = append(cmds, cmd)
167 }
168 case selector.ActiveMsg:
169 cmds = append(cmds, s.changeActive(msg))
170 // reset readme position
171 s.readme.GotoTop()
172 case tea.KeyMsg:
173 switch {
174 case key.Matches(msg, s.common.Keymap.Section):
175 s.activeBox = (s.activeBox + 1) % 2
176 }
177 }
178 switch s.activeBox {
179 case readmeBox:
180 r, cmd := s.readme.Update(msg)
181 s.readme = r.(*code.Code)
182 if cmd != nil {
183 cmds = append(cmds, cmd)
184 }
185 case selectorBox:
186 m, cmd := s.selector.Update(msg)
187 s.selector = m.(*selector.Selector)
188 if cmd != nil {
189 cmds = append(cmds, cmd)
190 }
191 }
192 return s, tea.Batch(cmds...)
193}
194
195// View implements tea.Model.
196func (s *Selection) View() string {
197 wm := s.common.Styles.SelectorBox.GetWidth() +
198 s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
199 s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
200 hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
201 rs := s.common.Styles.ReadmeBox.Copy().
202 Width(s.common.Width - wm).
203 Height(s.common.Height - hm)
204 if s.activeBox == readmeBox {
205 rs.BorderForeground(s.common.Styles.ActiveBorderColor)
206 }
207 readme := rs.Render(s.readme.View())
208 return lipgloss.JoinHorizontal(
209 lipgloss.Top,
210 readme,
211 s.selector.View(),
212 )
213}
214
215func (s *Selection) changeActive(msg selector.ActiveMsg) tea.Cmd {
216 cfg := s.s.Config()
217 r, err := cfg.Source.GetRepo(string(msg))
218 if err != nil {
219 return common.ErrorCmd(err)
220 }
221 rm, rp := r.Readme()
222 return s.readme.SetContent(rm, rp)
223}
224
225func repoUrl(cfg *appCfg.Config, name string) string {
226 port := ""
227 if cfg.Port != 22 {
228 port += fmt.Sprintf(":%d", cfg.Port)
229 }
230 return fmt.Sprintf("git clone ssh://%s/%s", cfg.Host+port, name)
231}