ui.go

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