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 ui.SetSize(c.Width, c.Height)
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.Header.GetHeight() +
68 ui.common.Styles.Header.GetVerticalFrameSize() +
69 ui.common.Styles.Footer.GetVerticalFrameSize() +
70 ui.footer.Height()
71 return
72}
73
74// ShortHelp implements help.KeyMap.
75func (ui *UI) ShortHelp() []key.Binding {
76 b := make([]key.Binding, 0)
77 switch ui.state {
78 case errorState:
79 b = append(b, ui.common.KeyMap.Back)
80 case loadedState:
81 b = append(b, ui.pages[ui.activePage].ShortHelp()...)
82 }
83 b = append(b,
84 ui.common.KeyMap.Quit,
85 ui.common.KeyMap.Help,
86 )
87 return b
88}
89
90// FullHelp implements help.KeyMap.
91func (ui *UI) FullHelp() [][]key.Binding {
92 b := make([][]key.Binding, 0)
93 switch ui.state {
94 case errorState:
95 b = append(b, []key.Binding{ui.common.KeyMap.Back})
96 case loadedState:
97 b = append(b, ui.pages[ui.activePage].FullHelp()...)
98 }
99 b = append(b, []key.Binding{
100 ui.common.KeyMap.Quit,
101 ui.common.KeyMap.Help,
102 })
103 return b
104}
105
106// SetSize implements common.Component.
107func (ui *UI) SetSize(width, height int) {
108 ui.common.SetSize(width, height)
109 wm, hm := ui.getMargins()
110 ui.header.SetSize(width-wm, height-hm)
111 ui.footer.SetSize(width-wm, height-hm)
112 for _, p := range ui.pages {
113 if p != nil {
114 p.SetSize(width-wm, height-hm)
115 }
116 }
117}
118
119// Init implements tea.Model.
120func (ui *UI) Init() tea.Cmd {
121 ui.pages[selectionPage] = selection.New(ui.s, ui.common)
122 ui.pages[repoPage] = repo.New(ui.s, ui.common)
123 ui.SetSize(ui.common.Width, ui.common.Height)
124 cmds := make([]tea.Cmd, 0)
125 cmds = append(cmds,
126 ui.pages[selectionPage].Init(),
127 ui.pages[repoPage].Init(),
128 )
129 if ui.initialRepo != "" {
130 cmds = append(cmds, ui.initialRepoCmd(ui.initialRepo))
131 }
132 ui.state = loadedState
133 return tea.Batch(cmds...)
134}
135
136// Update implements tea.Model.
137func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
138 log.Printf("msg: %T", msg)
139 cmds := make([]tea.Cmd, 0)
140 switch msg := msg.(type) {
141 case tea.WindowSizeMsg:
142 ui.SetSize(msg.Width, msg.Height)
143 for i, p := range ui.pages {
144 m, cmd := p.Update(msg)
145 ui.pages[i] = m.(common.Page)
146 if cmd != nil {
147 cmds = append(cmds, cmd)
148 }
149 }
150 case tea.KeyMsg, tea.MouseMsg:
151 switch msg := msg.(type) {
152 case tea.KeyMsg:
153 switch {
154 case key.Matches(msg, ui.common.KeyMap.Back) && ui.error != nil:
155 ui.error = nil
156 ui.state = loadedState
157 case key.Matches(msg, ui.common.KeyMap.Help):
158 ui.footer.SetShowAll(!ui.footer.ShowAll())
159 case key.Matches(msg, ui.common.KeyMap.Quit):
160 return ui, tea.Quit
161 case ui.activePage == repoPage && key.Matches(msg, ui.common.KeyMap.Back):
162 ui.activePage = selectionPage
163 }
164 }
165 case common.ErrorMsg:
166 ui.error = msg
167 ui.state = errorState
168 return ui, nil
169 case selector.SelectMsg:
170 switch msg.IdentifiableItem.(type) {
171 case selection.Item:
172 if ui.activePage == selectionPage {
173 cmds = append(cmds, ui.setRepoCmd(msg.ID()))
174 }
175 }
176 }
177 h, cmd := ui.header.Update(msg)
178 ui.header = h.(*header.Header)
179 if cmd != nil {
180 cmds = append(cmds, cmd)
181 }
182 f, cmd := ui.footer.Update(msg)
183 ui.footer = f.(*footer.Footer)
184 if cmd != nil {
185 cmds = append(cmds, cmd)
186 }
187 if ui.state == loadedState {
188 m, cmd := ui.pages[ui.activePage].Update(msg)
189 ui.pages[ui.activePage] = m.(common.Page)
190 if cmd != nil {
191 cmds = append(cmds, cmd)
192 }
193 }
194 // This fixes determining the height margin of the footer.
195 ui.SetSize(ui.common.Width, ui.common.Height)
196 return ui, tea.Batch(cmds...)
197}
198
199// View implements tea.Model.
200func (ui *UI) View() string {
201 var view string
202 wm, hm := ui.getMargins()
203 footer := ui.footer.View()
204 style := ui.common.Styles.App.Copy()
205 switch ui.state {
206 case startState:
207 view = "Loading..."
208 case errorState:
209 err := ui.common.Styles.ErrorTitle.Render("Bummer")
210 err += ui.common.Styles.ErrorBody.Render(ui.error.Error())
211 view = ui.common.Styles.Error.Copy().
212 Width(ui.common.Width -
213 wm -
214 ui.common.Styles.ErrorBody.GetHorizontalFrameSize()).
215 Height(ui.common.Height -
216 hm -
217 ui.common.Styles.Error.GetVerticalFrameSize()).
218 Render(err)
219 case loadedState:
220 view = ui.pages[ui.activePage].View()
221 default:
222 view = "Unknown state :/ this is a bug!"
223 }
224 return style.Render(
225 lipgloss.JoinVertical(lipgloss.Bottom,
226 ui.header.View(),
227 view,
228 footer,
229 ),
230 )
231}
232
233func (ui *UI) setRepoCmd(rn string) tea.Cmd {
234 rs := ui.s.Source()
235 return func() tea.Msg {
236 for _, r := range rs.AllRepos() {
237 if r.Repo() == rn {
238 ui.activePage = repoPage
239 return repo.RepoMsg(r)
240 }
241 }
242 return common.ErrorMsg(git.ErrMissingRepo)
243 }
244}
245
246func (ui *UI) initialRepoCmd(rn string) tea.Cmd {
247 rs := ui.s.Source()
248 return func() tea.Msg {
249 for _, r := range rs.AllRepos() {
250 if r.Repo() == rn {
251 ui.activePage = repoPage
252 return repo.RepoMsg(r)
253 }
254 }
255 return nil
256 }
257}