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 fix yankable component
110 yank := func(text string) *yankable.Yankable {
111 return yankable.New(
112 lipgloss.NewStyle().Foreground(lipgloss.Color("168")),
113 lipgloss.NewStyle().Foreground(lipgloss.Color("168")).SetString("Copied!"),
114 text,
115 )
116 }
117 // Put configured repos first
118 for _, r := range cfg.Repos {
119 items = append(items, Item{
120 Title: r.Name,
121 Name: r.Repo,
122 Description: r.Note,
123 LastUpdate: time.Now(),
124 URL: yank(repoUrl(cfg, r.Name)),
125 })
126 }
127 for _, r := range cfg.Source.AllRepos() {
128 exists := false
129 for _, item := range items {
130 item := item.(Item)
131 if item.Name == r.Name() {
132 exists = true
133 break
134 }
135 }
136 if !exists {
137 items = append(items, Item{
138 Title: r.Name(),
139 Name: r.Name(),
140 Description: "",
141 LastUpdate: time.Now(),
142 URL: yank(repoUrl(cfg, r.Name())),
143 })
144 }
145 }
146 return tea.Batch(
147 s.selector.Init(),
148 s.selector.SetItems(items),
149 )
150}
151
152// Update implements tea.Model.
153func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
154 cmds := make([]tea.Cmd, 0)
155 switch msg := msg.(type) {
156 case tea.WindowSizeMsg:
157 r, cmd := s.readme.Update(msg)
158 s.readme = r.(*code.Code)
159 if cmd != nil {
160 cmds = append(cmds, cmd)
161 }
162 m, cmd := s.selector.Update(msg)
163 s.selector = m.(*selector.Selector)
164 if cmd != nil {
165 cmds = append(cmds, cmd)
166 }
167 case selector.ActiveMsg:
168 cmds = append(cmds, s.changeActive(msg))
169 // reset readme position
170 s.readme.GotoTop()
171 case tea.KeyMsg:
172 switch {
173 case key.Matches(msg, s.common.Keymap.Section):
174 s.activeBox = (s.activeBox + 1) % 2
175 }
176 }
177 switch s.activeBox {
178 case readmeBox:
179 r, cmd := s.readme.Update(msg)
180 s.readme = r.(*code.Code)
181 if cmd != nil {
182 cmds = append(cmds, cmd)
183 }
184 case selectorBox:
185 m, cmd := s.selector.Update(msg)
186 s.selector = m.(*selector.Selector)
187 if cmd != nil {
188 cmds = append(cmds, cmd)
189 }
190 }
191 return s, tea.Batch(cmds...)
192}
193
194// View implements tea.Model.
195func (s *Selection) View() string {
196 wm := s.common.Styles.SelectorBox.GetWidth() +
197 s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
198 s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
199 hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
200 rs := s.common.Styles.ReadmeBox.Copy().
201 Width(s.common.Width - wm).
202 Height(s.common.Height - hm)
203 if s.activeBox == readmeBox {
204 rs.BorderForeground(s.common.Styles.ActiveBorderColor)
205 }
206 readme := rs.Render(s.readme.View())
207 return lipgloss.JoinHorizontal(
208 lipgloss.Top,
209 readme,
210 s.selector.View(),
211 )
212}
213
214func (s *Selection) changeActive(msg selector.ActiveMsg) tea.Cmd {
215 cfg := s.s.Config()
216 r, err := cfg.Source.GetRepo(string(msg))
217 if err != nil {
218 return common.ErrorCmd(err)
219 }
220 rm, rp := r.Readme()
221 return s.readme.SetContent(rm, rp)
222}
223
224func repoUrl(cfg *appCfg.Config, name string) string {
225 port := ""
226 if cfg.Port != 22 {
227 port += fmt.Sprintf(":%d", cfg.Port)
228 }
229 return fmt.Sprintf("git clone ssh://%s/%s", cfg.Host+port, name)
230}