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	cmd := tea.Batch(
122		ui.pages[selectionPage].Init(),
123		ui.pages[repoPage].Init(),
124	)
125	var msg tea.Msg
126	if cmd != nil {
127		msg = cmd()
128		switch msg := msg.(type) {
129		case common.ErrorMsg:
130			ui.state = errorState
131			return common.ErrorCmd(msg)
132		}
133	}
134	ui.state = loadedState
135	if msg != nil {
136		return func() tea.Msg { return msg }
137	}
138	return nil
139}
140
141// Update implements tea.Model.
142func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
143	log.Printf("msg: %T", msg)
144	cmds := make([]tea.Cmd, 0)
145	switch msg := msg.(type) {
146	case tea.WindowSizeMsg:
147		ui.SetSize(msg.Width, msg.Height)
148		for i, p := range ui.pages {
149			m, cmd := p.Update(msg)
150			ui.pages[i] = m.(common.Page)
151			if cmd != nil {
152				cmds = append(cmds, cmd)
153			}
154		}
155	case tea.KeyMsg, tea.MouseMsg:
156		switch msg := msg.(type) {
157		case tea.KeyMsg:
158			switch {
159			case key.Matches(msg, ui.common.KeyMap.Back) && ui.error != nil:
160				ui.error = nil
161				ui.state = loadedState
162			case key.Matches(msg, ui.common.KeyMap.Help):
163				ui.footer.SetShowAll(!ui.footer.ShowAll())
164			case key.Matches(msg, ui.common.KeyMap.Quit):
165				return ui, tea.Quit
166			case ui.activePage == repoPage && key.Matches(msg, ui.common.KeyMap.Back):
167				ui.activePage = selectionPage
168			}
169		}
170	case common.ErrorMsg:
171		ui.error = msg
172		ui.state = errorState
173		return ui, nil
174	case selector.SelectMsg:
175		switch msg.IdentifiableItem.(type) {
176		case selection.Item:
177			if ui.activePage == selectionPage {
178				ui.activePage = repoPage
179				cmds = append(cmds, ui.setRepoCmd(msg.ID()))
180			}
181		}
182	}
183	h, cmd := ui.header.Update(msg)
184	ui.header = h.(*header.Header)
185	if cmd != nil {
186		cmds = append(cmds, cmd)
187	}
188	f, cmd := ui.footer.Update(msg)
189	ui.footer = f.(*footer.Footer)
190	if cmd != nil {
191		cmds = append(cmds, cmd)
192	}
193	if ui.state == loadedState {
194		m, cmd := ui.pages[ui.activePage].Update(msg)
195		ui.pages[ui.activePage] = m.(common.Page)
196		if cmd != nil {
197			cmds = append(cmds, cmd)
198		}
199	}
200	// This fixes determining the height margin of the footer.
201	ui.SetSize(ui.common.Width, ui.common.Height)
202	return ui, tea.Batch(cmds...)
203}
204
205// View implements tea.Model.
206func (ui *UI) View() string {
207	var view string
208	wm, hm := ui.getMargins()
209	footer := ui.footer.View()
210	style := ui.common.Styles.App.Copy()
211	switch ui.state {
212	case startState:
213		view = "Loading..."
214	case errorState:
215		err := ui.common.Styles.ErrorTitle.Render("Bummer")
216		err += ui.common.Styles.ErrorBody.Render(ui.error.Error())
217		view = ui.common.Styles.Error.Copy().
218			Width(ui.common.Width -
219				wm -
220				ui.common.Styles.ErrorBody.GetHorizontalFrameSize()).
221			Height(ui.common.Height -
222				hm -
223				ui.common.Styles.Error.GetVerticalFrameSize()).
224			Render(err)
225	case loadedState:
226		view = ui.pages[ui.activePage].View()
227	default:
228		view = "Unknown state :/ this is a bug!"
229	}
230	return style.Render(
231		lipgloss.JoinVertical(lipgloss.Bottom,
232			ui.header.View(),
233			view,
234			footer,
235		),
236	)
237}
238
239func (ui *UI) setRepoCmd(rn string) tea.Cmd {
240	rs := ui.s.Config().Source
241	return func() tea.Msg {
242		for _, r := range rs.AllRepos() {
243			if r.Name() == rn {
244				return repo.RepoMsg(r)
245			}
246		}
247		return common.ErrorMsg(git.ErrMissingRepo)
248	}
249}