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