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