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