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