1package ui
2
3import (
4 "log"
5 "os"
6
7 "github.com/charmbracelet/bubbles/key"
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/charmbracelet/lipgloss"
10 "github.com/charmbracelet/soft-serve/config"
11 "github.com/charmbracelet/soft-serve/ui/common"
12 "github.com/charmbracelet/soft-serve/ui/components/footer"
13 "github.com/charmbracelet/soft-serve/ui/components/header"
14 "github.com/charmbracelet/soft-serve/ui/components/selector"
15 "github.com/charmbracelet/soft-serve/ui/git"
16 "github.com/charmbracelet/soft-serve/ui/pages/repo"
17 "github.com/charmbracelet/soft-serve/ui/pages/selection"
18 "github.com/gliderlabs/ssh"
19)
20
21type page int
22
23const (
24 selectionPage page = iota
25 repoPage
26)
27
28type sessionState int
29
30const (
31 startState sessionState = iota
32 errorState
33 loadedState
34)
35
36// UI is the main UI model.
37type UI struct {
38 cfg *config.Config
39 session ssh.Session
40 rs git.GitRepoSource
41 initialRepo string
42 common common.Common
43 pages []common.Page
44 activePage page
45 state sessionState
46 header *header.Header
47 footer *footer.Footer
48 error error
49}
50
51// New returns a new UI model.
52func New(cfg *config.Config, s ssh.Session, c common.Common, initialRepo string) *UI {
53 src := &source{cfg.Source}
54 h := header.New(c, cfg.Name)
55 ui := &UI{
56 cfg: cfg,
57 session: s,
58 rs: src,
59 common: c,
60 pages: make([]common.Page, 2), // selection & repo
61 activePage: selectionPage,
62 state: startState,
63 header: h,
64 initialRepo: initialRepo,
65 }
66 ui.footer = footer.New(c, ui)
67 return ui
68}
69
70func (ui *UI) getMargins() (wm, hm int) {
71 wm = ui.common.Styles.App.GetHorizontalFrameSize()
72 hm = ui.common.Styles.App.GetVerticalFrameSize() +
73 ui.common.Styles.Footer.GetVerticalFrameSize() +
74 ui.footer.Height()
75 switch ui.activePage {
76 case selectionPage:
77 hm += ui.common.Styles.Header.GetHeight() +
78 ui.common.Styles.Header.GetVerticalFrameSize()
79 case repoPage:
80 }
81 return
82}
83
84// ShortHelp implements help.KeyMap.
85func (ui *UI) ShortHelp() []key.Binding {
86 b := make([]key.Binding, 0)
87 switch ui.state {
88 case errorState:
89 b = append(b, ui.common.KeyMap.Back)
90 case loadedState:
91 b = append(b, ui.pages[ui.activePage].ShortHelp()...)
92 }
93 b = append(b,
94 ui.common.KeyMap.Quit,
95 ui.common.KeyMap.Help,
96 )
97 return b
98}
99
100// FullHelp implements help.KeyMap.
101func (ui *UI) FullHelp() [][]key.Binding {
102 b := make([][]key.Binding, 0)
103 switch ui.state {
104 case errorState:
105 b = append(b, []key.Binding{ui.common.KeyMap.Back})
106 case loadedState:
107 b = append(b, ui.pages[ui.activePage].FullHelp()...)
108 }
109 b = append(b, []key.Binding{
110 ui.common.KeyMap.Quit,
111 ui.common.KeyMap.Help,
112 })
113 return b
114}
115
116// SetSize implements common.Component.
117func (ui *UI) SetSize(width, height int) {
118 ui.common.SetSize(width, height)
119 wm, hm := ui.getMargins()
120 ui.header.SetSize(width-wm, height-hm)
121 ui.footer.SetSize(width-wm, height-hm)
122 for _, p := range ui.pages {
123 if p != nil {
124 p.SetSize(width-wm, height-hm)
125 }
126 }
127}
128
129// Init implements tea.Model.
130func (ui *UI) Init() tea.Cmd {
131 ui.pages[selectionPage] = selection.New(
132 ui.cfg,
133 ui.session.PublicKey(),
134 ui.common,
135 )
136 ui.pages[repoPage] = repo.New(
137 ui.cfg,
138 ui.rs,
139 ui.common,
140 )
141 ui.SetSize(ui.common.Width, ui.common.Height)
142 cmds := make([]tea.Cmd, 0)
143 cmds = append(cmds,
144 ui.pages[selectionPage].Init(),
145 ui.pages[repoPage].Init(),
146 )
147 if ui.initialRepo != "" {
148 cmds = append(cmds, ui.initialRepoCmd(ui.initialRepo))
149 }
150 ui.state = loadedState
151 ui.SetSize(ui.common.Width, ui.common.Height)
152 return tea.Batch(cmds...)
153}
154
155// Update implements tea.Model.
156func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
157 if os.Getenv("DEBUG") == "true" {
158 log.Printf("ui msg: %T", msg)
159 }
160 cmds := make([]tea.Cmd, 0)
161 switch msg := msg.(type) {
162 case tea.WindowSizeMsg:
163 ui.SetSize(msg.Width, msg.Height)
164 for i, p := range ui.pages {
165 m, cmd := p.Update(msg)
166 ui.pages[i] = m.(common.Page)
167 if cmd != nil {
168 cmds = append(cmds, cmd)
169 }
170 }
171 case tea.KeyMsg, tea.MouseMsg:
172 switch msg := msg.(type) {
173 case tea.KeyMsg:
174 switch {
175 case key.Matches(msg, ui.common.KeyMap.Back) && ui.error != nil:
176 ui.error = nil
177 ui.state = loadedState
178 case key.Matches(msg, ui.common.KeyMap.Help):
179 ui.footer.SetShowAll(!ui.footer.ShowAll())
180 case key.Matches(msg, ui.common.KeyMap.Quit):
181 return ui, tea.Quit
182 case ui.activePage == repoPage && key.Matches(msg, ui.common.KeyMap.Back):
183 ui.activePage = selectionPage
184 }
185 }
186 case common.ErrorMsg:
187 ui.error = msg
188 ui.state = errorState
189 return ui, nil
190 case selector.SelectMsg:
191 switch msg.IdentifiableItem.(type) {
192 case selection.Item:
193 if ui.activePage == selectionPage {
194 cmds = append(cmds, ui.setRepoCmd(msg.ID()))
195 }
196 }
197 }
198 h, cmd := ui.header.Update(msg)
199 ui.header = h.(*header.Header)
200 if cmd != nil {
201 cmds = append(cmds, cmd)
202 }
203 f, cmd := ui.footer.Update(msg)
204 ui.footer = f.(*footer.Footer)
205 if cmd != nil {
206 cmds = append(cmds, cmd)
207 }
208 if ui.state == loadedState {
209 m, cmd := ui.pages[ui.activePage].Update(msg)
210 ui.pages[ui.activePage] = m.(common.Page)
211 if cmd != nil {
212 cmds = append(cmds, cmd)
213 }
214 }
215 // This fixes determining the height margin of the footer.
216 ui.SetSize(ui.common.Width, ui.common.Height)
217 return ui, tea.Batch(cmds...)
218}
219
220// View implements tea.Model.
221func (ui *UI) View() string {
222 var view string
223 wm, hm := ui.getMargins()
224 footer := ui.footer.View()
225 style := ui.common.Styles.App.Copy()
226 switch ui.state {
227 case startState:
228 view = "Loading..."
229 case errorState:
230 err := ui.common.Styles.ErrorTitle.Render("Bummer")
231 err += ui.common.Styles.ErrorBody.Render(ui.error.Error())
232 view = ui.common.Styles.Error.Copy().
233 Width(ui.common.Width -
234 wm -
235 ui.common.Styles.ErrorBody.GetHorizontalFrameSize()).
236 Height(ui.common.Height -
237 hm -
238 ui.common.Styles.Error.GetVerticalFrameSize()).
239 Render(err)
240 case loadedState:
241 view = ui.pages[ui.activePage].View()
242 default:
243 view = "Unknown state :/ this is a bug!"
244 }
245 switch ui.activePage {
246 case selectionPage:
247 view = lipgloss.JoinVertical(lipgloss.Bottom,
248 ui.header.View(),
249 view,
250 )
251 case repoPage:
252 }
253 return style.Render(
254 lipgloss.JoinVertical(lipgloss.Bottom,
255 view,
256 footer,
257 ),
258 )
259}
260
261func (ui *UI) setRepoCmd(rn string) tea.Cmd {
262 return func() tea.Msg {
263 for _, r := range ui.rs.AllRepos() {
264 if r.Repo() == rn {
265 ui.activePage = repoPage
266 return repo.RepoMsg(r)
267 }
268 }
269 return common.ErrorMsg(git.ErrMissingRepo)
270 }
271}
272
273func (ui *UI) initialRepoCmd(rn string) tea.Cmd {
274 return func() tea.Msg {
275 for _, r := range ui.rs.AllRepos() {
276 if r.Repo() == rn {
277 ui.activePage = repoPage
278 return repo.RepoMsg(r)
279 }
280 }
281 return nil
282 }
283}