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