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