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